用户实测定论: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 更新双平台放置约定
164 lines
6.5 KiB
Dart
164 lines
6.5 KiB
Dart
import 'dart:io';
|
||
|
||
import '../utils/command_runner.dart';
|
||
|
||
/// 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 || Platform.isWindows;
|
||
|
||
/// 驱动是否已安装(macOS:驱动目录;Windows:安装标记,同步场景请用 queryInstalled)。
|
||
static bool isInstalled() {
|
||
if (Platform.isMacOS) return Directory(installedMarker).existsSync();
|
||
return false; // Windows 走异步 queryInstalled(注册表查询需要子进程)
|
||
}
|
||
|
||
/// 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');
|
||
}
|
||
|
||
/// 内置驱动安装包的文件系统路径;未打包携带返回 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);
|
||
if (match.isEmpty) {
|
||
throw StateError('驱动 dmg 挂载失败');
|
||
}
|
||
final mountPoint = match.last.group(0)!;
|
||
log('驱动卷已挂载:$mountPoint');
|
||
|
||
try {
|
||
final pkg = Directory(mountPoint)
|
||
.listSync()
|
||
.whereType<File>()
|
||
.where((f) => f.path.toLowerCase().endsWith('.pkg'))
|
||
.toList();
|
||
if (pkg.isEmpty) {
|
||
throw StateError('驱动 dmg 内未找到 .pkg 安装包');
|
||
}
|
||
final pkgPath = pkg.first.path;
|
||
|
||
final script =
|
||
'do shell script "installer -pkg \\"$pkgPath\\" -target /" '
|
||
'with administrator privileges with prompt "安装 HiTi 打印机驱动"';
|
||
log('开始安装驱动(请在系统弹窗中输入管理员密码)…');
|
||
await runCommand('osascript', ['-e', script], timeoutMs: 180000);
|
||
log('驱动安装完成');
|
||
} finally {
|
||
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;
|
||
}
|
||
}
|