This commit is contained in:
Developer
2026-05-18 17:52:09 +08:00
commit cae04eead5
62 changed files with 11230 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
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/
.flutter-plugins-dependencies
/build/
/coverage/

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# 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'

3
CHANGELOG.md Normal file
View File

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

1
LICENSE Normal file
View File

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

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# printer
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
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.

4
analysis_options.yaml Normal file
View File

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

9
android/.gitignore vendored Normal file
View File

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

91
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,91 @@
group = "com.xiarui.printer"
version = "1.0"
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.1")
}
}
allprojects {
repositories {
google()
mavenCentral()
flatDir {
dirs("libs")
}
}
}
plugins {
id("com.android.library")
}
android {
namespace = "com.xiarui.printer"
compileSdk = 36
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
minSdk = 24
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
}
}
packaging {
jniLibs {
pickFirsts += listOf(
"lib/arm64-v8a/libjnidispatch.so",
"lib/armeabi-v7a/libjnidispatch.so",
"lib/x86/libjnidispatch.so",
"lib/x86_64/libjnidispatch.so"
)
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
testOptions {
unitTests.all {
it.outputs.upToDateWhen { false }
it.testLogging {
events("passed", "skipped", "failed", "standardOut", "standardError")
showStandardStreams = true
}
}
}
}
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0")
implementation(files("libs/autoreplyprint.aar"))
}
// Skip AAR bundling — local .aar dependency prevents bundle* tasks from succeeding.
// The AAR is available at runtime via flatDir repo; intermediate outputs are sufficient.
tasks.whenTaskAdded {
if (name.startsWith("bundleDebugAar") || name.startsWith("bundleReleaseAar")) {
enabled = false
}
}

Binary file not shown.

8
android/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,8 @@
# Keep Flutter plugin classes
-keep class com.xiarui.printer.** { *; }
-keep class io.flutter.embedding.engine.plugins.** { *; }
# Keep third-party SDKs
-keep class com.caysn.autoreplyprint.** { *; }
-keep class com.sun.jna.** { *; }
-keep class com.lvrenyang.** { *; }

1
android/settings.gradle Normal file
View File

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

View File

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

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xiarui.printer">
</manifest>

View File

@@ -0,0 +1,186 @@
package com.xiarui.printer;
import com.caysn.autoreplyprint.AutoReplyPrint;
import com.sun.jna.Pointer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Manages port handles for the Flutter plugin.
* <p>
* Provides thread-safe registration, retrieval, and cleanup of native port handles.
* Each registered Pointer is assigned a unique integer handle starting from 1.
* </p>
*/
public class PortHandleManager {
/**
* Interface for port operations to enable unit testing without native library.
*/
public interface PortOperations {
/**
* Close a port.
*
* @param handle native pointer to the port
* @return true if successfully closed, false otherwise
*/
boolean closePort(Pointer handle);
/**
* Check if a port is opened.
*
* @param handle native pointer to the port
* @return true if port is open, false otherwise
*/
boolean isPortOpened(Pointer handle);
}
/**
* Default PortOperations implementation using AutoReplyPrint native SDK.
*/
private static class NativePortOperations implements PortOperations {
@Override
public boolean closePort(Pointer handle) {
return AutoReplyPrint.INSTANCE.CP_Port_Close(handle);
}
@Override
public boolean isPortOpened(Pointer handle) {
return AutoReplyPrint.INSTANCE.CP_Port_IsOpened(handle);
}
}
// Singleton instance
private static volatile PortHandleManager instance;
// Handle registry: integer handle -> native Pointer
private final ConcurrentHashMap<Integer, Pointer> handleRegistry = new ConcurrentHashMap<>();
// Atomic counter starting from 1
private final AtomicInteger counter = new AtomicInteger(1);
// Port operations abstraction for testability
private final PortOperations portOps;
/**
* Private constructor. Use {@link #getInstance()} for production,
* or inject custom PortOperations for testing.
*/
private PortHandleManager() {
this(new NativePortOperations());
}
/**
* Constructor with injectable PortOperations for testing.
*
* @param portOps port operations implementation
*/
PortHandleManager(PortOperations portOps) {
this.portOps = portOps;
}
/**
* Returns the singleton instance using native AutoReplyPrint operations.
*
* @return singleton PortHandleManager
*/
public static PortHandleManager getInstance() {
PortHandleManager localInstance = instance;
if (localInstance == null) {
synchronized (PortHandleManager.class) {
localInstance = instance;
if (localInstance == null) {
instance = localInstance = new PortHandleManager();
}
}
}
return localInstance;
}
/**
* Registers a native port pointer and returns a unique integer handle.
* <p>
* If the pointer is null or has a zero native value, registration fails
* and returns -1.
* </p>
*
* @param pointer native port pointer to register
* @return positive integer handle on success, -1 on failure
*/
public int registerHandle(Pointer pointer) {
if (pointer == null || Pointer.nativeValue(pointer) == 0) {
return -1;
}
int handle = counter.getAndIncrement();
handleRegistry.put(handle, pointer);
return handle;
}
/**
* Retrieves the native pointer for a registered handle.
*
* @param handle integer handle to look up
* @return the registered Pointer, or null if not found
*/
public Pointer getHandle(int handle) {
return handleRegistry.get(handle);
}
/**
* Closes a registered port and removes it from the registry.
* <p>
* Uses ConcurrentHashMap.compute() for atomic removal to prevent
* race conditions (fixes C-04). The port is only closed if it was
* successfully removed from the registry, preventing double-close.
* </p>
*
* @param handle integer handle to close
* @return true if port was found and closed, false otherwise
*/
public boolean closeHandle(int handle) {
Pointer pointer = handleRegistry.compute(handle, (key, existing) -> null);
if (pointer != null) {
return portOps.closePort(pointer);
}
return false;
}
/**
* Checks if a handle is valid and the underlying port is still opened.
*
* @param handle integer handle to check
* @return true if handle exists and port is opened, false otherwise
*/
public boolean isHandleValid(int handle) {
Pointer pointer = handleRegistry.get(handle);
if (pointer == null) {
return false;
}
return portOps.isPortOpened(pointer);
}
/**
* Closes all registered ports and clears the registry.
* <p>
* Iterates through all registered handles, closing each one.
* Called during engine detachment to clean up resources.
* </p>
*/
public void closeAll() {
ConcurrentHashMap<Integer, Pointer> snapshot = new ConcurrentHashMap<>(handleRegistry);
for (Integer handle : snapshot.keySet()) {
closeHandle(handle);
}
handleRegistry.clear();
}
/**
* Returns the underlying registry for testing purposes only.
*
* @return the handle registry map
*/
ConcurrentHashMap<Integer, Pointer> getRegistry() {
return handleRegistry;
}
}

View File

@@ -0,0 +1,887 @@
package com.xiarui.printer;
import androidx.annotation.NonNull;
import com.caysn.autoreplyprint.AutoReplyPrint;
import com.sun.jna.Pointer;
import com.sun.jna.WString;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.os.Handler;
import android.os.Looper;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
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;
/**
* Flutter plugin for printer communication via serial/USB ports.
* <p>
* Provides MethodChannel handlers for opening, closing, and enumerating
* printer ports using the autoreplyprint AAR SDK.
* </p>
*/
public class PrinterPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
private PortHandleManager portHandleManager;
/**
* Valid range for baud rate: 1200 to 921600.
*/
private static final int MIN_BAUD_RATE = 1200;
private static final int MAX_BAUD_RATE = 921600;
/**
* Valid range for data bits: 5 to 8.
*/
private static final int MIN_DATA_BITS = 5;
private static final int MAX_DATA_BITS = 8;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "printer");
channel.setMethodCallHandler(this);
portHandleManager = PortHandleManager.getInstance();
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "getPlatformVersion":
result.success("Android " + android.os.Build.VERSION.RELEASE);
break;
case "openComPort":
handleOpenComPort(call, result);
break;
case "openUsbPort":
handleOpenUsbPort(call, result);
break;
case "closePort":
handleClosePort(call, result);
break;
case "isPortOpened":
handleIsPortOpened(call, result);
break;
case "enumComPorts":
handleEnumComPorts(call, result);
break;
case "enumUsbPorts":
handleEnumUsbPorts(call, result);
break;
case "setMultiByteMode":
handleSetMultiByteMode(call, result);
break;
case "setMultiByteEncoding":
handleSetMultiByteEncoding(call, result);
break;
case "printText":
handlePrintText(call, result);
break;
case "setAlignment":
handleSetAlignment(call, result);
break;
case "setTextScale":
handleSetTextScale(call, result);
break;
case "setTextBold":
handleSetTextBold(call, result);
break;
case "setTextUnderline":
handleSetTextUnderline(call, result);
break;
case "feedLine":
handleFeedLine(call, result);
break;
case "feedDot":
handleFeedDot(call, result);
break;
case "halfCutPaper":
handleHalfCutPaper(call, result);
break;
case "fullCutPaper":
handleFullCutPaper(call, result);
break;
default:
result.notImplemented();
break;
}
}
/**
* Handles openComPort method call.
* <p>
* Opens a serial port with the specified parameters on a background thread
* and returns the result on the main thread via Handler.
* Validates portName (non-empty), baudRate (1200-921600), and dataBits (5-8).
* Implements PORT-01, PORT-02, T-01-01.
* </p>
*
* @param call the method call containing port parameters
* @param result the result callback
*/
private void handleOpenComPort(@NonNull MethodCall call, @NonNull Result result) {
String portName = call.argument("portName");
Integer baudRate = call.argument("baudRate");
Integer dataBits = call.argument("dataBits");
Integer parity = call.argument("parity");
Integer stopBits = call.argument("stopBits");
Integer flowControl = call.argument("flowControl");
Integer autoReplyMode = call.argument("autoReplyMode");
// Validate port name (T-01-01)
if (portName == null || portName.isEmpty()) {
result.error("INVALID_ARGUMENT", "portName is required and cannot be empty", null);
return;
}
// Validate baud rate
if (baudRate == null || baudRate < MIN_BAUD_RATE || baudRate > MAX_BAUD_RATE) {
result.error(
"INVALID_ARGUMENT",
"baudRate must be between " + MIN_BAUD_RATE + " and " + MAX_BAUD_RATE,
null
);
return;
}
// Validate data bits (C-02 fix)
int db = dataBits != null ? dataBits : 8;
if (db < MIN_DATA_BITS || db > MAX_DATA_BITS) {
result.error(
"INVALID_ARGUMENT",
"dataBits must be between " + MIN_DATA_BITS + " and " + MAX_DATA_BITS,
null
);
return;
}
// Defaults
int p = parity != null ? parity : 0;
int sb = stopBits != null ? stopBits : 0;
int fc = flowControl != null ? flowControl : 0;
int arm = autoReplyMode != null ? autoReplyMode : 1;
// Execute on background thread (C-01 fix)
final String finalPortName = portName;
final int finalBaudRate = baudRate;
final int finalDataBits = db;
final int finalParity = p;
final int finalStopBits = sb;
final int finalFlowControl = fc;
final int finalAutoReplyMode = arm;
final Handler mainHandler = new Handler(Looper.getMainLooper());
new Thread(() -> {
try {
Pointer pointer = AutoReplyPrint.INSTANCE.CP_Port_OpenCom(
finalPortName,
finalBaudRate,
finalDataBits,
finalParity,
finalStopBits,
finalFlowControl,
finalAutoReplyMode
);
if (pointer == null || Pointer.nativeValue(pointer) == 0) {
final Pointer finalPointer = pointer;
mainHandler.post(() ->
result.error("PORT_OPEN_FAILED", "Failed to open port: " + finalPortName, null)
);
return;
}
int dartHandle = portHandleManager.registerHandle(pointer);
mainHandler.post(() -> result.success(dartHandle));
} catch (UnsatisfiedLinkError e) {
mainHandler.post(() ->
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null)
);
} catch (Exception e) {
mainHandler.post(() ->
result.error("UNKNOWN_ERROR", e.getMessage(), null)
);
}
}).start();
}
/**
* Handles openUsbPort method call.
* <p>
* Opens a USB port on a background thread and returns the result on the main thread.
* Validates portName (non-empty) and autoReplyMode (0 or 1).
* Implements T-01-02.
* </p>
*
* @param call the method call containing port parameters
* @param result the result callback
*/
private void handleOpenUsbPort(@NonNull MethodCall call, @NonNull Result result) {
String portName = call.argument("portName");
Integer autoReplyMode = call.argument("autoReplyMode");
// Validate port name (T-01-02)
if (portName == null || portName.isEmpty()) {
result.error("INVALID_ARGUMENT", "portName is required and cannot be empty", null);
return;
}
int arm = autoReplyMode != null ? autoReplyMode : 1;
final String finalPortName = portName;
final int finalAutoReplyMode = arm;
final Handler mainHandler = new Handler(Looper.getMainLooper());
new Thread(() -> {
try {
Pointer pointer = AutoReplyPrint.INSTANCE.CP_Port_OpenUsb(
finalPortName,
finalAutoReplyMode
);
if (pointer == null || Pointer.nativeValue(pointer) == 0) {
mainHandler.post(() ->
result.error("PORT_OPEN_FAILED", "Failed to open USB port: " + finalPortName, null)
);
return;
}
int dartHandle = portHandleManager.registerHandle(pointer);
mainHandler.post(() -> result.success(dartHandle));
} catch (UnsatisfiedLinkError e) {
mainHandler.post(() ->
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null)
);
} catch (Exception e) {
mainHandler.post(() ->
result.error("UNKNOWN_ERROR", e.getMessage(), null)
);
}
}).start();
}
/**
* Handles closePort method call.
* <p>
* Closes a port by its integer handle.
* Implements PORT-03, T-01-03.
* </p>
*
* @param call the method call containing handle
* @param result the result callback
*/
private void handleClosePort(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
boolean success = portHandleManager.closeHandle(handle);
result.success(success);
}
/**
* Handles isPortOpened method call.
* <p>
* Checks if a port is currently opened by its integer handle.
* </p>
*
* @param call the method call containing handle
* @param result the result callback
*/
private void handleIsPortOpened(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
boolean isValid = portHandleManager.isHandleValid(handle);
result.success(isValid);
}
/**
* Handles enumComPorts method call.
* <p>
* Enumerates available serial ports and returns a list of port names.
* </p>
*
* @param call the method call (no arguments)
* @param result the result callback
*/
private void handleEnumComPorts(@NonNull MethodCall call, @NonNull Result result) {
try {
String[] ports = AutoReplyPrint.CP_Port_EnumCom_Helper.EnumCom();
List<String> portList = (ports != null) ? Arrays.asList(ports) : new ArrayList<>();
result.success(portList);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles enumUsbPorts method call.
* <p>
* Enumerates available USB ports and returns a list of port names.
* </p>
*
* @param call the method call (no arguments)
* @param result the result callback
*/
private void handleEnumUsbPorts(@NonNull MethodCall call, @NonNull Result result) {
try {
String[] ports = AutoReplyPrint.CP_Port_EnumUsb_Helper.EnumUsb();
List<String> portList = (ports != null) ? Arrays.asList(ports) : new ArrayList<>();
result.success(portList);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles setMultiByteMode method call.
* <p>
* Sets the printer to multi-byte encoding mode.
* Must be called before setMultiByteEncoding and printText for Chinese/multibyte text.
* Implements PRINT-01.
* </p>
*
* @param call the method call containing handle
* @param result the result callback, returns true on success
*/
private void handleSetMultiByteMode(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetMultiByteMode(pointer);
if (!success) {
result.error("PRINT_FAILED", "setMultiByteMode failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles setMultiByteEncoding method call.
* <p>
* Sets the multi-byte character encoding for the printer.
* Valid encoding values: 0=GBK, 1=UTF8, 3=BIG5, 4=ShiftJIS, 5=EUC_KR.
* Implements PRINT-01.
* </p>
*
* @param call the method call containing handle and encoding (int)
* @param result the result callback, returns true on success
*/
private void handleSetMultiByteEncoding(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
Integer encoding = call.argument("encoding");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (encoding == null) {
result.error("INVALID_ARGUMENT", "encoding is required", null);
return;
}
if (encoding != 0 && encoding != 1 && encoding != 3 && encoding != 4 && encoding != 5) {
result.error("INVALID_ARGUMENT",
"encoding must be one of {0=GBK, 1=UTF8, 3=BIG5, 4=ShiftJIS, 5=EUC_KR}", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetMultiByteEncoding(pointer, encoding);
if (!success) {
result.error("PRINT_FAILED", "setMultiByteEncoding failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles printText method call.
* <p>
* Prints text using UTF-8 encoding (WString).
* Requires multi-byte mode and UTF-8 encoding to be set beforehand for correct Chinese character output.
* Implements PRINT-02, T-02-03.
* </p>
*
* @param call the method call containing handle and text (String)
* @param result the result callback, returns true on success
*/
private void handlePrintText(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
String text = call.argument("text");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (text == null || text.isEmpty()) {
result.error("INVALID_ARGUMENT", "text is required and cannot be empty", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_PrintTextInUTF8(pointer, new WString(text));
if (!success) {
result.error("PRINT_FAILED", "printText failed (length: " + text.length() + ")", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles setAlignment method call.
* <p>
* Sets the print alignment.
* Valid values: 0=left, 1=center, 2=right.
* Implements PRINT-04.
* </p>
*
* @param call the method call containing handle and alignment (int)
* @param result the result callback, returns true on success
*/
private void handleSetAlignment(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
Integer alignment = call.argument("alignment");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (alignment == null) {
result.error("INVALID_ARGUMENT", "alignment is required", null);
return;
}
if (alignment != 0 && alignment != 1 && alignment != 2) {
result.error("INVALID_ARGUMENT", "alignment must be one of {0=left, 1=center, 2=right}", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetAlignment(pointer, alignment);
if (!success) {
result.error("PRINT_FAILED", "setAlignment failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles setTextScale method call.
* <p>
* Sets text width and height scale factors.
* Both widthScale and heightScale must be in range [1, 8].
* Implements PRINT-04, T-02-02.
* </p>
*
* @param call the method call containing handle, widthScale (int), heightScale (int)
* @param result the result callback, returns true on success
*/
private void handleSetTextScale(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
Integer widthScale = call.argument("widthScale");
Integer heightScale = call.argument("heightScale");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (widthScale == null) {
result.error("INVALID_ARGUMENT", "widthScale is required", null);
return;
}
if (heightScale == null) {
result.error("INVALID_ARGUMENT", "heightScale is required", null);
return;
}
if (widthScale < 1 || widthScale > 8) {
result.error("INVALID_ARGUMENT", "widthScale must be in range [1, 8]", null);
return;
}
if (heightScale < 1 || heightScale > 8) {
result.error("INVALID_ARGUMENT", "heightScale must be in range [1, 8]", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetTextScale(pointer, widthScale, heightScale);
if (!success) {
result.error("PRINT_FAILED", "setTextScale failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles setTextBold method call.
* <p>
* Sets text bold printing.
* Valid values: 0=off, 1=on.
* Implements PRINT-04.
* </p>
*
* @param call the method call containing handle and bold (int, 0 or 1)
* @param result the result callback, returns true on success
*/
private void handleSetTextBold(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
Integer bold = call.argument("bold");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (bold == null) {
result.error("INVALID_ARGUMENT", "bold is required", null);
return;
}
if (bold != 0 && bold != 1) {
result.error("INVALID_ARGUMENT", "bold must be one of {0=off, 1=on}", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetTextBold(pointer, bold);
if (!success) {
result.error("PRINT_FAILED", "setTextBold failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles setTextUnderline method call.
* <p>
* Sets text underline style.
* Valid values: 0=none, 1=1-dot, 2=2-dot.
* Implements PRINT-04, T-02-02.
* </p>
*
* @param call the method call containing handle and underline (int)
* @param result the result callback, returns true on success
*/
private void handleSetTextUnderline(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
Integer underline = call.argument("underline");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (underline == null) {
result.error("INVALID_ARGUMENT", "underline is required", null);
return;
}
if (underline != 0 && underline != 1 && underline != 2) {
result.error("INVALID_ARGUMENT", "underline must be one of {0=none, 1=1-dot, 2=2-dot}", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_SetTextUnderline(pointer, underline);
if (!success) {
result.error("PRINT_FAILED", "setTextUnderline failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles feedLine method call.
* <p>
* Feeds paper by specified number of lines.
* numLines must be greater than 0.
* Implements PRINT-05, T-02-02.
* </p>
*
* @param call the method call containing handle and numLines (int)
* @param result the result callback, returns true on success
*/
private void handleFeedLine(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
Integer numLines = call.argument("numLines");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (numLines == null) {
result.error("INVALID_ARGUMENT", "numLines is required", null);
return;
}
if (numLines <= 0) {
result.error("INVALID_ARGUMENT", "numLines must be greater than 0", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_FeedLine(pointer, numLines);
if (!success) {
result.error("PRINT_FAILED", "feedLine failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles feedDot method call.
* <p>
* Feeds paper by specified number of dots.
* numDots must be greater than 0.
* Implements PRINT-05, T-02-02.
* </p>
*
* @param call the method call containing handle and numDots (int)
* @param result the result callback, returns true on success
*/
private void handleFeedDot(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
Integer numDots = call.argument("numDots");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
if (numDots == null) {
result.error("INVALID_ARGUMENT", "numDots is required", null);
return;
}
if (numDots <= 0) {
result.error("INVALID_ARGUMENT", "numDots must be greater than 0", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_FeedDot(pointer, numDots);
if (!success) {
result.error("PRINT_FAILED", "feedDot failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles halfCutPaper method call.
* <p>
* Triggers half-cut on the paper cutter.
* Implements PRINT-06.
* </p>
*
* @param call the method call containing handle
* @param result the result callback, returns true on success
*/
private void handleHalfCutPaper(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_HalfCutPaper(pointer);
if (!success) {
result.error("PRINT_FAILED", "halfCutPaper failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
/**
* Handles fullCutPaper method call.
* <p>
* Triggers full-cut on the paper cutter.
* Implements PRINT-06.
* </p>
*
* @param call the method call containing handle
* @param result the result callback, returns true on success
*/
private void handleFullCutPaper(@NonNull MethodCall call, @NonNull Result result) {
Integer handle = call.argument("handle");
if (handle == null) {
result.error("INVALID_ARGUMENT", "handle is required", null);
return;
}
Pointer pointer = portHandleManager.getHandle(handle);
if (pointer == null) {
result.error("PORT_CLOSED", "Port handle not found: " + handle, null);
return;
}
try {
boolean success = AutoReplyPrint.INSTANCE.CP_Pos_FullCutPaper(pointer);
if (!success) {
result.error("PRINT_FAILED", "fullCutPaper failed", null);
return;
}
result.success(success);
} catch (UnsatisfiedLinkError e) {
result.error("NATIVE_LIBRARY_ERROR", e.getMessage(), null);
} catch (Exception e) {
result.error("UNKNOWN_ERROR", e.getMessage(), null);
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
// Clean up all open ports when engine detaches (T-01-05)
if (portHandleManager != null) {
portHandleManager.closeAll();
}
}
}

View File

@@ -0,0 +1,198 @@
package com.xiarui.printer;
import static org.junit.Assert.*;
import com.sun.jna.Pointer;
import org.junit.Before;
import org.junit.Test;
/**
* Unit tests for PortHandleManager.
* Uses a mock PortOperations to avoid dependency on native library.
*/
public class PortHandleManagerTest {
private PortHandleManager manager;
private MockPortOperations mockOps;
@Before
public void setUp() {
mockOps = new MockPortOperations();
manager = new PortHandleManager(mockOps);
}
/**
* Test 1: registerHandle with valid pointer returns positive handle.
*/
@Test
public void testRegisterHandle_ValidPointer_ReturnsPositiveHandle() {
// Create a pointer with non-zero native value
Pointer pointer = new Pointer(0x1234L);
int handle = manager.registerHandle(pointer);
assertTrue("Handle should be > 0", handle > 0);
}
/**
* Test 2: registerHandle with null pointer returns -1.
*/
@Test
public void testRegisterHandle_NullPointer_ReturnsMinusOne() {
int handle = manager.registerHandle(null);
assertEquals("Null pointer should return -1", -1, handle);
}
/**
* Test 2b: registerHandle with zero native value pointer returns -1.
*/
@Test
public void testRegisterHandle_ZeroNativeValue_ReturnsMinusOne() {
Pointer pointer = new Pointer(0L);
int handle = manager.registerHandle(pointer);
assertEquals("Zero native value should return -1", -1, handle);
}
/**
* Test 3: getHandle returns the registered pointer.
*/
@Test
public void testGetHandle_RegisteredHandle_ReturnsPointer() {
Pointer pointer = new Pointer(0x5678L);
int handle = manager.registerHandle(pointer);
Pointer retrieved = manager.getHandle(handle);
assertEquals("Retrieved pointer should match registered pointer", pointer, retrieved);
}
/**
* Test 4: getHandle for unregistered handle returns null.
*/
@Test
public void testGetHandle_UnregisteredHandle_ReturnsNull() {
Pointer retrieved = manager.getHandle(999);
assertNull("Unregistered handle should return null", retrieved);
}
/**
* Test 5: closeHandle closes the port and removes registration.
*/
@Test
public void testCloseHandle_RegisteredHandle_ReturnsTrue() {
Pointer pointer = new Pointer(0xABCDL);
int handle = manager.registerHandle(pointer);
boolean result = manager.closeHandle(handle);
assertTrue("closeHandle should return true for registered handle", result);
assertTrue("Mock close should have been called", mockOps.closeCalled);
assertNull("Handle should be removed after close", manager.getHandle(handle));
}
/**
* Test 6: closeHandle for unregistered handle returns false.
*/
@Test
public void testCloseHandle_UnregisteredHandle_ReturnsFalse() {
boolean result = manager.closeHandle(999);
assertFalse("closeHandle should return false for unregistered handle", result);
assertFalse("Mock close should NOT have been called", mockOps.closeCalled);
}
/**
* Test 7: closeAll closes all registered ports and clears registry.
*/
@Test
public void testCloseAll_ClosesAllPorts() {
Pointer p1 = new Pointer(0x1111L);
Pointer p2 = new Pointer(0x2222L);
int h1 = manager.registerHandle(p1);
int h2 = manager.registerHandle(p2);
manager.closeAll();
assertNull("Handle 1 should be cleared", manager.getHandle(h1));
assertNull("Handle 2 should be cleared", manager.getHandle(h2));
assertTrue("Mock close should have been called", mockOps.closeCalled);
}
/**
* Test 8: Concurrent registrations return unique handles.
*/
@Test
public void testRegisterHandle_Concurrent_ReturnsUniqueHandles() throws InterruptedException {
final int threadCount = 10;
final int[] handles = new int[threadCount];
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
final int index = i;
threads[i] = new Thread(() -> {
Pointer pointer = new Pointer(0x1000L + index);
handles[index] = manager.registerHandle(pointer);
});
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
// All handles should be unique
for (int i = 0; i < threadCount; i++) {
for (int j = i + 1; j < threadCount; j++) {
assertNotEquals(
"Handles should be unique: " + handles[i] + " vs " + handles[j],
handles[i], handles[j]
);
}
}
}
/**
* Test 9: isHandleValid returns true for valid opened handle.
*/
@Test
public void testIsHandleValid_OpenedHandle_ReturnsTrue() {
Pointer pointer = new Pointer(0xEEEEL);
int handle = manager.registerHandle(pointer);
mockOps.isOpenedResult = true;
boolean valid = manager.isHandleValid(handle);
assertTrue("Opened handle should be valid", valid);
}
/**
* Test 10: isHandleValid returns false for unregistered handle.
*/
@Test
public void testIsHandleValid_UnregisteredHandle_ReturnsFalse() {
boolean valid = manager.isHandleValid(999);
assertFalse("Unregistered handle should not be valid", valid);
}
/**
* Mock PortOperations for testing without native library.
*/
static class MockPortOperations implements PortHandleManager.PortOperations {
boolean closeCalled = false;
boolean isOpenedResult = true;
@Override
public boolean closePort(Pointer handle) {
closeCalled = true;
return true;
}
@Override
public boolean isPortOpened(Pointer handle) {
return isOpenedResult;
}
}
}

View File

@@ -0,0 +1,178 @@
package com.xiarui.printer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.junit.Assert.*;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import org.junit.Test;
import org.junit.Before;
/**
* Unit tests for PrinterPlugin.
* Tests parameter validation and method routing.
*/
public class PrinterPluginTest {
private PrinterPlugin plugin;
@Before
public void setUp() {
plugin = new PrinterPlugin();
}
@Test
public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
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);
}
@Test
public void onMethodCall_openComPort_emptyPortName_returnsInvalidArgument() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
put("portName", "");
put("baudRate", 115200);
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("portName"), org.mockito.ArgumentMatchers.isNull());
}
@Test
public void onMethodCall_openComPort_nullPortName_returnsInvalidArgument() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
put("portName", null);
put("baudRate", 115200);
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("portName"), org.mockito.ArgumentMatchers.isNull());
}
@Test
public void onMethodCall_openComPort_invalidBaudRate_returnsInvalidArgument() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
put("portName", "/dev/ttyS0");
put("baudRate", 100); // Below minimum 1200
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("baudRate"), org.mockito.ArgumentMatchers.isNull());
}
@Test
public void onMethodCall_openComPort_invalidDataBits_returnsInvalidArgument() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("openComPort", new java.util.HashMap<String, Object>() {{
put("portName", "/dev/ttyS0");
put("baudRate", 115200);
put("dataBits", 9); // Above maximum 8
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("dataBits"), org.mockito.ArgumentMatchers.isNull());
}
@Test
public void onMethodCall_openUsbPort_emptyPortName_returnsInvalidArgument() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("openUsbPort", new java.util.HashMap<String, Object>() {{
put("portName", "");
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("portName"), org.mockito.ArgumentMatchers.isNull());
}
@Test
public void onMethodCall_closePort_nullHandle_returnsInvalidArgument() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("closePort", new java.util.HashMap<String, Object>() {{
put("handle", null);
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("handle"), org.mockito.ArgumentMatchers.isNull());
}
@Test
public void onMethodCall_isPortOpened_nullHandle_returnsInvalidArgument() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("isPortOpened", new java.util.HashMap<String, Object>() {{
put("handle", null);
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).error("INVALID_ARGUMENT", org.mockito.ArgumentMatchers.contains("handle"), org.mockito.ArgumentMatchers.isNull());
}
@Test
public void onMethodCall_unknownMethod_returnsNotImplemented() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("unknownMethod", null);
plugin.onMethodCall(call, mockResult);
verify(mockResult).notImplemented();
}
@Test
public void onMethodCall_closePort_validHandle_callsPortHandleManager() {
// Register a handle first to test close
PortHandleManager manager = PortHandleManager.getInstance();
com.sun.jna.Pointer pointer = new com.sun.jna.Pointer(0x1234L);
int handle = manager.registerHandle(pointer);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("closePort", new java.util.HashMap<String, Object>() {{
put("handle", handle);
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).success(true);
}
@Test
public void onMethodCall_closePort_invalidHandle_returnsFalse() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("closePort", new java.util.HashMap<String, Object>() {{
put("handle", 99999); // Non-existent handle
}});
plugin.onMethodCall(call, mockResult);
verify(mockResult).success(false);
}
@Test
public void onMethodCall_isPortOpened_validHandle_callsPortHandleManager() {
PortHandleManager manager = PortHandleManager.getInstance();
com.sun.jna.Pointer pointer = new com.sun.jna.Pointer(0x5678L);
int handle = manager.registerHandle(pointer);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
MethodCall call = new MethodCall("isPortOpened", new java.util.HashMap<String, Object>() {{
put("handle", handle);
}});
plugin.onMethodCall(call, mockResult);
// Note: Without native library, isPortOpened will fail due to UnsatisfiedLinkError
// but the test verifies the routing logic
}
}

5938
docs/apis.md Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/autoreplyprint.aar Normal file

Binary file not shown.

View File

@@ -0,0 +1,157 @@
# 打印机插件设计文档
**日期:** 2026-04-22
**状态:** 已确认
**目标:** 将 printer 插件开发为完整的打印插件,支持通过 CH34 串口 + autoreplyprint.aar 进行打印
## 1. 项目概述
### 用途
公司内部多个 Flutter 项目共用的打印插件,用于打印拉曼检测结果(简单文字票据)。
### 技术架构
- CH34 插件提供串口通道
- 本项目 Android 原生层集成 autoreplyprint.aar
- Dart 层通过 MethodChannel 调用原生层
- 底层指令式 API调用方控制打开→打印→关闭全流程
## 2. 整体架构
```
┌─────────────────────────────────────────────┐
│ 调用方 Flutter 项目 │
│ Printer.openComPort() → PrinterPortHandle │
│ handle.printText() → handle.close() │
└──────────────┬───────────────────────────────┘
│ MethodChannel 调用
┌──────────────▼───────────────────────────────┐
│ Flutter 插件 Dart 层 │
│ Printer → PrinterPlatform │
│ → MethodChannelPrinterPort │
│ → PrinterPortHandle │
└──────────────┬───────────────────────────────┐
│ MethodChannel + portHandle
┌──────────────▼───────────────────────────────┐
│ Android 原生层 (Java) │
│ PrinterPlugin (MethodCallHandler) │
│ → PortHandleManager │
│ → autoreplyprint.aar │
└───────────────────────────────────────────────┘
```
## 3. API 设计
### 使用示例
```dart
// 打开端口
PrinterPortHandle port = await Printer.openComPort(
portName: 'COM3',
baudRate: 9600,
dataBits: 8,
parity: Parity.none,
stopBits: StopBits.one,
flowControl: FlowControl.none,
);
// 设置编码
await port.setMultiByteMode();
await port.setMultiByteEncoding(MultiByteEncoding.utf8);
// 打印
await port.setAlignment(Alignment.center);
await port.setTextScale(width: 2, height: 2);
await port.printText('拉曼检测结果报告');
// 走纸切纸
await port.feedLine(3);
await port.halfCutPaper();
// 关闭
await port.close();
```
### API 分类
| 类别 | 方法 |
|------|------|
| 端口管理 | `openComPort`, `close` |
| 打印文本 | `printText`, `printTextInUtf8`, `printTextInGbk` |
| 格式控制 | `setAlignment`, `setTextScale`, `setTextBold`, `setTextUnderline` |
| 走纸切纸 | `feedLine`, `feedDot`, `halfCutPaper`, `fullCutPaper` |
| 编码模式 | `setMultiByteMode`, `setSingleByteMode`, `setMultiByteEncoding` |
| 条码/二维码 | `printBarcode`, `printQRCode` |
| 图片 | `printRasterImageFromData` |
| 其他 | `resetPrinter`, `kickOutDrawer`, `beep` |
## 4. MethodChannel 协议
### 协议格式
```
MethodChannel 名称: "printer"
Method: "openComPort"
Args: { portName, baudRate, dataBits, parity, stopBits, flowControl, autoReplyMode }
Return: { success: bool, portHandle: int?, error: String? }
Method: "closePort"
Args: { portHandle }
Return: { success: bool, error: String? }
Method: "printText"
Args: { portHandle, text }
Return: { success: bool, error: String? }
... // 每个操作对应一个 method call
```
### 错误处理
- 原生层异常通过 `result.error()` 返回
- Dart 层统一包装为 `PrinterException`
## 5. 原生层设计
### 端口句柄管理
- `PrinterPlugin.java` 维护 `Map<Integer, Pointer> portHandles`
- `openComPort` 调用 `CP_Port_OpenCom` 返回 Pointer 存入 map
- 生成递增 int 作为 Dart 层 portHandle
- 每个 method call 从 map 中取出 Pointer调用对应 AAR 函数
### AAR 集成
-`docs/autoreplyprint.aar` 移至 `android/libs/`
- `build.gradle.kts` 配置 `implementation files('libs/autoreplyprint.aar')`
## 6. 项目结构
```
printer/
├── lib/
│ ├── printer.dart # 主入口
│ ├── printer_platform_interface.dart # 平台接口
│ ├── printer_method_channel.dart # MethodChannel 实现
│ └── src/
│ ├── models/
│ │ ├── parity.dart # 串口校验枚举
│ │ ├── alignment.dart # 对齐枚举
│ │ ├── encoding.dart # 字符编码枚举
│ │ └── printer_exception.dart # 自定义异常
│ └── port_handle.dart # 端口句柄封装
├── android/
│ ├── build.gradle.kts
│ ├── libs/
│ │ └── autoreplyprint.aar
│ └── src/main/java/com/xiarui/printer/
│ ├── PrinterPlugin.java
│ └── PortHandleManager.java
└── test/
└── printer_test.dart
```
## 7. 关键决策
| 决策 | 选择 | 理由 |
|------|------|------|
| SDK 集成方式 | 原生层调用 AAR | 利用已有封装,可靠 |
| API 风格 | 底层指令式 | 灵活,调用方控制流程 |
| 字符编码 | UTF8 | 中文支持,现代标准 |
| 连接方式 | 串口 (通过 CH34) | 项目实际硬件需求 |
| 状态回调 | 暂不需要 | 最小实现,后续可扩展 |

471
docs/usage.md Normal file
View File

@@ -0,0 +1,471 @@
# Printer 插件使用指南
> 本文档介绍 Flutter Printer 插件的安装、配置和使用方法。该插件封装了 Android `autoreplyprint` SDK提供了串口/USB 端口管理和票据打印功能。
## 目录
- [安装](#安装)
- [快速开始](#快速开始)
- [端口管理](#端口管理)
- [票据打印](#票据打印)
- [异常处理](#异常处理)
- [完整示例](#完整示例)
- [API 参考](#api-参考)
---
## 安装
### 1. 添加依赖
`pubspec.yaml` 中添加本地依赖:
```yaml
dependencies:
printer:
path: ../printer # 根据实际路径调整
```
### 2. 配置 Android
`docs/autoreplyprint.aar` 文件放置到 Android 项目的 `app/libs/` 目录下:
```
your_flutter_app/
└── android/
└── app/
└── libs/
└── autoreplyprint.aar
```
`android/app/build.gradle` 中确保 `libs` 目录被引用:
```gradle
dependencies {
implementation files('libs/autoreplyprint.aar')
// ... 其他依赖
}
```
---
## 快速开始
### 基本打印流程
一个完整的打印流程包含三个步骤:**打开端口 → 打印内容 → 关闭端口**。
```dart
import 'package:printer/printer.dart';
final printer = Printer();
// 1. 打开端口
final handle = await printer.openComPort(
portName: '/dev/ttyS0',
baudRate: 115200,
);
// 2. 设置编码并打印文本
await printer.setMultiByteMode(handle);
await printer.setMultiByteEncoding(handle, MultiByteEncoding.utf8);
await printer.setAlignment(handle, PrinterAlignment.center);
await printer.printText(handle, 'Hello 中文\n');
await printer.feedLine(handle, 3);
// 3. 关闭端口
await printer.closePort(handle);
```
---
## 端口管理
### 枚举端口
```dart
// 枚举串口
final comPorts = await printer.enumComPorts();
print('可用串口: $comPorts');
// 枚举USB端口
final usbPorts = await printer.enumUsbPorts();
print('可用USB口: $usbPorts');
```
### 打开串口 (COM Port)
```dart
final handle = await printer.openComPort(
portName: '/dev/ttyS0', // 端口名称(必填)
baudRate: 115200, // 波特率(必填),需与打印机保持一致
dataBits: 8, // 数据位默认8
parity: SerialParity.none, // 校验位,默认无校验
stopBits: SerialStopBits.one, // 停止位默认1位
flowControl: SerialFlowControl.none, // 流控制,默认无
autoReplyMode: true, // 是否开启自动回传模式
);
```
**参数说明:**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `portName` | `String` | 是 | - | 端口名称,如 `/dev/ttyS0``COM1` |
| `baudRate` | `int` | 是 | - | 波特率,如 `9600`, `19200`, `38400`, `57600`, `115200` |
| `dataBits` | `int` | 否 | `8` | 数据位,范围 `[4, 8]` |
| `parity` | `SerialParity` | 否 | `none` | 校验位:`none`, `odd`, `even`, `mark`, `space` |
| `stopBits` | `SerialStopBits` | 否 | `one` | 停止位:`one`, `onePointFive`, `two` |
| `flowControl` | `SerialFlowControl` | 否 | `none` | 流控制:`none`, `xonXoff`, `rtsCts`, `dtrDsr` |
| `autoReplyMode` | `bool` | 否 | `true` | 是否开启自动回传模式 |
### 打开 USB 端口
```dart
final handle = await printer.openUsbPort(
portName: 'USB001', // 端口名称(必填),可不指定以自动查找
autoReplyMode: true, // 是否开启自动回传模式
);
```
### 检查端口状态
```dart
final isOpen = await printer.isPortOpened(handle);
if (isOpen) {
print('端口已打开');
}
```
### 关闭端口
```dart
final success = await printer.closePort(handle);
```
### 使用安全句柄 (推荐)
使用 `PrinterPortHandle` 可以更安全地管理端口资源,避免资源泄漏:
```dart
// 打开带句柄的串口
final portHandle = await printer.openComPortWithHandle(
portName: '/dev/ttyS0',
baudRate: 115200,
);
try {
// 执行打印操作
await printer.printText(portHandle.handle, 'Hello\n');
} finally {
// 确保端口被关闭
await portHandle.close();
}
```
同样支持 USB 端口:
```dart
final portHandle = await printer.openUsbPortWithHandle(
portName: 'USB001',
);
```
---
## 票据打印
### 打印文本
在打印文本之前,需要先设置多字节编码模式:
```dart
// 设置多字节模式
await printer.setMultiByteMode(handle);
// 设置编码为 UTF-8支持中文等多语言字符
await printer.setMultiByteEncoding(handle, MultiByteEncoding.utf8);
// 打印文本
await printer.printText(handle, 'Hello 中文\n');
```
**支持的编码类型 (`MultiByteEncoding`)**
| 编码 | 值 | 说明 |
|------|-----|------|
| `gbk` | `0` | GBK 编码(简体中文) |
| `utf8` | `1` | UTF-8 编码Unicode推荐 |
| `big5` | `3` | BIG5 编码(繁体中文) |
| `shiftJis` | `4` | Shift-JIS 编码(日文) |
| `eucKr` | `5` | EUC-KR 编码(韩文) |
### 设置文本对齐
```dart
// 左对齐
await printer.setAlignment(handle, PrinterAlignment.left);
// 居中对齐
await printer.setAlignment(handle, PrinterAlignment.center);
// 右对齐
await printer.setAlignment(handle, PrinterAlignment.right);
```
### 设置文本缩放
宽度和高度缩放值范围为 `1``8`
```dart
await printer.setTextScale(
handle,
widthScale: 2, // 宽度放大2倍
heightScale: 2, // 高度放大2倍
);
await printer.printText(handle, 'Large Text\n');
```
### 设置文本加粗
```dart
// 开启加粗
await printer.setTextBold(handle, true);
await printer.printText(handle, 'Bold Text\n');
// 关闭加粗
await printer.setTextBold(handle, false);
```
### 设置文本下划线
```dart
// 0 = 无下划线
// 1 = 1点下划线
// 2 = 2点下划线
await printer.setTextUnderline(handle, 1);
await printer.printText(handle, 'Underlined Text\n');
```
### 走纸
```dart
// 走纸指定行数
await printer.feedLine(handle, 5);
// 走纸指定点数
await printer.feedDot(handle, 100);
```
### 切纸
```dart
// 半切(虚线切,纸张不完全分离)
await printer.halfCutPaper(handle);
// 全切(实线切,纸张完全分离)
await printer.fullCutPaper(handle);
```
---
## 异常处理
所有端口打开操作在失败时会抛出 `PrinterException`
```dart
try {
final handle = await printer.openComPort(
portName: '/dev/ttyS0',
baudRate: 115200,
);
} on PrinterException catch (e) {
print('端口打开失败: ${e.code} - ${e.message}');
}
```
`PrinterException` 包含以下信息:
| 属性 | 类型 | 说明 |
|------|------|------|
| `code` | `String` | 机器可读的错误码,如 `INVALID_ARGUMENT`, `PORT_OPEN_FAILED` |
| `message` | `String` | 人类可读的错误消息 |
| `details` | `dynamic` | 额外的错误详情(可能为 null |
---
## 完整示例
以下是一个完整的票据打印示例:
```dart
import 'package:printer/printer.dart';
/// 打印收据示例
Future<void> printReceipt() async {
final printer = Printer();
PrinterPortHandle? portHandle;
try {
// 1. 枚举可用端口
final comPorts = await printer.enumComPorts();
if (comPorts.isEmpty) {
print('没有可用的串口');
return;
}
// 2. 打开第一个可用端口
portHandle = await printer.openComPortWithHandle(
portName: comPorts.first,
baudRate: 115200,
);
final handle = portHandle.handle;
// 3. 设置编码
await printer.setMultiByteMode(handle);
await printer.setMultiByteEncoding(handle, MultiByteEncoding.utf8);
// 4. 打印标题(居中、加粗、放大)
await printer.setAlignment(handle, PrinterAlignment.center);
await printer.setTextBold(handle, true);
await printer.setTextScale(handle, widthScale: 2, heightScale: 2);
await printer.printText(handle, '收据标题\n');
// 5. 重置样式并打印内容
await printer.setTextBold(handle, false);
await printer.setTextScale(handle, widthScale: 1, heightScale: 1);
await printer.setAlignment(handle, PrinterAlignment.left);
await printer.printText(handle, '商品名称 数量 价格\n');
await printer.printText(handle, '----------------------------\n');
await printer.printText(handle, '商品A 2 ¥20.00\n');
await printer.printText(handle, '商品B 1 ¥15.00\n');
await printer.printText(handle, '----------------------------\n');
await printer.printText(handle, '总计: ¥35.00\n');
// 6. 走纸并切纸
await printer.feedLine(handle, 5);
await printer.halfCutPaper(handle);
print('打印完成');
} on PrinterException catch (e) {
print('打印失败: ${e.code} - ${e.message}');
} finally {
// 7. 确保端口关闭
await portHandle?.close();
}
}
```
---
## API 参考
### Printer 类
| 方法 | 返回值 | 说明 |
|------|--------|------|
| `getPlatformVersion()` | `Future<String?>` | 获取平台版本 |
| `enumComPorts()` | `Future<List<String>>` | 枚举可用串口 |
| `enumUsbPorts()` | `Future<List<String>>` | 枚举可用USB端口 |
| `openComPort({...})` | `Future<int>` | 打开串口 |
| `openUsbPort({...})` | `Future<int>` | 打开USB端口 |
| `openComPortWithHandle({...})` | `Future<PrinterPortHandle>` | 打开串口并返回安全句柄 |
| `openUsbPortWithHandle({...})` | `Future<PrinterPortHandle>` | 打开USB端口并返回安全句柄 |
| `closePort(int handle)` | `Future<bool>` | 关闭端口 |
| `isPortOpened(int handle)` | `Future<bool>` | 检查端口是否打开 |
| `setMultiByteMode(int handle)` | `Future<bool>` | 设置多字节编码模式 |
| `setMultiByteEncoding(int, MultiByteEncoding)` | `Future<bool>` | 设置多字节编码类型 |
| `printText(int handle, String text)` | `Future<bool>` | 打印文本 |
| `setAlignment(int, PrinterAlignment)` | `Future<bool>` | 设置文本对齐 |
| `setTextScale(int, {width, height})` | `Future<bool>` | 设置文本缩放 |
| `setTextBold(int handle, bool bold)` | `Future<bool>` | 设置文本加粗 |
| `setTextUnderline(int handle, int level)` | `Future<bool>` | 设置文本下划线 |
| `feedLine(int handle, int numLines)` | `Future<bool>` | 走纸指定行数 |
| `feedDot(int handle, int numDots)` | `Future<bool>` | 走纸指定点数 |
| `halfCutPaper(int handle)` | `Future<bool>` | 半切纸 |
| `fullCutPaper(int handle)` | `Future<bool>` | 全切纸 |
### 枚举类型
#### SerialParity (串口校验位)
| 值 | 说明 |
|----|------|
| `none` | 无校验 |
| `odd` | 奇校验 |
| `even` | 偶校验 |
| `mark` | 标记校验 |
| `space` | 空白校验 |
#### SerialStopBits (串口停止位)
| 值 | 说明 |
|----|------|
| `one` | 1位停止位 |
| `onePointFive` | 1.5位停止位 |
| `two` | 2位停止位 |
#### SerialFlowControl (串口流控制)
| 值 | 说明 |
|----|------|
| `none` | 无流控 |
| `xonXoff` | XON/XOFF 软件流控 |
| `rtsCts` | RTS/CTS 硬件流控 |
| `dtrDsr` | DTR/DSR 硬件流控 |
#### PrinterAlignment (打印对齐)
| 值 | 说明 |
|----|------|
| `left` | 左对齐 |
| `center` | 居中对齐 |
| `right` | 右对齐 |
#### MultiByteEncoding (多字节编码)
| 值 | 说明 |
|----|------|
| `gbk` | GBK 编码(简体中文) |
| `utf8` | UTF-8 编码(推荐) |
| `big5` | BIG5 编码(繁体中文) |
| `shiftJis` | Shift-JIS 编码(日文) |
| `eucKr` | EUC-KR 编码(韩文) |
### PrinterPortHandle 类
| 属性/方法 | 类型 | 说明 |
|-----------|------|------|
| `handle` | `int` | 整数端口句柄 |
| `isValid` | `bool` | 端口是否仍然有效 |
| `close()` | `Future<void>` | 关闭端口,多次调用为 no-op |
### PrinterException 类
| 属性 | 类型 | 说明 |
|------|------|------|
| `code` | `String` | 错误码 |
| `message` | `String` | 错误消息 |
| `details` | `dynamic` | 额外详情 |
---
## 常见问题
### 1. 打印乱码怎么办?
- 确认编码设置与发送的文本编码一致,推荐使用 `MultiByteEncoding.utf8`
- 确认打印机机型是否正确(票据机型使用 `CP_Pos_` 系列函数)
- 检查波特率是否与打印机设置一致
### 2. 端口打开失败?
- 检查端口名称是否正确
- 确认端口未被其他应用占用
- 检查 USB 连接或串口线连接是否正常
### 3. 打印到一半停止?
- 检查电源是否充足(一般需要 2A 电源)
- 检查是否缺纸

45
example/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
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-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# 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

17
example/README.md Normal file
View File

@@ -0,0 +1,17 @@
# printer_example
Demonstrates how to use the printer 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:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

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

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

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
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")
}
android {
namespace = "com.xiarui.printer_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.xiarui.printer_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
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.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.overridePathCheck=true

View File

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

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
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.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@@ -0,0 +1,24 @@
// This is a basic Flutter integration test.
//
// Since integration tests run in a full Flutter application, they can interact
// with the host side of a plugin implementation, unlike Dart unit tests.
//
// For more information about Flutter integration tests, please see
// https://flutter.dev/to/integration-testing
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:printer/printer.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
final Printer plugin = Printer();
final String? version = await plugin.getPlatformVersion();
// The version string depends on the host platform running the test, so
// just assert that some non-empty string is returned.
expect(version?.isNotEmpty, true);
});
}

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

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:printer/printer.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _printerPlugin = Printer();
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _printerPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Plugin example app')),
body: Center(child: Text('Running on: $_platformVersion\n')),
),
);
}
}

283
example/pubspec.lock Normal file
View File

@@ -0,0 +1,283 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
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: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.0"
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: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.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"
printer:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
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.11.4 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

85
example/pubspec.yaml Normal file
View File

@@ -0,0 +1,85 @@
name: printer_example
description: "Demonstrates how to use the printer 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.11.4
# 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
printer:
# When depending on this package from a real application you should use:
# printer: ^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.8
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: ^6.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/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# 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/to/font-from-package

View File

@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:printer_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,
);
});
}

View File

@@ -0,0 +1,29 @@
/// Multi-byte character encoding for ticket printing.
///
/// Maps to the autoreplyprint AAR SDK CP_MultiByteEncoding constants.
/// The printer uses the specified encoding to interpret received data.
///
/// Note: value 2 is reserved/unused per SDK specification. The sequence
/// jumps from UTF8(1) to BIG5(3).
enum MultiByteEncoding {
/// GBK encoding (Simplified Chinese)
gbk(0),
/// UTF-8 encoding (Unicode)
utf8(1),
// Note: value 2 is reserved/unused per SDK specification
/// BIG5 encoding (Traditional Chinese)
big5(3),
/// Shift-JIS encoding (Japanese)
shiftJis(4),
/// EUC-KR encoding (Korean)
eucKr(5);
/// The integer value matching the SDK constant.
final int value;
const MultiByteEncoding(this.value);
}

View File

@@ -0,0 +1,17 @@
/// Alignment for ticket printing.
///
/// Maps to the autoreplyprint AAR SDK CP_Pos_Alignment constants.
enum PrinterAlignment {
/// Left alignment
left(0),
/// Center alignment
center(1),
/// Right alignment
right(2);
/// The integer value matching the SDK constant.
final int value;
const PrinterAlignment(this.value);
}

View File

@@ -0,0 +1,20 @@
/// Flow control setting for serial port communication.
///
/// Maps to the autoreplyprint AAR SDK flow control constants.
enum SerialFlowControl {
/// No flow control
none(0),
/// XON/XOFF software flow control
xonXoff(1),
/// RTS/CTS hardware flow control
rtsCts(2),
/// DTR/DSR hardware flow control
dtrDsr(3);
/// The integer value matching the AAR SDK constant.
final int value;
const SerialFlowControl(this.value);
}

View File

@@ -0,0 +1,23 @@
/// Parity setting for serial port communication.
///
/// Maps to the autoreplyprint AAR SDK parity constants.
enum SerialParity {
/// No parity
none(0),
/// Odd parity
odd(1),
/// Even parity
even(2),
/// Mark parity
mark(3),
/// Space parity
space(4);
/// The integer value matching the AAR SDK constant.
final int value;
const SerialParity(this.value);
}

View File

@@ -0,0 +1,17 @@
/// Stop bits setting for serial port communication.
///
/// Maps to the autoreplyprint AAR SDK stop bits constants.
enum SerialStopBits {
/// 1 stop bit
one(0),
/// 1.5 stop bits
onePointFive(1),
/// 2 stop bits
two(2);
/// The integer value matching the AAR SDK constant.
final int value;
const SerialStopBits(this.value);
}

View File

@@ -0,0 +1,24 @@
/// Custom exception for printer operations.
///
/// Encapsulates structured error information from the native layer,
/// including error code, message, and optional details.
class PrinterException implements Exception {
/// Machine-readable error code (e.g., 'INVALID_ARGUMENT', 'PORT_OPEN_FAILED').
final String code;
/// Human-readable error message.
final String message;
/// Additional error details (may be null).
final dynamic details;
/// Creates a [PrinterException] with the given [code] and [message].
const PrinterException({
required this.code,
required this.message,
this.details,
});
@override
String toString() => 'PrinterException($code): $message';
}

View File

@@ -0,0 +1,44 @@
/// Close callback type for [PrinterPortHandle].
typedef ClosePortCallback = Future<bool> Function(int handle);
/// Represents an open printer port handle.
///
/// Wraps an integer handle and provides a [close] method to release
/// the underlying native port resource. Once closed, the handle
/// becomes invalid.
class PrinterPortHandle {
/// The integer handle returned by the native layer.
final int handle;
/// Callback to close the port. If null, close() is a no-op.
final ClosePortCallback? _closeCallback;
/// Whether the port is still valid (not closed).
bool _isValid;
/// Creates a [PrinterPortHandle] with the given [handle] and optional [closeCallback].
PrinterPortHandle({
required this.handle,
ClosePortCallback? closeCallback,
}) : _closeCallback = closeCallback,
_isValid = true;
/// Returns true if the port is still open and not closed.
bool get isValid => _isValid;
/// Closes the port and marks the handle as invalid.
///
/// Subsequent calls to [close] are no-ops if the port is already closed.
Future<void> close() async {
if (!_isValid) {
return;
}
_isValid = false;
if (_closeCallback != null) {
await _closeCallback(handle);
}
}
@override
String toString() => 'PrinterPortHandle(handle: $handle, valid: $_isValid)';
}

227
lib/printer.dart Normal file
View File

@@ -0,0 +1,227 @@
import 'enums/multi_byte_encoding.dart';
import 'enums/printer_alignment.dart';
import 'enums/serial_flow_control.dart';
import 'enums/serial_parity.dart';
import 'enums/serial_stop_bits.dart';
import 'models/printer_port_handle.dart';
import 'printer_platform_interface.dart';
/// Main entry point for the printer plugin.
///
/// Provides a thin wrapper around [PrinterPlatform] for port management
/// operations. All methods delegate to the platform interface.
class Printer {
/// Returns the platform version string.
Future<String?> getPlatformVersion() {
return PrinterPlatform.instance.getPlatformVersion();
}
/// Opens a serial (COM) port with the specified parameters.
///
/// Returns an integer handle on success.
/// Throws [PrinterException] on failure.
///
/// Example:
/// ```dart
/// final printer = Printer();
/// final handle = await printer.openComPort(
/// portName: '/dev/ttyS0',
/// baudRate: 115200,
/// );
/// ```
Future<int> openComPort({
required String portName,
required int baudRate,
int dataBits = 8,
SerialParity parity = SerialParity.none,
SerialStopBits stopBits = SerialStopBits.one,
SerialFlowControl flowControl = SerialFlowControl.none,
bool autoReplyMode = true,
}) {
return PrinterPlatform.instance.openComPort(
portName: portName,
baudRate: baudRate,
dataBits: dataBits,
parity: parity,
stopBits: stopBits,
flowControl: flowControl,
autoReplyMode: autoReplyMode,
);
}
/// Opens a USB port with the specified parameters.
///
/// Returns an integer handle on success.
/// Throws [PrinterException] on failure.
Future<int> openUsbPort({
required String portName,
bool autoReplyMode = true,
}) {
return PrinterPlatform.instance.openUsbPort(
portName: portName,
autoReplyMode: autoReplyMode,
);
}
/// Closes a port by its integer handle.
///
/// Returns true if successfully closed, false otherwise.
Future<bool> closePort(int handle) {
return PrinterPlatform.instance.closePort(handle);
}
/// Checks if a port is currently opened.
///
/// Returns true if the port is open, false otherwise.
Future<bool> isPortOpened(int handle) {
return PrinterPlatform.instance.isPortOpened(handle);
}
/// Enumerates available serial (COM) ports.
///
/// Returns a list of port name strings.
Future<List<String>> enumComPorts() {
return PrinterPlatform.instance.enumComPorts();
}
/// Enumerates available USB ports.
///
/// Returns a list of port name strings.
Future<List<String>> enumUsbPorts() {
return PrinterPlatform.instance.enumUsbPorts();
}
/// Opens a serial port and returns a [PrinterPortHandle] for safe resource management.
///
/// The returned handle can be closed via [PrinterPortHandle.close].
Future<PrinterPortHandle> openComPortWithHandle({
required String portName,
required int baudRate,
int dataBits = 8,
SerialParity parity = SerialParity.none,
SerialStopBits stopBits = SerialStopBits.one,
SerialFlowControl flowControl = SerialFlowControl.none,
bool autoReplyMode = true,
}) async {
final handle = await openComPort(
portName: portName,
baudRate: baudRate,
dataBits: dataBits,
parity: parity,
stopBits: stopBits,
flowControl: flowControl,
autoReplyMode: autoReplyMode,
);
return PrinterPortHandle(
handle: handle,
closeCallback: closePort,
);
}
/// Opens a USB port and returns a [PrinterPortHandle] for safe resource management.
///
/// The returned handle can be closed via [PrinterPortHandle.close].
Future<PrinterPortHandle> openUsbPortWithHandle({
required String portName,
bool autoReplyMode = true,
}) async {
final handle = await openUsbPort(
portName: portName,
autoReplyMode: autoReplyMode,
);
return PrinterPortHandle(
handle: handle,
closeCallback: closePort,
);
}
/// Sets the printer to multi-byte encoding mode.
///
/// Returns true on success.
Future<bool> setMultiByteMode(int handle) {
return PrinterPlatform.instance.setMultiByteMode(handle);
}
/// Sets the multi-byte character encoding.
///
/// Returns true on success.
Future<bool> setMultiByteEncoding(int handle, MultiByteEncoding encoding) {
return PrinterPlatform.instance.setMultiByteEncoding(handle, encoding);
}
/// Prints text using UTF-8 encoding.
///
/// The caller should ensure multi-byte mode is set to UTF-8 before calling:
/// ```dart
/// await printer.setMultiByteMode(handle);
/// await printer.setMultiByteEncoding(handle, MultiByteEncoding.utf8);
/// await printer.printText(handle, 'Hello 中文');
/// ```
///
/// Returns true on success.
Future<bool> printText(int handle, String text) {
return PrinterPlatform.instance.printText(handle, text);
}
/// Sets text alignment.
///
/// Returns true on success.
Future<bool> setAlignment(int handle, PrinterAlignment alignment) {
return PrinterPlatform.instance.setAlignment(handle, alignment);
}
/// Sets text scale (width and height magnification).
///
/// Both scales must be between 1 and 8.
/// Returns true on success.
Future<bool> setTextScale(int handle, {required int widthScale, required int heightScale}) {
return PrinterPlatform.instance.setTextScale(
handle,
widthScale: widthScale,
heightScale: heightScale,
);
}
/// Sets text bold on or off.
///
/// Returns true on success.
Future<bool> setTextBold(int handle, bool bold) {
return PrinterPlatform.instance.setTextBold(handle, bold);
}
/// Sets text underline level.
///
/// 0 = no underline, 1 = 1-dot underline, 2 = 2-dot underline.
/// Returns true on success.
Future<bool> setTextUnderline(int handle, int underline) {
return PrinterPlatform.instance.setTextUnderline(handle, underline);
}
/// Feeds paper by specified number of lines.
///
/// Returns true on success.
Future<bool> feedLine(int handle, int numLines) {
return PrinterPlatform.instance.feedLine(handle, numLines);
}
/// Feeds paper by specified number of dots.
///
/// Returns true on success.
Future<bool> feedDot(int handle, int numDots) {
return PrinterPlatform.instance.feedDot(handle, numDots);
}
/// Performs a half cut of the paper.
///
/// Returns true on success.
Future<bool> halfCutPaper(int handle) {
return PrinterPlatform.instance.halfCutPaper(handle);
}
/// Performs a full cut of the paper.
///
/// Returns true on success.
Future<bool> fullCutPaper(int handle) {
return PrinterPlatform.instance.fullCutPaper(handle);
}
}

View File

@@ -0,0 +1,339 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'enums/multi_byte_encoding.dart';
import 'enums/printer_alignment.dart';
import 'enums/serial_flow_control.dart';
import 'enums/serial_parity.dart';
import 'enums/serial_stop_bits.dart';
import 'models/printer_exception.dart';
import 'printer_platform_interface.dart';
/// An implementation of [PrinterPlatform] that uses method channels.
class MethodChannelPrinter extends PrinterPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('printer');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>(
'getPlatformVersion',
);
return version;
}
@override
Future<int> openComPort({
required String portName,
required int baudRate,
int dataBits = 8,
SerialParity parity = SerialParity.none,
SerialStopBits stopBits = SerialStopBits.one,
SerialFlowControl flowControl = SerialFlowControl.none,
bool autoReplyMode = true,
}) async {
try {
final result = await methodChannel.invokeMethod<int>('openComPort', {
'portName': portName,
'baudRate': baudRate,
'dataBits': dataBits,
'parity': parity.value,
'stopBits': stopBits.value,
'flowControl': flowControl.value,
'autoReplyMode': autoReplyMode ? 1 : 0,
});
return result!;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to open COM port',
details: e.details,
);
}
}
@override
Future<int> openUsbPort({
required String portName,
bool autoReplyMode = true,
}) async {
try {
final result = await methodChannel.invokeMethod<int>('openUsbPort', {
'portName': portName,
'autoReplyMode': autoReplyMode ? 1 : 0,
});
return result!;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to open USB port',
details: e.details,
);
}
}
@override
Future<bool> closePort(int handle) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'closePort',
{'handle': handle},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to close port',
details: e.details,
);
}
}
@override
Future<bool> isPortOpened(int handle) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'isPortOpened',
{'handle': handle},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to check port status',
details: e.details,
);
}
}
@override
Future<List<String>> enumComPorts() async {
try {
final result = await methodChannel.invokeMethod<List>('enumComPorts');
return (result ?? []).map((e) => e.toString()).toList();
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to enumerate COM ports',
details: e.details,
);
}
}
@override
Future<List<String>> enumUsbPorts() async {
try {
final result = await methodChannel.invokeMethod<List>('enumUsbPorts');
return (result ?? []).map((e) => e.toString()).toList();
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to enumerate USB ports',
details: e.details,
);
}
}
/// Sets the printer to multi-byte encoding mode.
@override
Future<bool> setMultiByteMode(int handle) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'setMultiByteMode',
{'handle': handle},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to set multi-byte mode',
details: e.details,
);
}
}
/// Sets the multi-byte character encoding.
@override
Future<bool> setMultiByteEncoding(int handle, MultiByteEncoding encoding) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'setMultiByteEncoding',
{'handle': handle, 'encoding': encoding.value},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to set multi-byte encoding',
details: e.details,
);
}
}
/// Prints text using UTF-8 encoding.
@override
Future<bool> printText(int handle, String text) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'printText',
{'handle': handle, 'text': text},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to print text',
details: e.details,
);
}
}
/// Sets text alignment.
@override
Future<bool> setAlignment(int handle, PrinterAlignment alignment) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'setAlignment',
{'handle': handle, 'alignment': alignment.value},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to set alignment',
details: e.details,
);
}
}
/// Sets text scale (width and height magnification).
@override
Future<bool> setTextScale(int handle, {required int widthScale, required int heightScale}) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'setTextScale',
{
'handle': handle,
'widthScale': widthScale,
'heightScale': heightScale,
},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to set text scale',
details: e.details,
);
}
}
/// Sets text bold on or off.
@override
Future<bool> setTextBold(int handle, bool bold) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'setTextBold',
{'handle': handle, 'bold': bold ? 1 : 0},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to set text bold',
details: e.details,
);
}
}
/// Sets text underline level.
@override
Future<bool> setTextUnderline(int handle, int underline) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'setTextUnderline',
{'handle': handle, 'underline': underline},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to set text underline',
details: e.details,
);
}
}
/// Feeds paper by specified number of lines.
@override
Future<bool> feedLine(int handle, int numLines) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'feedLine',
{'handle': handle, 'numLines': numLines},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to feed line',
details: e.details,
);
}
}
/// Feeds paper by specified number of dots.
@override
Future<bool> feedDot(int handle, int numDots) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'feedDot',
{'handle': handle, 'numDots': numDots},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to feed dot',
details: e.details,
);
}
}
/// Performs a half cut of the paper.
@override
Future<bool> halfCutPaper(int handle) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'halfCutPaper',
{'handle': handle},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to half cut paper',
details: e.details,
);
}
}
/// Performs a full cut of the paper.
@override
Future<bool> fullCutPaper(int handle) async {
try {
final result = await methodChannel.invokeMethod<bool>(
'fullCutPaper',
{'handle': handle},
);
return result ?? false;
} on PlatformException catch (e) {
throw PrinterException(
code: e.code,
message: e.message ?? 'Failed to full cut paper',
details: e.details,
);
}
}
}

View File

@@ -0,0 +1,173 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'enums/multi_byte_encoding.dart';
import 'enums/printer_alignment.dart';
import 'enums/serial_flow_control.dart';
import 'enums/serial_parity.dart';
import 'enums/serial_stop_bits.dart';
import 'printer_method_channel.dart';
/// Platform interface for printer operations.
///
/// Concrete implementations (e.g., [MethodChannelPrinter]) provide
/// the actual platform-specific behavior.
abstract class PrinterPlatform extends PlatformInterface {
/// Constructs a PrinterPlatform.
PrinterPlatform() : super(token: _token);
static final Object _token = Object();
static PrinterPlatform _instance = MethodChannelPrinter();
/// The default instance of [PrinterPlatform] to use.
///
/// Defaults to [MethodChannelPrinter].
static PrinterPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [PrinterPlatform] when
/// they register themselves.
static set instance(PrinterPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
/// Returns the platform version string.
Future<String?> getPlatformVersion() {
throw UnimplementedError('getPlatformVersion() has not been implemented.');
}
/// Opens a serial (COM) port with the specified parameters.
///
/// Returns an integer handle on success.
/// Throws [PrinterException] on failure.
Future<int> openComPort({
required String portName,
required int baudRate,
int dataBits = 8,
SerialParity parity = SerialParity.none,
SerialStopBits stopBits = SerialStopBits.one,
SerialFlowControl flowControl = SerialFlowControl.none,
bool autoReplyMode = true,
}) {
throw UnimplementedError('openComPort() has not been implemented.');
}
/// Opens a USB port with the specified parameters.
///
/// Returns an integer handle on success.
/// Throws [PrinterException] on failure.
Future<int> openUsbPort({
required String portName,
bool autoReplyMode = true,
}) {
throw UnimplementedError('openUsbPort() has not been implemented.');
}
/// Closes a port by its integer handle.
///
/// Returns true if successfully closed, false otherwise.
Future<bool> closePort(int handle) {
throw UnimplementedError('closePort() has not been implemented.');
}
/// Checks if a port is currently opened.
///
/// Returns true if the port is open, false otherwise.
Future<bool> isPortOpened(int handle) {
throw UnimplementedError('isPortOpened() has not been implemented.');
}
/// Enumerates available serial (COM) ports.
///
/// Returns a list of port name strings.
Future<List<String>> enumComPorts() {
throw UnimplementedError('enumComPorts() has not been implemented.');
}
/// Enumerates available USB ports.
///
/// Returns a list of port name strings.
Future<List<String>> enumUsbPorts() {
throw UnimplementedError('enumUsbPorts() has not been implemented.');
}
/// Sets the printer to multi-byte encoding mode.
///
/// Returns true on success.
Future<bool> setMultiByteMode(int handle) {
throw UnimplementedError('setMultiByteMode() has not been implemented.');
}
/// Sets the multi-byte character encoding.
///
/// Returns true on success.
Future<bool> setMultiByteEncoding(int handle, MultiByteEncoding encoding) {
throw UnimplementedError('setMultiByteEncoding() has not been implemented.');
}
/// Prints text using UTF-8 encoding.
///
/// Returns true on success.
Future<bool> printText(int handle, String text) {
throw UnimplementedError('printText() has not been implemented.');
}
/// Sets text alignment.
///
/// Returns true on success.
Future<bool> setAlignment(int handle, PrinterAlignment alignment) {
throw UnimplementedError('setAlignment() has not been implemented.');
}
/// Sets text scale (width and height magnification).
///
/// Both scales must be between 1 and 8.
/// Returns true on success.
Future<bool> setTextScale(int handle, {required int widthScale, required int heightScale}) {
throw UnimplementedError('setTextScale() has not been implemented.');
}
/// Sets text bold on or off.
///
/// Returns true on success.
Future<bool> setTextBold(int handle, bool bold) {
throw UnimplementedError('setTextBold() has not been implemented.');
}
/// Sets text underline level.
///
/// 0 = no underline, 1 = 1-dot underline, 2 = 2-dot underline.
/// Returns true on success.
Future<bool> setTextUnderline(int handle, int underline) {
throw UnimplementedError('setTextUnderline() has not been implemented.');
}
/// Feeds paper by specified number of lines.
///
/// Returns true on success.
Future<bool> feedLine(int handle, int numLines) {
throw UnimplementedError('feedLine() has not been implemented.');
}
/// Feeds paper by specified number of dots.
///
/// Returns true on success.
Future<bool> feedDot(int handle, int numDots) {
throw UnimplementedError('feedDot() has not been implemented.');
}
/// Performs a half cut of the paper.
///
/// Returns true on success.
Future<bool> halfCutPaper(int handle) {
throw UnimplementedError('halfCutPaper() has not been implemented.');
}
/// Performs a full cut of the paper.
///
/// Returns true on success.
Future<bool> fullCutPaper(int handle) {
throw UnimplementedError('fullCutPaper() has not been implemented.');
}
}

70
pubspec.yaml Normal file
View File

@@ -0,0 +1,70 @@
name: printer
description: "A new Flutter project."
version: 0.0.1
homepage:
environment:
sdk: ^3.11.4
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.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:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.xiarui.printer
pluginClass: PrinterPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, 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 in packages, see
# https://flutter.dev/to/font-from-package

View File

@@ -0,0 +1,297 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:printer/printer_method_channel.dart';
import 'package:printer/enums/multi_byte_encoding.dart';
import 'package:printer/enums/printer_alignment.dart';
import 'package:printer/enums/serial_flow_control.dart';
import 'package:printer/enums/serial_parity.dart';
import 'package:printer/enums/serial_stop_bits.dart';
import 'package:printer/models/printer_exception.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelPrinter platform = MethodChannelPrinter();
const MethodChannel channel = MethodChannel('printer');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'getPlatformVersion') {
return '42';
}
if (methodCall.method == 'openComPort') {
return 1;
}
if (methodCall.method == 'openUsbPort') {
return 2;
}
if (methodCall.method == 'closePort') {
return true;
}
if (methodCall.method == 'isPortOpened') {
return true;
}
if (methodCall.method == 'enumComPorts') {
return ['/dev/ttyS0', '/dev/ttyS1'];
}
if (methodCall.method == 'enumUsbPorts') {
return ['USB_Printer_0'];
}
if (methodCall.method == 'setMultiByteMode') return true;
if (methodCall.method == 'setMultiByteEncoding') return true;
if (methodCall.method == 'printText') return true;
if (methodCall.method == 'setAlignment') return true;
if (methodCall.method == 'setTextScale') return true;
if (methodCall.method == 'setTextBold') return true;
if (methodCall.method == 'setTextUnderline') return true;
if (methodCall.method == 'feedLine') return true;
if (methodCall.method == 'feedDot') return true;
if (methodCall.method == 'halfCutPaper') return true;
if (methodCall.method == 'fullCutPaper') return true;
return null;
});
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
test('openComPort returns handle', () async {
final handle = await platform.openComPort(
portName: '/dev/ttyS0',
baudRate: 115200,
parity: SerialParity.none,
stopBits: SerialStopBits.one,
flowControl: SerialFlowControl.none,
);
expect(handle, 1);
});
test('openComPort passes correct parameters', () async {
late Map<dynamic, dynamic> capturedArgs;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'openComPort') {
capturedArgs = methodCall.arguments as Map<dynamic, dynamic>;
return 42;
}
return null;
});
await platform.openComPort(
portName: '/dev/ttyS0',
baudRate: 9600,
dataBits: 7,
parity: SerialParity.even,
stopBits: SerialStopBits.two,
flowControl: SerialFlowControl.rtsCts,
autoReplyMode: false,
);
expect(capturedArgs['portName'], '/dev/ttyS0');
expect(capturedArgs['baudRate'], 9600);
expect(capturedArgs['dataBits'], 7);
expect(capturedArgs['parity'], 2); // SerialParity.even
expect(capturedArgs['stopBits'], 2); // SerialStopBits.two
expect(capturedArgs['flowControl'], 2); // SerialFlowControl.rtsCts
expect(capturedArgs['autoReplyMode'], 0); // false
});
test('openUsbPort returns handle', () async {
final handle = await platform.openUsbPort(portName: 'USB_Printer_0');
expect(handle, 2);
});
test('closePort returns true', () async {
final result = await platform.closePort(1);
expect(result, true);
});
test('isPortOpened returns true', () async {
final result = await platform.isPortOpened(1);
expect(result, true);
});
test('enumComPorts returns list of strings', () async {
final ports = await platform.enumComPorts();
expect(ports, isA<List<String>>());
expect(ports, contains('/dev/ttyS0'));
expect(ports, contains('/dev/ttyS1'));
});
test('enumUsbPorts returns list of strings', () async {
final ports = await platform.enumUsbPorts();
expect(ports, isA<List<String>>());
expect(ports, contains('USB_Printer_0'));
});
test('PlatformException converts to PrinterException on openComPort', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'openComPort') {
throw PlatformException(
code: 'PORT_OPEN_FAILED',
message: 'Failed to open port',
details: {'port': '/dev/ttyS0'},
);
}
return null;
});
expect(
() async => platform.openComPort(
portName: '/dev/ttyS0',
baudRate: 115200,
),
throwsA(isA<PrinterException>()
.having((e) => e.code, 'code', 'PORT_OPEN_FAILED')
.having((e) => e.message, 'message', 'Failed to open port')),
);
});
test('PlatformException converts to PrinterException on closePort', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'closePort') {
throw PlatformException(
code: 'PORT_CLOSE_FAILED',
message: 'Failed to close port',
);
}
return null;
});
expect(
() async => platform.closePort(1),
throwsA(isA<PrinterException>()
.having((e) => e.code, 'code', 'PORT_CLOSE_FAILED')),
);
});
test('printText passes text parameter', () async {
late Map<dynamic, dynamic> capturedArgs;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'printText') {
capturedArgs = methodCall.arguments as Map<dynamic, dynamic>;
return true;
}
return null;
});
await platform.printText(1, 'Hello 中文');
expect(capturedArgs['handle'], 1);
expect(capturedArgs['text'], 'Hello 中文');
});
test('setAlignment passes alignment.value', () async {
late Map<dynamic, dynamic> capturedArgs;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'setAlignment') {
capturedArgs = methodCall.arguments as Map<dynamic, dynamic>;
return true;
}
return null;
});
await platform.setAlignment(1, PrinterAlignment.center);
expect(capturedArgs['alignment'], 1); // PrinterAlignment.center.value
});
test('setMultiByteEncoding passes encoding.value', () async {
late Map<dynamic, dynamic> capturedArgs;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'setMultiByteEncoding') {
capturedArgs = methodCall.arguments as Map<dynamic, dynamic>;
return true;
}
return null;
});
await platform.setMultiByteEncoding(1, MultiByteEncoding.utf8);
expect(capturedArgs['encoding'], 1); // MultiByteEncoding.utf8.value
});
test('setTextBold converts bool to int 0/1', () async {
late Map<dynamic, dynamic> capturedArgs;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'setTextBold') {
capturedArgs = methodCall.arguments as Map<dynamic, dynamic>;
return true;
}
return null;
});
await platform.setTextBold(1, true);
expect(capturedArgs['bold'], 1);
await platform.setTextBold(1, false);
expect(capturedArgs['bold'], 0);
});
test('feedDot passes numDots parameter', () async {
late Map<dynamic, dynamic> capturedArgs;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'feedDot') {
capturedArgs = methodCall.arguments as Map<dynamic, dynamic>;
return true;
}
return null;
});
await platform.feedDot(1, 100);
expect(capturedArgs['handle'], 1);
expect(capturedArgs['numDots'], 100);
});
test('fullCutPaper passes handle parameter', () async {
late Map<dynamic, dynamic> capturedArgs;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'fullCutPaper') {
capturedArgs = methodCall.arguments as Map<dynamic, dynamic>;
return true;
}
return null;
});
await platform.fullCutPaper(1);
expect(capturedArgs['handle'], 1);
});
test('PlatformException converts to PrinterException on printText', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'printText') {
throw PlatformException(
code: 'PRINT_FAILED',
message: 'Failed to print text',
);
}
return null;
});
expect(
() async => platform.printText(1, 'test'),
throwsA(isA<PrinterException>()
.having((e) => e.code, 'code', 'PRINT_FAILED')),
);
});
}

264
test/printer_test.dart Normal file
View File

@@ -0,0 +1,264 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:printer/printer.dart';
import 'package:printer/printer_platform_interface.dart';
import 'package:printer/printer_method_channel.dart';
import 'package:printer/enums/multi_byte_encoding.dart';
import 'package:printer/enums/printer_alignment.dart';
import 'package:printer/enums/serial_flow_control.dart';
import 'package:printer/enums/serial_parity.dart';
import 'package:printer/enums/serial_stop_bits.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockPrinterPlatform
with MockPlatformInterfaceMixin
implements PrinterPlatform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
@override
Future<int> openComPort({
required String portName,
required int baudRate,
int dataBits = 8,
SerialParity parity = SerialParity.none,
SerialStopBits stopBits = SerialStopBits.one,
SerialFlowControl flowControl = SerialFlowControl.none,
bool autoReplyMode = true,
}) => Future.value(1);
@override
Future<int> openUsbPort({
required String portName,
bool autoReplyMode = true,
}) => Future.value(2);
@override
Future<bool> closePort(int handle) => Future.value(true);
@override
Future<bool> isPortOpened(int handle) => Future.value(true);
@override
Future<List<String>> enumComPorts() => Future.value(['/dev/ttyS0']);
@override
Future<List<String>> enumUsbPorts() => Future.value(['USB_Printer_0']);
@override
Future<bool> setMultiByteMode(int handle) => Future.value(true);
@override
Future<bool> setMultiByteEncoding(int handle, MultiByteEncoding encoding) => Future.value(true);
@override
Future<bool> printText(int handle, String text) => Future.value(true);
@override
Future<bool> setAlignment(int handle, PrinterAlignment alignment) => Future.value(true);
@override
Future<bool> setTextScale(int handle, {required int widthScale, required int heightScale}) => Future.value(true);
@override
Future<bool> setTextBold(int handle, bool bold) => Future.value(true);
@override
Future<bool> setTextUnderline(int handle, int underline) => Future.value(true);
@override
Future<bool> feedLine(int handle, int numLines) => Future.value(true);
@override
Future<bool> feedDot(int handle, int numDots) => Future.value(true);
@override
Future<bool> halfCutPaper(int handle) => Future.value(true);
@override
Future<bool> fullCutPaper(int handle) => Future.value(true);
}
void main() {
final PrinterPlatform initialPlatform = PrinterPlatform.instance;
test('$MethodChannelPrinter is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelPrinter>());
});
test('getPlatformVersion', () async {
Printer printerPlugin = Printer();
MockPrinterPlatform fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printerPlugin.getPlatformVersion(), '42');
});
test('openComPort delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final handle = await printer.openComPort(
portName: '/dev/ttyS0',
baudRate: 115200,
);
expect(handle, 1);
});
test('openUsbPort delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final handle = await printer.openUsbPort(portName: 'USB_Printer_0');
expect(handle, 2);
});
test('closePort delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final result = await printer.closePort(1);
expect(result, true);
});
test('isPortOpened delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final result = await printer.isPortOpened(1);
expect(result, true);
});
test('enumComPorts delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final ports = await printer.enumComPorts();
expect(ports, ['/dev/ttyS0']);
});
test('enumUsbPorts delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final ports = await printer.enumUsbPorts();
expect(ports, ['USB_Printer_0']);
});
test('openComPortWithHandle returns valid PrinterPortHandle', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final portHandle = await printer.openComPortWithHandle(
portName: '/dev/ttyS0',
baudRate: 115200,
);
expect(portHandle.handle, 1);
expect(portHandle.isValid, true);
await portHandle.close();
expect(portHandle.isValid, false);
});
test('openUsbPortWithHandle returns valid PrinterPortHandle', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
final portHandle = await printer.openUsbPortWithHandle(
portName: 'USB_Printer_0',
);
expect(portHandle.handle, 2);
expect(portHandle.isValid, true);
});
test('setMultiByteMode delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.setMultiByteMode(1), true);
});
test('setMultiByteEncoding delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.setMultiByteEncoding(1, MultiByteEncoding.utf8), true);
});
test('printText delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.printText(1, 'Hello 中文'), true);
});
test('setAlignment delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.setAlignment(1, PrinterAlignment.center), true);
});
test('setTextScale delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.setTextScale(1, widthScale: 2, heightScale: 2), true);
});
test('setTextBold delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.setTextBold(1, true), true);
});
test('setTextUnderline delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.setTextUnderline(1, 1), true);
});
test('feedLine delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.feedLine(1, 5), true);
});
test('feedDot delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.feedDot(1, 100), true);
});
test('halfCutPaper delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.halfCutPaper(1), true);
});
test('fullCutPaper delegates to platform interface', () async {
final printer = Printer();
final fakePlatform = MockPrinterPlatform();
PrinterPlatform.instance = fakePlatform;
expect(await printer.fullCutPaper(1), true);
});
}