196 lines
5.7 KiB
Dart
196 lines
5.7 KiB
Dart
import 'package:json_annotation/json_annotation.dart';
|
||
|
||
part 'serial_card_models.g.dart';
|
||
|
||
const String _serialCardDefaultKeyHex = 'FFFFFFFFFFFF';
|
||
|
||
/// DK25-ST 可以识别的卡片类型。
|
||
enum SerialCardType {
|
||
undefined(0x00, '未定义', null),
|
||
mifare(0x01, 'Mifare(M1)', 4),
|
||
ultralight(0x02, 'Ultralight / NFC 标签', 7),
|
||
iso14443B(0x03, 'ISO14443-B', 8),
|
||
iso14443ACpu(0x04, 'ISO14443-A CPU 卡', 4),
|
||
iso15693(0x05, 'ISO15693', 8);
|
||
|
||
const SerialCardType(this.code, this.title, this.uidLength);
|
||
|
||
final int code;
|
||
final String title;
|
||
final int? uidLength;
|
||
|
||
static SerialCardType fromCode(int code) {
|
||
return values.where((type) => type.code == code).firstOrNull ?? undefined;
|
||
}
|
||
}
|
||
|
||
/// 业务卡号读取策略,旧饭卡默认保留 M1 数据块优先的行为。
|
||
@JsonEnum(valueField: 'code')
|
||
enum SerialCardReadMode {
|
||
smart('smart', '智能识别'),
|
||
uidOnly('uid_only', '仅读取 UID'),
|
||
mifareBlockOnly('mifare_block_only', '仅 M1 业务块');
|
||
|
||
const SerialCardReadMode(this.code, this.title);
|
||
|
||
final String code;
|
||
final String title;
|
||
}
|
||
|
||
/// M1 卡扇区认证使用的密钥类型。
|
||
@JsonEnum(valueField: 'code')
|
||
enum SerialMifareKeyType {
|
||
keyA('key_a', 'KEY A', 0x0A),
|
||
keyB('key_b', 'KEY B', 0x0B);
|
||
|
||
const SerialMifareKeyType(this.code, this.title, this.protocolValue);
|
||
|
||
final String code;
|
||
final String title;
|
||
final int protocolValue;
|
||
}
|
||
|
||
/// 读写卡配置保存在已有 setting.sysJson 扩展键中,不改变数据库结构。
|
||
@JsonSerializable()
|
||
class SerialCardConfig {
|
||
static const String defaultKeyHex = _serialCardDefaultKeyHex;
|
||
|
||
const SerialCardConfig({
|
||
this.readMode = SerialCardReadMode.smart,
|
||
this.mifareBlockAddress = 1,
|
||
this.mifareKeyType = SerialMifareKeyType.keyA,
|
||
this.keyAHex = _serialCardDefaultKeyHex,
|
||
this.keyBHex = _serialCardDefaultKeyHex,
|
||
this.showMifareScanButton = false,
|
||
});
|
||
|
||
@JsonKey(unknownEnumValue: SerialCardReadMode.smart)
|
||
final SerialCardReadMode readMode;
|
||
|
||
@JsonKey(fromJson: _serialCardBlockAddressFromJson)
|
||
final int mifareBlockAddress;
|
||
|
||
@JsonKey(unknownEnumValue: SerialMifareKeyType.keyA)
|
||
final SerialMifareKeyType mifareKeyType;
|
||
|
||
@JsonKey(
|
||
fromJson: _serialCardKeyFromJson,
|
||
toJson: _serialCardKeyToJson,
|
||
)
|
||
final String keyAHex;
|
||
|
||
@JsonKey(
|
||
fromJson: _serialCardKeyFromJson,
|
||
toJson: _serialCardKeyToJson,
|
||
)
|
||
final String keyBHex;
|
||
|
||
/// M1 全块扫描属于现场诊断能力,默认隐藏,避免普通业务读卡误触耗时扫描。
|
||
final bool showMifareScanButton;
|
||
|
||
String get selectedKeyHex =>
|
||
mifareKeyType == SerialMifareKeyType.keyA ? keyAHex : keyBHex;
|
||
|
||
SerialCardConfig copyWith({
|
||
SerialCardReadMode? readMode,
|
||
int? mifareBlockAddress,
|
||
SerialMifareKeyType? mifareKeyType,
|
||
String? keyAHex,
|
||
String? keyBHex,
|
||
bool? showMifareScanButton,
|
||
}) {
|
||
return SerialCardConfig(
|
||
readMode: readMode ?? this.readMode,
|
||
mifareBlockAddress: mifareBlockAddress ?? this.mifareBlockAddress,
|
||
mifareKeyType: mifareKeyType ?? this.mifareKeyType,
|
||
keyAHex: keyAHex ?? this.keyAHex,
|
||
keyBHex: keyBHex ?? this.keyBHex,
|
||
showMifareScanButton: showMifareScanButton ?? this.showMifareScanButton,
|
||
);
|
||
}
|
||
|
||
factory SerialCardConfig.fromJson(Map<String, dynamic> json) =>
|
||
_$SerialCardConfigFromJson(json);
|
||
|
||
Map<String, dynamic> toJson() => _$SerialCardConfigToJson(this);
|
||
}
|
||
|
||
/// M1 数据块扫描中发现的非空内容。
|
||
///
|
||
/// 扫描严格跳过厂商块和扇区密钥块,因此这里只包含允许读取的普通数据块。
|
||
class SerialMifareBlockScanItem {
|
||
const SerialMifareBlockScanItem({
|
||
required this.blockAddress,
|
||
required this.hex,
|
||
this.text,
|
||
});
|
||
|
||
final int blockAddress;
|
||
final String hex;
|
||
final String? text;
|
||
|
||
/// 单个 M1 数据块最多保存 16 字节文本;不再限定历史卡号必须为 12 位。
|
||
///
|
||
/// 该判断只用于界面标记候选位置,最终仍需结合实际发卡规则确认。
|
||
bool get isCardNumberCandidate {
|
||
final value = text;
|
||
return value != null && RegExp(r'^[a-zA-Z0-9]{1,16}$').hasMatch(value);
|
||
}
|
||
}
|
||
|
||
/// M1 安全数据块只读扫描进度。
|
||
class SerialMifareScanProgress {
|
||
const SerialMifareScanProgress({
|
||
required this.processedBlocks,
|
||
required this.totalBlocks,
|
||
required this.readableBlocks,
|
||
required this.failedBlocks,
|
||
required this.items,
|
||
this.currentBlockAddress,
|
||
this.completed = false,
|
||
this.detectedS50 = false,
|
||
this.stopReason,
|
||
});
|
||
|
||
const SerialMifareScanProgress.initial({required this.totalBlocks})
|
||
: processedBlocks = 0,
|
||
readableBlocks = 0,
|
||
failedBlocks = 0,
|
||
items = const [],
|
||
currentBlockAddress = null,
|
||
completed = false,
|
||
detectedS50 = false,
|
||
stopReason = null;
|
||
|
||
final int processedBlocks;
|
||
final int totalBlocks;
|
||
final int readableBlocks;
|
||
final int failedBlocks;
|
||
final List<SerialMifareBlockScanItem> items;
|
||
final int? currentBlockAddress;
|
||
final bool completed;
|
||
|
||
/// 旧版扫描容量推断标记;当前完整扫描 215 个安全块时固定为 false,仅保留结果模型兼容性。
|
||
final bool detectedS50;
|
||
|
||
/// 非正常完整遍历时的结束原因;已读取到的块结果仍然保留并展示。
|
||
final String? stopReason;
|
||
|
||
double get fraction => totalBlocks <= 0
|
||
? 0
|
||
: (processedBlocks / totalBlocks).clamp(0, 1).toDouble();
|
||
}
|
||
|
||
int _serialCardBlockAddressFromJson(Object? value) {
|
||
return value is int && value >= 0 && value <= 255 ? value : 1;
|
||
}
|
||
|
||
String _serialCardKeyFromJson(Object? value) {
|
||
final key = value?.toString().replaceAll(RegExp(r'\s+'), '').toUpperCase();
|
||
return key != null && RegExp(r'^[0-9A-F]{12}$').hasMatch(key)
|
||
? key
|
||
: SerialCardConfig.defaultKeyHex;
|
||
}
|
||
|
||
String _serialCardKeyToJson(String value) => _serialCardKeyFromJson(value);
|