feat(windows): 向导内置 HiTi Windows 驱动一键安装(对齐 mac 的"装 App 即用"体验)
用户实测定论:CS-2xx 的 USB 是自定义设备(驱动包内含 CS2xxhcd.inf 内核驱动), PavoApi 直打依赖驱动建立的 USB 通道——无驱动时枚举到设备但状态返回离线, "免驱动"假设不成立。产品要求两平台一致:驱动随 App 内置、向导一键安装。 - assets/drivers/CS2xx_Windows_Driver.zip:HiTi 官网 CS-2xx Windows 驱动 (2.6.25.48)整目录 zip(含 CS2xx/Setup64.exe),33MB,不入 git - DriverInstall Windows 分支:Expand-Archive 解压到临时目录 → 递归定位 Setup64.exe(兜底 setup/autorun)→ PowerShell Start-Process -Verb RunAs -Wait 提权安装(UAC 一次)→ 写安装标记(HKCU\Software\zax\desktop-plugin, 系统驱动安装状态无统一可查标志)→ finally 清理临时目录 - 向导第 2 步统一双平台状态机:驱动未装(异步查标记/目录)→"一键安装驱动 (内置安装包)";轮询 3 秒自动刷新安装/连接状态 - bundledInstallerPath 双平台资产路径推导(mac .dmg / win .zip); .gitignore 增 *.zip;assets/drivers/README 更新双平台放置约定
This commit is contained in:
1 parent
527baf7ec5
commit
170e9ddc5e
4 files changed
+156
-53
No files matched your search
@@ -45,3 +45,5 @@ app.*.map.json
|
||||
/android/app/release
|
||||
/dist/
|
||||
assets/drivers/*.dmg
|
||||
assets/drivers/*.exe
|
||||
assets/drivers/*.zip
|
||||
@@ -1,9 +1,10 @@
|
||||
# HiTi Mac 驱动安装包目录
|
||||
# HiTi 驱动安装包目录
|
||||
|
||||
此目录用于打包时**内置 HiTi macOS 打印驱动**(向导"一键安装驱动"功能的数据源):
|
||||
此目录用于打包时**内置 HiTi 打印驱动**(向导"一键安装驱动"功能的数据源):
|
||||
|
||||
- 构建 macOS 安装包前,把 `HiTiCardTypeMacDrivers_2.4.2.dmg`(约 25MB)放入本目录,
|
||||
然后执行 `bash tool/package_macos.sh`——驱动会随 dmg 安装包分发
|
||||
- **dmg 文件本身不入 git**(.gitignore 已忽略 `assets/drivers/*.dmg`)
|
||||
- 缺少 dmg 时构建不受影响;向导会自动降级为"官网链接手动下载"模式
|
||||
- **Windows 不需要此目录放任何东西**(PavoApi 直打,驱动可选)
|
||||
- **macOS**:放 `HiTiCardTypeMacDrivers_2.4.2.dmg`(约 25MB)
|
||||
- **Windows**:放 `CS2xx_Windows_Driver.zip`(HiTi 官网 CS-2xx Windows 驱动整个文件夹打成 zip,
|
||||
内含 `CS2xx/Setup64.exe`;约 33MB)。mac 上打 zip 注意排除 `__MACOSX`:
|
||||
`cd <驱动所在目录> && zip -r CS2xx_Windows_Driver.zip CS2_Driver_x.x.x.x -x "*.DS_Store"`
|
||||
- 安装包文件**不入 git**(.gitignore 已忽略 `*.dmg/*.exe/*.zip`),构建安装包的机器放好后再打包
|
||||
- 缺失时构建不受影响;向导自动降级为"官网链接手动下载"模式
|
||||
+119
-34
@@ -2,44 +2,85 @@ import 'dart:io';
|
||||
|
||||
import '../utils/command_runner.dart';
|
||||
|
||||
/// HiTi Mac 驱动(CUPS 过滤器)内置安装:App 资源携带驱动 dmg,
|
||||
/// 向导一键挂载静默安装(osascript 提权一次),已装判断 = /Library/Printers/HiTi 存在。
|
||||
/// Windows 不适用(PavoApi 直打,官方驱动可选)。
|
||||
/// HiTi 打印驱动内置安装:App 资源携带驱动安装包,向导一键安装。
|
||||
///
|
||||
/// - macOS:CUPS 过滤器 pkg(dmg 内置),已装判断 = /Library/Printers/HiTi 存在,
|
||||
/// 安装 = 挂载 dmg → osascript 提权 installer → 卸载卷
|
||||
/// - Windows:HiTi 官方驱动 exe(assets/drivers/*.exe 内置),UAC 提权运行;
|
||||
/// 已装判断 = 我们自己的注册表标记(HKCU\Software\zax\desktop-plugin,
|
||||
/// 系统级驱动安装状态无统一可查标志,安装成功后写标记最可靠)
|
||||
class DriverInstall {
|
||||
static const installedMarker = '/Library/Printers/HiTi';
|
||||
static const _winMarkKey = r'HKCU\Software\zax\desktop-plugin';
|
||||
static const _winMarkValue = 'HiTiDriverInstalled';
|
||||
|
||||
static bool get isSupported => Platform.isMacOS;
|
||||
static bool get isSupported => Platform.isMacOS || Platform.isWindows;
|
||||
|
||||
/// 驱动是否已安装。
|
||||
static bool isInstalled() => Directory(installedMarker).existsSync();
|
||||
|
||||
/// 内置驱动 dmg 的文件系统路径;未打包携带(如开发运行/产物缺失)返回 null。
|
||||
///
|
||||
/// Flutter assets 在 .app 内的真实路径:
|
||||
/// [app]/Contents/Frameworks/App.framework/Resources/flutter_assets/[asset]
|
||||
/// 从可执行文件位置反推(exe 在 [app]/Contents/MacOS/)。
|
||||
static String? bundledDmgPath() {
|
||||
if (!Platform.isMacOS) return null;
|
||||
final exe = Platform.resolvedExecutable;
|
||||
final exeDir = File(exe).parent.path; // .../Contents/MacOS
|
||||
final driversDir =
|
||||
'$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers';
|
||||
final dir = Directory(driversDir);
|
||||
if (!dir.existsSync()) return null;
|
||||
final dmgs = dir
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.toLowerCase().endsWith('.dmg'))
|
||||
.toList();
|
||||
return dmgs.isEmpty ? null : dmgs.first.path;
|
||||
/// 驱动是否已安装(macOS:驱动目录;Windows:安装标记,同步场景请用 queryInstalled)。
|
||||
static bool isInstalled() {
|
||||
if (Platform.isMacOS) return Directory(installedMarker).existsSync();
|
||||
return false; // Windows 走异步 queryInstalled(注册表查询需要子进程)
|
||||
}
|
||||
|
||||
/// 从内置 dmg 静默安装驱动(挂载 → 找 pkg → osascript 提权 installer → 卸载卷)。
|
||||
/// 提权弹窗由系统显示(一次管理员密码),超时 3 分钟。
|
||||
static Future<void> installFromDmg(String dmgPath, {void Function(String line)? onLog}) async {
|
||||
final log = onLog ?? (_) {};
|
||||
/// Windows:异步查询安装标记。
|
||||
static Future<bool> queryInstalled() async {
|
||||
if (!Platform.isWindows) return isInstalled();
|
||||
final out = await runCommand(
|
||||
'reg',
|
||||
['query', _winMarkKey, '/v', _winMarkValue],
|
||||
tolerateExit: true,
|
||||
);
|
||||
return out.contains('0x1');
|
||||
}
|
||||
|
||||
// 1. 只读挂载(挂载点带随机序号,必须从输出解析,不能拼名字)
|
||||
/// 内置驱动安装包的文件系统路径;未打包携带返回 null。
|
||||
///
|
||||
/// Flutter assets 在包内的真实路径(两平台结构不同):
|
||||
/// - macOS:[exe]/../Frameworks/App.framework/Resources/flutter_assets
|
||||
/// - Windows:[exe]/data/flutter_assets
|
||||
static String? bundledInstallerPath() {
|
||||
if (Platform.isMacOS) {
|
||||
final exeDir = File(Platform.resolvedExecutable).parent.path;
|
||||
return _firstFile(
|
||||
'$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers',
|
||||
'.dmg');
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
final exeDir = File(Platform.resolvedExecutable).parent.path;
|
||||
return _firstFile('$exeDir/data/flutter_assets/assets/drivers', '.zip');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _firstFile(String dirPath, String ext) {
|
||||
final dir = Directory(dirPath);
|
||||
if (!dir.existsSync()) return null;
|
||||
final files = dir
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.toLowerCase().endsWith(ext))
|
||||
.toList();
|
||||
return files.isEmpty ? null : files.first.path;
|
||||
}
|
||||
|
||||
/// 静默安装(macOS)/提权安装(Windows,UAC 确认一次)。
|
||||
static Future<void> install(String installerPath, {void Function(String line)? onLog}) async {
|
||||
final log = onLog ?? (_) {};
|
||||
if (Platform.isMacOS) {
|
||||
await _installMac(installerPath, log);
|
||||
return;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
await _installWindows(installerPath, log);
|
||||
return;
|
||||
}
|
||||
throw UnsupportedError('当前平台不支持驱动安装');
|
||||
}
|
||||
|
||||
// ---------------- macOS:dmg → pkg → osascript 提权 ----------------
|
||||
|
||||
static Future<void> _installMac(String dmgPath, void Function(String) log) async {
|
||||
// 挂载点带随机序号,必须从 attach 输出解析,不能拼名字
|
||||
final attachOut = await runCommand('hdiutil', ['attach', '-nobrowse', '-readonly', dmgPath],
|
||||
timeoutMs: 30000);
|
||||
final match = RegExp(r'/Volumes/[^\t\n]+').allMatches(attachOut);
|
||||
@@ -50,7 +91,6 @@ class DriverInstall {
|
||||
log('驱动卷已挂载:$mountPoint');
|
||||
|
||||
try {
|
||||
// 2. 找 pkg
|
||||
final pkg = Directory(mountPoint)
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
@@ -61,7 +101,6 @@ class DriverInstall {
|
||||
}
|
||||
final pkgPath = pkg.first.path;
|
||||
|
||||
// 3. osascript 提权安装(AppleScript 字符串内嵌套引号用 \" 转义)
|
||||
final script =
|
||||
'do shell script "installer -pkg \\"$pkgPath\\" -target /" '
|
||||
'with administrator privileges with prompt "安装 HiTi 打印机驱动"';
|
||||
@@ -69,10 +108,56 @@ class DriverInstall {
|
||||
await runCommand('osascript', ['-e', script], timeoutMs: 180000);
|
||||
log('驱动安装完成');
|
||||
} finally {
|
||||
// 4. 卸载卷(失败不影响安装结果)
|
||||
try {
|
||||
await runCommand('hdiutil', ['detach', mountPoint, '-force'], tolerateExit: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Windows:解压 zip → UAC 提权 Setup64 → 写安装标记 ----------------
|
||||
|
||||
static Future<void> _installWindows(String zipPath, void Function(String) log) async {
|
||||
// 1. 解压到临时目录(驱动是"安装器 + 数据文件"的多文件结构,内置为 zip)
|
||||
final extractDir =
|
||||
'${Directory.systemTemp.path}/zax-hiti-driver-${DateTime.now().millisecondsSinceEpoch}';
|
||||
log('解压驱动安装包…');
|
||||
await runCommand('powershell', [
|
||||
'-Command',
|
||||
'Expand-Archive -Path "$zipPath" -DestinationPath "$extractDir" -Force'
|
||||
], timeoutMs: 120000);
|
||||
|
||||
try {
|
||||
// 2. 递归找安装器(优先 64 位 Setup64.exe,兜底 autorun.exe)
|
||||
final setup = _findFileDeep(extractDir, 'setup64.exe') ??
|
||||
_findFileDeep(extractDir, 'setup.exe') ??
|
||||
_findFileDeep(extractDir, 'autorun.exe');
|
||||
if (setup == null) {
|
||||
throw StateError('驱动包内未找到安装程序(Setup64.exe)');
|
||||
}
|
||||
log('开始安装 HiTi 打印机驱动(请在 UAC 弹窗中确认,安装向导可能需要点几次"下一步")…');
|
||||
// -Verb RunAs 触发 UAC;-Wait 等安装程序退出(用户取消 UAC 时 PowerShell 报错)
|
||||
final script = 'Start-Process -FilePath "$setup" -Verb RunAs -Wait';
|
||||
await runCommand('powershell', ['-Command', script], timeoutMs: 600000);
|
||||
log('驱动安装程序已退出');
|
||||
} finally {
|
||||
// 3. 清理临时目录(失败不影响安装结果)
|
||||
try {
|
||||
await Directory(extractDir).delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 4. 写安装标记(我们自己的注册表键;0x1 = 已装)
|
||||
await runCommand('reg',
|
||||
['add', _winMarkKey, '/v', _winMarkValue, '/t', 'REG_DWORD', '/d', '1', '/f']);
|
||||
log('已记录驱动安装状态');
|
||||
}
|
||||
|
||||
static String? _findFileDeep(String root, String lowerName) {
|
||||
final hits = Directory(root)
|
||||
.listSync(recursive: true, followLinks: false)
|
||||
.whereType<File>()
|
||||
.where((f) => f.uri.pathSegments.last.toLowerCase() == lowerName)
|
||||
.toList();
|
||||
return hits.isEmpty ? null : hits.first.path;
|
||||
}
|
||||
}
|
||||
+27
-12
@@ -48,17 +48,22 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
String? _driverInstallError;
|
||||
PrinterStatus? _printerStatus;
|
||||
|
||||
/// 驱动是否已装(null=查询中;mac=驱动目录,win=注册表安装标记)
|
||||
bool? _driverInstalled;
|
||||
|
||||
Agent get _agent => widget.agent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkPrinter();
|
||||
_queryDriver();
|
||||
// 第 2 步期间自动轮询:用户手动装驱动 / 插上打印机后页面自动刷新
|
||||
// (驱动目录判断零成本;getStatus 走 lpoptions,3s 一次可接受)
|
||||
_pollTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
if (_step == 1 && mounted && !_driverInstalling) {
|
||||
_checkPrinter(silent: true);
|
||||
_queryDriver();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -71,6 +76,11 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _queryDriver() async {
|
||||
final installed = await DriverInstall.queryInstalled();
|
||||
if (mounted) setState(() => _driverInstalled = installed);
|
||||
}
|
||||
|
||||
Future<void> _checkPrinter({bool silent = false}) async {
|
||||
if (!silent) setState(() => _printerChecking = true);
|
||||
try {
|
||||
@@ -192,16 +202,17 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
|
||||
// ---------------- Step 1:打印机检测 ----------------
|
||||
|
||||
/// 一键安装内置驱动(mac):装完自动重检(队列要等打印机连上/系统识别后才出现)。
|
||||
/// 一键安装内置驱动:装完自动重检(队列要等打印机连上/系统识别后才出现)。
|
||||
Future<void> _installDriver() async {
|
||||
final dmg = DriverInstall.bundledDmgPath();
|
||||
if (dmg == null) return;
|
||||
final installer = DriverInstall.bundledInstallerPath();
|
||||
if (installer == null) return;
|
||||
setState(() {
|
||||
_driverInstalling = true;
|
||||
_driverInstallError = null;
|
||||
});
|
||||
try {
|
||||
await DriverInstall.installFromDmg(dmg, onLog: _agent.log.write);
|
||||
await DriverInstall.install(installer, onLog: _agent.log.write);
|
||||
await _queryDriver();
|
||||
await _checkPrinter();
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _driverInstallError = '$e');
|
||||
@@ -213,8 +224,8 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
Widget _stepPrinter(ColorScheme scheme) {
|
||||
final s = _printerStatus;
|
||||
final ready = s != null && s.online;
|
||||
final driverMissing = Platform.isMacOS && !DriverInstall.isInstalled();
|
||||
final bundledDmg = Platform.isMacOS ? DriverInstall.bundledDmgPath() : null;
|
||||
final driverMissing = _driverInstalled == false;
|
||||
final bundledInstaller = DriverInstall.bundledInstallerPath();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -227,17 +238,19 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// mac:驱动未装 → 首选一键安装内置驱动,其次官网链接
|
||||
// 驱动未装 → 首选一键安装内置驱动,其次官网链接(mac/win 通用)
|
||||
if (driverMissing) ...[
|
||||
Text(
|
||||
'本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。',
|
||||
Platform.isMacOS
|
||||
? '本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。'
|
||||
: '本机尚未安装 HiTi 打印机驱动。直打通道需要驱动建立的 USB 连接,装好即可自动识别打印机。',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: _driverInstalling || bundledDmg == null ? null : _installDriver,
|
||||
onPressed: _driverInstalling || bundledInstaller == null ? null : _installDriver,
|
||||
icon: _driverInstalling
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
@@ -246,7 +259,7 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
)
|
||||
: const Icon(Icons.download, size: 16),
|
||||
label: Text(_driverInstalling
|
||||
? '正在安装…(弹窗中输入管理员密码)'
|
||||
? '正在安装…(请在系统弹窗中确认)'
|
||||
: '一键安装驱动(内置安装包)'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -257,7 +270,7 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (bundledDmg == null)
|
||||
if (bundledInstaller == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
@@ -278,7 +291,9 @@ class _SetupWizardState extends State<SetupWizard> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'安装完成后页面会自动刷新(也可手动重新检测);随后 USB 连接打印机并开机,系统识别出现打印队列即完成。',
|
||||
Platform.isMacOS
|
||||
? '安装完成后:USB 连接打印机并开机,系统识别出现打印队列即完成。'
|
||||
: '安装完成后 USB 连接打印机并开机,回到这里重新检测即完成。',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
Reference in new issue
Block a user