vite-uni-dev-tool 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/const.cjs +1 -1
- package/dist/const.d.ts +12 -0
- package/dist/const.d.ts.map +1 -1
- package/dist/const.js +1 -1
- package/dist/core-shared.d.ts +1 -1
- package/dist/core-shared.d.ts.map +1 -1
- package/dist/core-shared.js +1 -1
- package/dist/core.d.ts +10 -3
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +2 -2
- package/dist/i18n/locales/en.cjs +1 -1
- package/dist/i18n/locales/en.d.ts +81 -0
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.cjs +1 -1
- package/dist/i18n/locales/zh-Hans.d.ts +82 -1
- package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/modules/devConsole/index.cjs +1 -1
- package/dist/modules/devConsole/index.js +3 -3
- package/dist/modules/devEvent/index.cjs +3 -3
- package/dist/modules/devEvent/index.d.ts +1 -0
- package/dist/modules/devEvent/index.d.ts.map +1 -1
- package/dist/modules/devEvent/index.js +3 -3
- package/dist/modules/devIntercept/index.cjs +14 -13
- package/dist/modules/devIntercept/index.d.ts +19 -0
- package/dist/modules/devIntercept/index.d.ts.map +1 -1
- package/dist/modules/devIntercept/index.js +14 -13
- package/dist/modules/devStore/index.cjs +1 -1
- package/dist/modules/devStore/index.d.ts +21 -0
- package/dist/modules/devStore/index.d.ts.map +1 -1
- package/dist/modules/devStore/index.js +1 -1
- package/dist/plugins/uniDevTool/transform/transformMain.cjs +1 -1
- package/dist/plugins/uniDevTool/transform/transformMain.js +1 -1
- package/dist/type.d.ts +47 -2
- package/dist/type.d.ts.map +1 -1
- package/dist/utils/language.cjs +1 -1
- package/dist/utils/language.d.ts.map +1 -1
- package/dist/utils/language.js +1 -1
- package/dist/utils/object.cjs +1 -1
- package/dist/utils/object.d.ts.map +1 -1
- package/dist/utils/object.js +1 -1
- package/dist/v3/DevTool/components/BluetoothList/BluetoothItem.vue +199 -0
- package/dist/v3/DevTool/components/BluetoothList/BluetoothTool.vue +730 -0
- package/dist/v3/DevTool/components/BluetoothList/index.vue +167 -0
- package/dist/v3/{CaptureScreen → DevTool/components/CaptureScreen}/index.vue +109 -109
- package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/ConsoleItem.vue +225 -224
- package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/RunJSInput.vue +247 -249
- package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/index.vue +171 -160
- package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/staticTips.ts +1145 -1145
- package/dist/v3/{DevToolButton → DevTool/components/DevToolButton}/index.vue +7 -4
- package/dist/v3/{DevToolTitle → DevTool/components/DevToolTitle}/index.vue +24 -24
- package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/DevToolOverlay.vue +197 -182
- package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/const.ts +28 -5
- package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/dataUtils.ts +48 -48
- package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/useDevToolData.ts +387 -338
- package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/useDevToolHandlers.ts +629 -549
- package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/useDevToolOverlay.ts +197 -184
- package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/index.vue +67 -16
- package/dist/v3/{ElEvent → DevTool/components/ElEvent}/ElEventItem.vue +105 -105
- package/dist/v3/{ElEvent → DevTool/components/ElEvent}/index.vue +106 -109
- package/dist/v3/{Instance → DevTool/components/Instance}/components/InstanceTreeNode.vue +265 -265
- package/dist/v3/{Instance → DevTool/components/Instance}/flatten.ts +226 -226
- package/dist/v3/{Instance → DevTool/components/Instance}/index.vue +94 -94
- package/dist/v3/{Instance → DevTool/components/Instance}/registry.ts +49 -49
- package/dist/v3/{Instance → DevTool/components/Instance}/transformTree.ts +375 -375
- package/dist/v3/{Instance → DevTool/components/Instance}/transformTreeCtx.ts +268 -268
- package/dist/v3/{Instance → DevTool/components/Instance}/typing.d.ts +43 -43
- package/dist/v3/{InstanceDetail → DevTool/components/InstanceDetail}/index.vue +485 -485
- package/dist/v3/{JsonDetail → DevTool/components/JsonDetail}/index.vue +70 -70
- package/dist/v3/{NFCList → DevTool/components/NFCList}/NFCItem.vue +112 -113
- package/dist/v3/{NFCList → DevTool/components/NFCList}/NFCTool.vue +454 -478
- package/dist/v3/{NFCList → DevTool/components/NFCList}/const.ts +56 -56
- package/dist/v3/{NFCList → DevTool/components/NFCList}/index.vue +94 -98
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/InterceptConfig.vue +624 -608
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/InterceptItem.vue +140 -140
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkDetail.vue +287 -296
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkIntercept.vue +88 -93
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkItem.vue +163 -167
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkSend.vue +589 -556
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/const.ts +4 -4
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/hooks/useNetworkForm.ts +86 -86
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/index.vue +160 -160
- package/dist/v3/{NetworkList → DevTool/components/NetworkList}/utils.ts +101 -101
- package/dist/v3/{Performance → DevTool/components/Performance}/index.vue +498 -495
- package/dist/v3/{Performance → DevTool/components/Performance}/modules/PerformanceMetrics.vue +153 -153
- package/dist/v3/{Performance → DevTool/components/Performance}/modules/PerformanceWidget.vue +12 -9
- package/dist/v3/{Performance → DevTool/components/Performance}/modules/usePerformanceChart.ts +460 -460
- package/dist/v3/{Performance → DevTool/components/Performance}/modules/usePerformanceData.ts +258 -258
- package/dist/v3/{PiniaList → DevTool/components/PiniaList}/index.vue +93 -94
- package/dist/v3/{RouteList → DevTool/components/RouteList}/index.vue +21 -24
- package/dist/v3/{RunJS → DevTool/components/RunJS}/index.vue +148 -148
- package/dist/v3/{ScanCodeList → DevTool/components/ScanCodeList}/ScanCodeItem.vue +97 -98
- package/dist/v3/{ScanCodeList → DevTool/components/ScanCodeList}/index.vue +100 -104
- package/dist/v3/{SettingButton → DevTool/components/SettingButton}/index.vue +45 -45
- package/dist/v3/{SettingList → DevTool/components/SettingList}/index.vue +218 -150
- package/dist/v3/DevTool/components/SettingList/modules/SettingBarrage.vue +304 -0
- package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingDevTool.vue +212 -208
- package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingInfo.vue +157 -119
- package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingLanguage.vue +74 -74
- package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingLog.vue +230 -230
- package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingNetwork.vue +3 -3
- package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingTheme.vue +37 -7
- package/dist/v3/{SettingList → DevTool/components/SettingList}/typing.d.ts +2 -2
- package/dist/v3/{SourceCode → DevTool/components/SourceCode}/Line.vue +127 -116
- package/dist/v3/{SourceCode → DevTool/components/SourceCode}/index.vue +8 -8
- package/dist/v3/{SourceCode → DevTool/components/SourceCode}/parseCode.ts +609 -701
- package/dist/v3/{StorageList → DevTool/components/StorageList}/index.vue +174 -174
- package/dist/v3/{TransferList → DevTool/components/TransferList}/TransferDetail.vue +268 -268
- package/dist/v3/{TransferList → DevTool/components/TransferList}/TransferItem.vue +4 -4
- package/dist/v3/{TransferList → DevTool/components/TransferList}/index.vue +8 -8
- package/dist/v3/{UniEvent → DevTool/components/UniEvent}/UniEventItem.vue +6 -7
- package/dist/v3/{UniEvent → DevTool/components/UniEvent}/index.vue +6 -6
- package/dist/v3/{VuexList → DevTool/components/VuexList}/index.vue +84 -84
- package/dist/v3/{WebSocket → DevTool/components/WebSocket}/WebSocketDetail.vue +8 -8
- package/dist/v3/{WebSocket → DevTool/components/WebSocket}/WebSocketItem.vue +4 -4
- package/dist/v3/{WebSocket → DevTool/components/WebSocket}/index.vue +8 -8
- package/dist/v3/DevTool/index.vue +179 -5
- package/dist/v3/{AppTransition → components/AppTransition}/index.vue +176 -170
- package/dist/v3/{AutoSizer → components/AutoSizer}/index.vue +192 -192
- package/dist/v3/{AutoSizer → components/AutoSizer}/index1.vue +184 -184
- package/dist/v3/{AutoSizer → components/AutoSizer}/utils.ts +49 -49
- package/dist/v3/components/Barrage/BarrageItem.vue +137 -0
- package/dist/v3/components/Barrage/index.vue +202 -0
- package/dist/v3/{CircularButton → components/CircularButton}/index.vue +84 -84
- package/dist/v3/{CustomSwiper → components/CustomSwiper}/CustomSwiperItem.vue +49 -49
- package/dist/v3/{CustomSwiper → components/CustomSwiper}/index.vue +104 -104
- package/dist/v3/{DraggableContainer → components/DraggableContainer}/index.vue +1 -1
- package/dist/v3/{Empty → components/Empty}/index.vue +29 -29
- package/dist/v3/{FilterInput → components/FilterInput}/index.vue +1 -1
- package/dist/v3/{FilterSelect → components/FilterSelect}/index.vue +179 -179
- package/dist/v3/{JsonPretty → components/JsonPretty}/components/Brackets/index.vue +27 -27
- package/dist/v3/{JsonPretty → components/JsonPretty}/components/Carets/index.vue +59 -59
- package/dist/v3/{JsonPretty → components/JsonPretty}/components/CheckController/index.vue +136 -136
- package/dist/v3/{JsonPretty → components/JsonPretty}/components/TreeNode/index.vue +387 -381
- package/dist/v3/{JsonPretty → components/JsonPretty}/hooks/useClipboard.ts +21 -21
- package/dist/v3/{JsonPretty → components/JsonPretty}/hooks/useError.ts +21 -21
- package/dist/v3/{JsonPretty → components/JsonPretty}/index.vue +16 -13
- package/dist/v3/{JsonPretty → components/JsonPretty}/type.ts +127 -126
- package/dist/v3/{JsonPretty → components/JsonPretty}/utils/index.ts +169 -169
- package/dist/v3/{MovableContainer → components/MovableContainer}/index.vue +1 -1
- package/dist/v3/{Pick → components/Pick}/index.vue +322 -322
- package/dist/v3/{Tabs → components/Tabs}/index.vue +30 -4
- package/dist/v3/{Tag → components/Tag}/index.vue +113 -113
- package/dist/v3/{VirtualList → components/VirtualList}/AutoSize.vue +40 -40
- package/dist/v3/{VirtualList → components/VirtualList}/index.vue +416 -412
- package/dist/v3/hooks/useBluetooth/index.ts +561 -0
- package/dist/v3/hooks/useContainerStyle.ts +153 -153
- package/dist/v3/hooks/useNFC/index.ts +107 -107
- package/dist/v3/hooks/useNFC/typing.d.ts +396 -396
- package/dist/v3/hooks/useNFC/useNFCAndroid.ts +966 -966
- package/dist/v3/hooks/useNFC/useNFCMpWeiXin.ts +812 -812
- package/dist/v3/hooks/useNFC/utils.ts +754 -754
- package/dist/v3/hooks/useRequest/index.ts +586 -573
- package/dist/v3/hooks/useRequest/utils.ts +267 -267
- package/dist/v3/hooks/useScanCode/index.ts +206 -206
- package/dist/v3/hooks/useWebsocket/README.md +79 -0
- package/dist/v3/hooks/useWebsocket/index.ts +253 -0
- package/dist/v3/styles/theme.css +17 -10
- package/dist/v3/styles/theme.ts +12 -12
- package/package.json +59 -64
- package/dist/plugins/uniParseStock/index.d.ts +0 -10
- package/dist/plugins/uniParseStock/index.d.ts.map +0 -1
- /package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/index.css +0 -0
- /package/dist/v3/{SettingList → DevTool/components/SettingList}/index.css +0 -0
- /package/dist/v3/{Empty → components/Empty}/empty.png +0 -0
- /package/dist/v3/{VirtualList → components/VirtualList}/readme.md +0 -0
|
@@ -1,754 +1,754 @@
|
|
|
1
|
-
// 创建一个 ArrayBuffer 并查看其值
|
|
2
|
-
export function transformArrayBuffer(buffer: ArrayBuffer): NFC.TransformId {
|
|
3
|
-
if (!buffer) {
|
|
4
|
-
return {
|
|
5
|
-
byteArray: [],
|
|
6
|
-
hexArray: [],
|
|
7
|
-
hexString: '',
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// 将 ArrayBuffer 转为 Uint8Array(按字节读取)
|
|
12
|
-
const uint8Array = new Uint8Array(buffer);
|
|
13
|
-
|
|
14
|
-
// 方式1:转为普通数组
|
|
15
|
-
const byteArray = Array.from(uint8Array);
|
|
16
|
-
// console.log('ArrayBuffer 字节值(十进制):', byteArray);
|
|
17
|
-
|
|
18
|
-
// 方式2:转为十六进制(NFC 场景常用,因为 NFC 数据多以十六进制展示)
|
|
19
|
-
const hexArray = byteArray.map((byte) =>
|
|
20
|
-
byte.toString(16).padStart(2, '0').toUpperCase(),
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const hexString = hexArray.join(':');
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
byteArray,
|
|
27
|
-
hexArray,
|
|
28
|
-
|
|
29
|
-
hexString,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 将字节数组转换为字符串
|
|
35
|
-
*
|
|
36
|
-
* @export
|
|
37
|
-
* @param {number[]} bytes
|
|
38
|
-
* @return {*}
|
|
39
|
-
*/
|
|
40
|
-
export function byteToString(bytes: number[]) {
|
|
41
|
-
return bytes.map((byte) => String.fromCharCode(byte)).join('');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 将字符串转换为字节数组
|
|
46
|
-
*
|
|
47
|
-
* @export
|
|
48
|
-
* @param {string} str
|
|
49
|
-
* @return {*} {number[]}
|
|
50
|
-
*/
|
|
51
|
-
export function stringToBytes(str: string): number[] {
|
|
52
|
-
return Array.from(str).map((char) => char.charCodeAt(0));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function stringToArrayBuffer(str: string): ArrayBuffer {
|
|
56
|
-
const bytes = stringToBytes(str); // 使用现有工具函数
|
|
57
|
-
const uint8Array = new Uint8Array(bytes);
|
|
58
|
-
return uint8Array.buffer.slice(
|
|
59
|
-
uint8Array.byteOffset,
|
|
60
|
-
uint8Array.byteOffset + uint8Array.byteLength,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function arrayNumberToArrayBuffer(numbers: number[]): ArrayBuffer {
|
|
65
|
-
const buffer = new ArrayBuffer(numbers.length);
|
|
66
|
-
const view = new DataView(buffer);
|
|
67
|
-
numbers.forEach((number, index) => {
|
|
68
|
-
view.setUint8(index, number);
|
|
69
|
-
});
|
|
70
|
-
return buffer;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 可以添加解析函数来解析 ATQA 信息
|
|
74
|
-
export function parseAtqa(atqaBuffer: ArrayBuffer) {
|
|
75
|
-
const uint8Array = new Uint8Array(atqaBuffer);
|
|
76
|
-
if (uint8Array.length < 2) return null;
|
|
77
|
-
|
|
78
|
-
const byte1 = uint8Array[0];
|
|
79
|
-
const byte2 = uint8Array[1];
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
uidType: (byte1 >> 7) & 0x01, // UID 类型
|
|
83
|
-
storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
|
|
84
|
-
manufacturer: byte1 & 0x0f, // 制造商代码
|
|
85
|
-
cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
|
|
86
|
-
reserved: byte2 & 0x0f, // 保留位
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function parseAtqaAndroid(atqa: number[]) {
|
|
91
|
-
const byte1 = atqa[0];
|
|
92
|
-
const byte2 = atqa[1];
|
|
93
|
-
return {
|
|
94
|
-
uidType: (byte1 >> 7) & 0x01, // UID 类型
|
|
95
|
-
storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
|
|
96
|
-
manufacturer: byte1 & 0x0f, // 制造商代码
|
|
97
|
-
cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
|
|
98
|
-
reserved: byte2 & 0x0f, // 保留位
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 获取 SAK 类型
|
|
104
|
-
*
|
|
105
|
-
* @export
|
|
106
|
-
* @param {number} sak
|
|
107
|
-
* @return {*}
|
|
108
|
-
*/
|
|
109
|
-
export function getSakType(sak: number) {
|
|
110
|
-
switch (sak) {
|
|
111
|
-
case 0x00:
|
|
112
|
-
return '未指定类型';
|
|
113
|
-
case 0x08:
|
|
114
|
-
return 'Mifare Classic 1K';
|
|
115
|
-
case 0x09:
|
|
116
|
-
return 'Mifare Classic Mini';
|
|
117
|
-
case 0x10:
|
|
118
|
-
return 'Mifare Plus';
|
|
119
|
-
case 0x11:
|
|
120
|
-
return 'Mifare Classic 4K';
|
|
121
|
-
case 0x18:
|
|
122
|
-
return 'Mifare Ultralight / Ultralight C';
|
|
123
|
-
case 0x20:
|
|
124
|
-
return 'DESFire';
|
|
125
|
-
default:
|
|
126
|
-
return '未知';
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function getSakHex(sak: number) {
|
|
131
|
-
return '0x' + (sak < 16 ? '0' : '') + sak.toString(16).toUpperCase();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** 获取读写标准 */
|
|
135
|
-
export function getReadWriteStandard(nfcTypes: NFC.NfcType[]) {
|
|
136
|
-
return nfcTypes.map((nfcType) => {
|
|
137
|
-
switch (nfcType) {
|
|
138
|
-
case 'NFC-A':
|
|
139
|
-
case 'MIFARE Classic':
|
|
140
|
-
case 'MIFARE Ultralight':
|
|
141
|
-
return 'ISO 14443-3A';
|
|
142
|
-
case 'NFC-B':
|
|
143
|
-
return 'ISO 14443-3B';
|
|
144
|
-
case 'NFC-F':
|
|
145
|
-
return 'ISO 14443-3F';
|
|
146
|
-
case 'NFC-V':
|
|
147
|
-
return 'ISO 15693';
|
|
148
|
-
case 'Ndef':
|
|
149
|
-
case 'NDEF':
|
|
150
|
-
return 'NDEF';
|
|
151
|
-
case 'ISO-DEP':
|
|
152
|
-
return 'ISO 14443-4';
|
|
153
|
-
case 'Ndef Formatable':
|
|
154
|
-
return 'NDEF Formatable';
|
|
155
|
-
default:
|
|
156
|
-
return '未知';
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function getAndroidTechs(techs: string[]) {
|
|
162
|
-
return techs.map((tech) => {
|
|
163
|
-
switch (tech) {
|
|
164
|
-
case 'android.nfc.tech.IsoDep':
|
|
165
|
-
return 'ISO-DEP';
|
|
166
|
-
case 'android.nfc.tech.MifareClassic':
|
|
167
|
-
return 'MIFARE Classic';
|
|
168
|
-
case 'android.nfc.tech.MifareUltralight':
|
|
169
|
-
return 'MIFARE Ultralight';
|
|
170
|
-
case 'android.nfc.tech.NfcA':
|
|
171
|
-
return 'NFC-A';
|
|
172
|
-
case 'android.nfc.tech.NfcB':
|
|
173
|
-
return 'NFC-B';
|
|
174
|
-
case 'android.nfc.tech.NfcF':
|
|
175
|
-
return 'NFC-F';
|
|
176
|
-
case 'android.nfc.tech.NfcV':
|
|
177
|
-
return 'NFC-V';
|
|
178
|
-
case 'android.nfc.tech.Ndef':
|
|
179
|
-
return 'Ndef';
|
|
180
|
-
case 'android.nfc.tech.NdefFormatable':
|
|
181
|
-
return 'Ndef Formatable';
|
|
182
|
-
default:
|
|
183
|
-
return '';
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function getTnfDescription(tnf: string) {
|
|
189
|
-
switch (tnf) {
|
|
190
|
-
case '0':
|
|
191
|
-
return 'TNF_EMPTY';
|
|
192
|
-
case '1':
|
|
193
|
-
return 'TNF_WELL_KNOWN';
|
|
194
|
-
case '2':
|
|
195
|
-
return 'TNF_MIME_MEDIA';
|
|
196
|
-
case '3':
|
|
197
|
-
return 'TNF_ABSOLUTE_URI';
|
|
198
|
-
case '4':
|
|
199
|
-
return 'TNF_EXTERNAL_TYPE';
|
|
200
|
-
case '5':
|
|
201
|
-
return 'TNF_UNKNOWN';
|
|
202
|
-
case '6':
|
|
203
|
-
return 'TNF_UNCHANGED';
|
|
204
|
-
case '7':
|
|
205
|
-
return 'TNF_RESERVED';
|
|
206
|
-
default:
|
|
207
|
-
return '未知';
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* 获取URI记录的具体类型描述
|
|
213
|
-
* @param prefixCode - URI前缀代码(payload的第一个字节)
|
|
214
|
-
* @returns URI类型描述
|
|
215
|
-
*/
|
|
216
|
-
function getUriTypeDescription(prefixCode: number): string {
|
|
217
|
-
const uriPrefixes: { [key: number]: string } = {
|
|
218
|
-
0x00: '未指定(纯URI)',
|
|
219
|
-
0x01: 'http://www.', // http://www.
|
|
220
|
-
0x02: 'https://www.', // https://www.
|
|
221
|
-
0x03: 'http://', // http://
|
|
222
|
-
0x04: 'https://', // https://
|
|
223
|
-
0x05: 'tel:', // tel: (电话号码)
|
|
224
|
-
0x06: 'mailto:', // mailto: (邮箱地址)
|
|
225
|
-
0x07: 'ftp://anonymous:anonymous@', // ftp://anonymous:anonymous@
|
|
226
|
-
0x08: 'ftp://ftp.', // ftp://ftp.
|
|
227
|
-
0x09: 'ftps://', // ftps://
|
|
228
|
-
0x0a: 'sftp://', // sftp://
|
|
229
|
-
0x0b: 'smb://', // smb://
|
|
230
|
-
0x0c: 'nfs://', // nfs://
|
|
231
|
-
0x0d: 'ftp://', // ftp://
|
|
232
|
-
0x0e: 'dav://', // dav://
|
|
233
|
-
0x0f: 'news:', // news:
|
|
234
|
-
0x10: 'telnet://', // telnet://
|
|
235
|
-
0x11: 'imap:', // imap:
|
|
236
|
-
0x12: 'rtsp://', // rtsp://
|
|
237
|
-
0x13: 'urn:', // urn:
|
|
238
|
-
0x14: 'pop:', // pop:
|
|
239
|
-
0x15: 'sip:', // sip:
|
|
240
|
-
0x16: 'sips:', // sips:
|
|
241
|
-
0x17: 'tftp:', // tftp:
|
|
242
|
-
0x18: 'btspp://', // btspp:// (Bluetooth)
|
|
243
|
-
0x19: 'btl2cap://', // btl2cap:// (Bluetooth)
|
|
244
|
-
0x1a: 'btgoep://', // btgoep:// (Bluetooth)
|
|
245
|
-
0x1b: 'tcpobex://', // tcpobex://
|
|
246
|
-
0x1c: 'irdaobex://', // irdaobex://
|
|
247
|
-
0x1d: 'file://', // file://
|
|
248
|
-
0x1e: 'urn:epc:id:', // urn:epc:id:
|
|
249
|
-
0x1f: 'urn:epc:tag:', // urn:epc:tag:
|
|
250
|
-
0x20: 'urn:epc:pat:', // urn:epc:pat:
|
|
251
|
-
0x21: 'urn:epc:raw:', // urn:epc:raw:
|
|
252
|
-
0x22: 'urn:epc:', // urn:epc:
|
|
253
|
-
0x23: 'urn:nfc:', // urn:nfc:
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
return uriPrefixes[prefixCode] || '未知';
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** 定义 URI 前缀映射表 */
|
|
260
|
-
const uriPrefixMap: { [key: string]: number } = {
|
|
261
|
-
'': 0x00, // 未指定协议
|
|
262
|
-
'http://www.': 0x01,
|
|
263
|
-
'https://www.': 0x02,
|
|
264
|
-
'http://': 0x03,
|
|
265
|
-
'https://': 0x04,
|
|
266
|
-
'tel:': 0x05, // 电话号码
|
|
267
|
-
'mailto:': 0x06, // 邮箱地址
|
|
268
|
-
'ftp://anonymous:anonymous@': 0x07,
|
|
269
|
-
'ftp://ftp.': 0x08,
|
|
270
|
-
'ftps://': 0x09,
|
|
271
|
-
'sftp://': 0x0a,
|
|
272
|
-
'smb://': 0x0b,
|
|
273
|
-
'nfs://': 0x0c,
|
|
274
|
-
'ftp://': 0x0d,
|
|
275
|
-
'dav://': 0x0e,
|
|
276
|
-
'news:': 0x0f,
|
|
277
|
-
'telnet://': 0x10,
|
|
278
|
-
'imap:': 0x11,
|
|
279
|
-
'rtsp://': 0x12,
|
|
280
|
-
'urn:': 0x13,
|
|
281
|
-
'pop:': 0x14,
|
|
282
|
-
'sip:': 0x15,
|
|
283
|
-
'sips:': 0x16,
|
|
284
|
-
'tftp:': 0x17,
|
|
285
|
-
'btspp://': 0x18,
|
|
286
|
-
'btl2cap://': 0x19,
|
|
287
|
-
'btgoep://': 0x1a,
|
|
288
|
-
'tcpobex://': 0x1b,
|
|
289
|
-
'irdaobex://': 0x1c,
|
|
290
|
-
'file://': 0x1d,
|
|
291
|
-
'urn:epc:id:': 0x1e,
|
|
292
|
-
'urn:epc:tag:': 0x1f,
|
|
293
|
-
'urn:epc:pat:': 0x20,
|
|
294
|
-
'urn:epc:raw:': 0x21,
|
|
295
|
-
'urn:epc:': 0x22,
|
|
296
|
-
'urn:nfc:': 0x23,
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* 创建 URI 类型的 NDEF 记录
|
|
301
|
-
* 根据 NFC Forum URI Record Type Definition 规范
|
|
302
|
-
*
|
|
303
|
-
* @param uri 完整的 URI 字符串,如 "tel:18888888888", "https://www.example.com"
|
|
304
|
-
* @returns NDEF 记录对象
|
|
305
|
-
*/
|
|
306
|
-
export function createUriNdefRecord(uri: string): NFC.NdefRecord {
|
|
307
|
-
let prefix = '';
|
|
308
|
-
let suffix = uri;
|
|
309
|
-
|
|
310
|
-
// 查找匹配的前缀
|
|
311
|
-
for (const [protocol, _code] of Object.entries(uriPrefixMap)) {
|
|
312
|
-
if (uri.startsWith(protocol) && protocol.length > prefix.length) {
|
|
313
|
-
prefix = protocol;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// 提取 URI 后缀部分
|
|
318
|
-
if (prefix) {
|
|
319
|
-
suffix = uri.substring(prefix.length);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// 获取前缀代码
|
|
323
|
-
const prefixCode = uriPrefixMap[prefix] || 0x00;
|
|
324
|
-
|
|
325
|
-
// 构建 payload:前缀代码 + 剩余 URI
|
|
326
|
-
const payloadArray = [prefixCode];
|
|
327
|
-
if (suffix) {
|
|
328
|
-
// 将后缀字符串转换为字节数组
|
|
329
|
-
const suffixBytes = stringToBytes(suffix);
|
|
330
|
-
payloadArray.push(...suffixBytes);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return {
|
|
334
|
-
tnf: 1, // TNF_WELL_KNOWN
|
|
335
|
-
type: 'U', // URI 类型
|
|
336
|
-
payload: arrayNumberToArrayBuffer(payloadArray),
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function getTypeDescription(type: string, payload?: number[]) {
|
|
341
|
-
switch (type) {
|
|
342
|
-
case 'T':
|
|
343
|
-
return '文本记录';
|
|
344
|
-
case 'U':
|
|
345
|
-
// URI记录需要根据payload的第一个字节确定具体类型
|
|
346
|
-
if (payload && payload.length > 0) {
|
|
347
|
-
const uriPrefixCode = payload[0]; // 第一个字节是URI前缀代码
|
|
348
|
-
return getUriTypeDescription(uriPrefixCode);
|
|
349
|
-
}
|
|
350
|
-
return 'URI记录';
|
|
351
|
-
case 'Sp':
|
|
352
|
-
return 'Smart Poster记录';
|
|
353
|
-
case 'Sig':
|
|
354
|
-
return '签名记录';
|
|
355
|
-
case 'application/vnd.wfa.wsc':
|
|
356
|
-
return 'Wi-Fi Simple Configuration记录';
|
|
357
|
-
case 'android.com:pkg':
|
|
358
|
-
return 'Android 包名记录';
|
|
359
|
-
case 'text/vcard':
|
|
360
|
-
return 'VCard 记录';
|
|
361
|
-
default:
|
|
362
|
-
// 如果是 MIME 类型格式,可以返回更具体的描述
|
|
363
|
-
if (type.startsWith('application/')) {
|
|
364
|
-
return `应用数据类型: ${type}`;
|
|
365
|
-
} else if (type.startsWith('text/')) {
|
|
366
|
-
return `文本数据类型: ${type}`;
|
|
367
|
-
} else if (
|
|
368
|
-
type.startsWith('image/') ||
|
|
369
|
-
type.startsWith('video/') ||
|
|
370
|
-
type.startsWith('audio/')
|
|
371
|
-
) {
|
|
372
|
-
return `媒体数据类型: ${type}`;
|
|
373
|
-
}
|
|
374
|
-
return '未知';
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* 通用 TLV 解析配置项接口
|
|
380
|
-
* 支持自定义各类 TLV 解析规则,适配不同格式数据
|
|
381
|
-
*/
|
|
382
|
-
interface TLVParseConfig {
|
|
383
|
-
// 合法 TLV 的 Type 高字节标识(默认:0x10,对应 NFC WiFi 数据)
|
|
384
|
-
legalTypeHigh?: number;
|
|
385
|
-
// 需要跳过的无效 TLV 类型(低字节)列表
|
|
386
|
-
skipTlvTypes?: number[];
|
|
387
|
-
// 长度解析策略(支持自定义,适配标准大端/低字节有效/高字节有效等)
|
|
388
|
-
lengthParseStrategy: (lengthHigh: number, lengthLow: number) => number;
|
|
389
|
-
// 目标 TLV 类型映射(键:TLV 低字节类型,值:字段名称 + 数据转换函数)
|
|
390
|
-
targetTlvMap: Record<
|
|
391
|
-
number,
|
|
392
|
-
{
|
|
393
|
-
fieldName: string;
|
|
394
|
-
converter: (valueBytes: number[]) => any;
|
|
395
|
-
}
|
|
396
|
-
>;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* 通用 TLV 解析工具函数
|
|
401
|
-
* 可适配 NFC 门禁卡、公交卡、WiFi 数据等各类 TLV 格式数据
|
|
402
|
-
* @param tlvBytes 原始 TLV 字节数组
|
|
403
|
-
* @param config 自定义解析配置项
|
|
404
|
-
* @returns 解析后的结果对象(键:配置中定义的 fieldName,值:转换后的数据)
|
|
405
|
-
*/
|
|
406
|
-
export function parseUniversalTLV(
|
|
407
|
-
tlvBytes: number[],
|
|
408
|
-
config: TLVParseConfig,
|
|
409
|
-
): Record<string, any> {
|
|
410
|
-
// 配置项默认值填充
|
|
411
|
-
const finalConfig = {
|
|
412
|
-
legalTypeHigh: 0x10,
|
|
413
|
-
skipTlvTypes: [],
|
|
414
|
-
...config,
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// 存储解析结果
|
|
418
|
-
const parseResult: Record<string, any> = {
|
|
419
|
-
raw: tlvBytes,
|
|
420
|
-
};
|
|
421
|
-
const byteLength = tlvBytes.length;
|
|
422
|
-
let index = 0;
|
|
423
|
-
|
|
424
|
-
// 循环解析 TLV 数据(核心:按配置项灵活解析)
|
|
425
|
-
while (index < byteLength) {
|
|
426
|
-
// 1. 校验是否有足够字节读取 Type(2 字节)
|
|
427
|
-
if (index + 1 >= byteLength) {
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const typeHigh = tlvBytes[index];
|
|
432
|
-
const typeLow = tlvBytes[index + 1];
|
|
433
|
-
const currentType = typeLow;
|
|
434
|
-
|
|
435
|
-
// 2. 过滤非法 TLV(非配置指定的高字节标识)
|
|
436
|
-
if (typeHigh !== finalConfig.legalTypeHigh) {
|
|
437
|
-
index++;
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// 3. 跳过配置中指定的无效 TLV(直接跳过头部 4 字节:Type2 + Length2)
|
|
442
|
-
if (finalConfig.skipTlvTypes?.includes(currentType)) {
|
|
443
|
-
index += 4;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// 4. 步进 Type 字节(2 字节),准备读取 Length
|
|
448
|
-
index += 2;
|
|
449
|
-
if (index + 1 >= byteLength) {
|
|
450
|
-
break;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// 5. 按自定义策略解析 Length(灵活适配不同格式)
|
|
454
|
-
const lengthHigh = tlvBytes[index];
|
|
455
|
-
const lengthLow = tlvBytes[index + 1];
|
|
456
|
-
const currentLength = finalConfig.lengthParseStrategy(
|
|
457
|
-
lengthHigh,
|
|
458
|
-
lengthLow,
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
// 6. 步进 Length 字节(2 字节),准备读取 Value
|
|
462
|
-
index += 2;
|
|
463
|
-
if (index + currentLength > byteLength) {
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// 7. 读取 Value 部分(按自定义长度截取)
|
|
468
|
-
const currentValue = tlvBytes.slice(index, index + currentLength);
|
|
469
|
-
|
|
470
|
-
// 8. 强制步进 Value 字节(避免索引卡死,确保遍历完整性)
|
|
471
|
-
index += currentLength;
|
|
472
|
-
|
|
473
|
-
// 9. 按自定义映射转换目标 TLV 数据
|
|
474
|
-
const targetTlvConfig = finalConfig.targetTlvMap[currentType];
|
|
475
|
-
if (targetTlvConfig) {
|
|
476
|
-
try {
|
|
477
|
-
const convertedData = targetTlvConfig.converter(currentValue);
|
|
478
|
-
parseResult[targetTlvConfig.fieldName] = convertedData;
|
|
479
|
-
} catch (_e) {
|
|
480
|
-
parseResult[targetTlvConfig.fieldName] = null;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return parseResult;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* 辅助工具:修正 authType 转换逻辑,适配 32(0x20)等新值
|
|
490
|
-
*/
|
|
491
|
-
const TLVConverterTools = {
|
|
492
|
-
// 1. 字节数组 → UTF-8 字符串(保持原有)
|
|
493
|
-
bytesToUtf8String: (bytes: number[]): string => {
|
|
494
|
-
try {
|
|
495
|
-
return new TextDecoder('utf-8', { fatal: false }).decode(
|
|
496
|
-
new Uint8Array(bytes),
|
|
497
|
-
);
|
|
498
|
-
} catch (_e) {
|
|
499
|
-
return bytes.map((byte) => String.fromCharCode(byte)).join('');
|
|
500
|
-
}
|
|
501
|
-
},
|
|
502
|
-
|
|
503
|
-
// 2. 字节数组 → WIFI 认证类型(核心修正:添加 32(0x20)的映射)
|
|
504
|
-
bytesToWifiAuthType: (bytes: number[]): string => {
|
|
505
|
-
// 适配当前字节数组的 [0, 32],取有效数据(低字节优先,与格式一致)
|
|
506
|
-
const authValue = bytes.length >= 2 ? bytes[1] || bytes[0] : bytes[0];
|
|
507
|
-
console.log(
|
|
508
|
-
`[authType 解析] 原始标识值:${authValue}(十六进制:0x${authValue.toString(16).padStart(2, '0')}`,
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
switch (authValue) {
|
|
512
|
-
case 1:
|
|
513
|
-
return 'OPEN (无认证)';
|
|
514
|
-
case 2:
|
|
515
|
-
case 34: // 0x22:原有 WPA2-Personal
|
|
516
|
-
return 'WPA2-Personal';
|
|
517
|
-
case 32: // 0x20:新增,适配当前字节数组的 authType 值
|
|
518
|
-
return 'WPA-Personal (兼容 WPA2)';
|
|
519
|
-
case 3:
|
|
520
|
-
return 'WPA-Personal';
|
|
521
|
-
default:
|
|
522
|
-
return `未知认证类型(0x${authValue.toString(16).padStart(4, '0')}`;
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
|
|
526
|
-
// 3. 其他工具函数(保持原有,可选)
|
|
527
|
-
bytesToHexString: (bytes: number[], separator = ''): string => {
|
|
528
|
-
return bytes
|
|
529
|
-
.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase())
|
|
530
|
-
.join(separator);
|
|
531
|
-
},
|
|
532
|
-
bytesToDecimal: (bytes: number[], isBigEndian = true): number => {
|
|
533
|
-
let result = 0;
|
|
534
|
-
if (isBigEndian) {
|
|
535
|
-
bytes.forEach((byte) => {
|
|
536
|
-
result = (result << 8) | byte;
|
|
537
|
-
});
|
|
538
|
-
} else {
|
|
539
|
-
bytes.reverse().forEach((byte) => {
|
|
540
|
-
result = (result << 8) | byte;
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
return result;
|
|
544
|
-
},
|
|
545
|
-
bytesToWifiEncryptionType: (bytes: number[]): string => {
|
|
546
|
-
const isAllFf = bytes.every((byte) => byte === 255);
|
|
547
|
-
if (isAllFf) {
|
|
548
|
-
return 'AES/TKIP (WPA2 兼容)';
|
|
549
|
-
}
|
|
550
|
-
const encryptValue = TLVConverterTools.bytesToDecimal(bytes);
|
|
551
|
-
return encryptValue === 0
|
|
552
|
-
? 'NONE (无加密)'
|
|
553
|
-
: `WEP(0x${encryptValue.toString(16).padStart(4, '0')}`;
|
|
554
|
-
},
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
// 定义 WiFi 数据的自定义解析配置
|
|
558
|
-
export const wifiTLVConfig: TLVParseConfig = {
|
|
559
|
-
legalTypeHigh: 0x10, // 合法 TLV 高字节为 16
|
|
560
|
-
skipTlvTypes: [14], // 跳过无效 TLV 类型 14
|
|
561
|
-
// 长度解析策略:低字节有效(适配非标准 WiFi 数据)
|
|
562
|
-
lengthParseStrategy: (lengthHigh, lengthLow) => lengthLow,
|
|
563
|
-
// 目标 TLV 映射(按需扩展)
|
|
564
|
-
targetTlvMap: {
|
|
565
|
-
69: {
|
|
566
|
-
// SSID 类型
|
|
567
|
-
fieldName: 'ssid',
|
|
568
|
-
converter: TLVConverterTools.bytesToUtf8String,
|
|
569
|
-
},
|
|
570
|
-
39: {
|
|
571
|
-
// 密码类型
|
|
572
|
-
fieldName: 'password',
|
|
573
|
-
converter: TLVConverterTools.bytesToUtf8String,
|
|
574
|
-
},
|
|
575
|
-
3: {
|
|
576
|
-
// 认证类型
|
|
577
|
-
fieldName: 'authType',
|
|
578
|
-
converter: TLVConverterTools.bytesToWifiAuthType,
|
|
579
|
-
},
|
|
580
|
-
32: {
|
|
581
|
-
// 加密类型
|
|
582
|
-
fieldName: 'encryptionType',
|
|
583
|
-
converter: TLVConverterTools.bytesToWifiEncryptionType,
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
// WSC 配置接口
|
|
589
|
-
|
|
590
|
-
// 生成 WSC 数据 (Android/Windows 更兼容的格式)
|
|
591
|
-
export function generateWSCBytesForAndroid(config: NFC.WSCConfig): number[] {
|
|
592
|
-
const bytes: number[] = [];
|
|
593
|
-
|
|
594
|
-
// 这是 Android 和 Windows 都接受的简化格式
|
|
595
|
-
// 参考: Wi-Fi Alliance WSC Specification 2.0
|
|
596
|
-
|
|
597
|
-
// 1. Credential 属性 (0x100e)
|
|
598
|
-
// 这是一个容器属性,包含其他属性
|
|
599
|
-
|
|
600
|
-
// 先收集所有属性
|
|
601
|
-
const credentialAttrs: number[] = [];
|
|
602
|
-
|
|
603
|
-
// SSID
|
|
604
|
-
const ssidBytes = Array.from(config.ssid, (c) => c.charCodeAt(0));
|
|
605
|
-
credentialAttrs.push(0x10, 0x45); // SSID
|
|
606
|
-
credentialAttrs.push(0x00, ssidBytes.length);
|
|
607
|
-
credentialAttrs.push(...ssidBytes);
|
|
608
|
-
|
|
609
|
-
// 认证类型
|
|
610
|
-
let authValue = 0x0020; // 默认 WPA2-Personal
|
|
611
|
-
if (config.authType === 'OPEN') authValue = 0x0001;
|
|
612
|
-
if (config.authType === 'WPA') authValue = 0x0022;
|
|
613
|
-
|
|
614
|
-
credentialAttrs.push(0x10, 0x03); // Authentication Type
|
|
615
|
-
credentialAttrs.push(0x00, 0x02);
|
|
616
|
-
credentialAttrs.push((authValue >> 8) & 0xff, authValue & 0xff);
|
|
617
|
-
|
|
618
|
-
// 加密类型
|
|
619
|
-
let encValue = 0x0008; // 默认 AES
|
|
620
|
-
if (config.encryptionType === 'NONE') encValue = 0x0001;
|
|
621
|
-
if (config.encryptionType === 'TKIP') encValue = 0x0004;
|
|
622
|
-
if (config.encryptionType === 'WEP') encValue = 0x0008;
|
|
623
|
-
|
|
624
|
-
credentialAttrs.push(0x10, 0x0f); // Encryption Type
|
|
625
|
-
credentialAttrs.push(0x00, 0x02);
|
|
626
|
-
credentialAttrs.push((encValue >> 8) & 0xff, encValue & 0xff);
|
|
627
|
-
|
|
628
|
-
// 网络密钥
|
|
629
|
-
if (config.authType !== 'OPEN') {
|
|
630
|
-
const keyBytes = Array.from(config.password, (c) => c.charCodeAt(0));
|
|
631
|
-
credentialAttrs.push(0x10, 0x27); // Network Key
|
|
632
|
-
credentialAttrs.push(0x00, keyBytes.length);
|
|
633
|
-
credentialAttrs.push(...keyBytes);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// 网络密钥索引 (WEP 需要)
|
|
637
|
-
if (config.encryptionType === 'WEP') {
|
|
638
|
-
const idx = config.networkKeyIndex || 1;
|
|
639
|
-
credentialAttrs.push(0x10, 0x28); // Network Key Index
|
|
640
|
-
credentialAttrs.push(0x00, 0x01);
|
|
641
|
-
credentialAttrs.push(idx);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// 将 Credential 属性添加到主字节数组
|
|
645
|
-
bytes.push(0x10, 0x0e); // Credential
|
|
646
|
-
bytes.push(0x00, credentialAttrs.length);
|
|
647
|
-
bytes.push(...credentialAttrs);
|
|
648
|
-
|
|
649
|
-
return bytes;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* vCard (text/vcard) 数据解析工具
|
|
654
|
-
* 适配 vCard 3.0 格式,支持提取核心联系人字段,具备容错性
|
|
655
|
-
*/
|
|
656
|
-
|
|
657
|
-
const vcardFieldMap: Record<string, string> = {
|
|
658
|
-
FN: 'fullName',
|
|
659
|
-
VERSION: 'version',
|
|
660
|
-
ORG: 'organization',
|
|
661
|
-
TEL: 'telephone',
|
|
662
|
-
ADR: 'address',
|
|
663
|
-
URL: 'url',
|
|
664
|
-
};
|
|
665
|
-
const reverseVcardFieldMap: Record<string, string> = Object.entries(
|
|
666
|
-
vcardFieldMap,
|
|
667
|
-
).reduce(
|
|
668
|
-
(acc, [key, value]) => {
|
|
669
|
-
acc[value] = key;
|
|
670
|
-
return acc;
|
|
671
|
-
},
|
|
672
|
-
{} as Record<string, string>,
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* 解析 vCard 格式的 payload 字符串,提取联系人信息
|
|
677
|
-
* @param vcardPayload NFC 读取到的 vCard 原始字符串(含 \n 分隔符)
|
|
678
|
-
* BEGIN:VCARD\nVERSION:3.0\nFN:any\nORG:any\nTEL:18888888888\nADR:杭州\nURL:www.baidu.com\nEND:VCARD\n
|
|
679
|
-
* @returns 结构化的联系人信息对象
|
|
680
|
-
*/
|
|
681
|
-
export function parseVCardPayload(vcardPayload: string): NFC.VCardContactInfo {
|
|
682
|
-
// 初始化默认结果
|
|
683
|
-
const contactInfo: NFC.VCardContactInfo = {
|
|
684
|
-
fullName: '',
|
|
685
|
-
version: '',
|
|
686
|
-
organization: '',
|
|
687
|
-
telephone: '',
|
|
688
|
-
address: '',
|
|
689
|
-
url: '',
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
// 步骤1:预处理 payload(去除首尾空白、处理不同分隔符 \n/\r\n/\r,拆分行为数组)
|
|
693
|
-
const processedPayload = vcardPayload
|
|
694
|
-
.trim()
|
|
695
|
-
.replace(/\r\n/g, '\n')
|
|
696
|
-
.replace(/\r/g, '\n');
|
|
697
|
-
const vcardLines = processedPayload
|
|
698
|
-
.split('\n')
|
|
699
|
-
.filter((line) => line.trim() !== '');
|
|
700
|
-
|
|
701
|
-
// 步骤2:验证是否为合法 vCard 格式
|
|
702
|
-
const isLegalVCard =
|
|
703
|
-
vcardLines.some((line) => line.startsWith('BEGIN:VCARD')) &&
|
|
704
|
-
vcardLines.some((line) => line.startsWith('END:VCARD'));
|
|
705
|
-
if (!isLegalVCard) {
|
|
706
|
-
console.warn(
|
|
707
|
-
'[vCard 解析] 无效的 vCard 格式,缺少 BEGIN:VCARD 或 END:VCARD 标记',
|
|
708
|
-
);
|
|
709
|
-
return contactInfo;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// 步骤3:遍历每行,解析关键字段(KEY:VALUE 格式)
|
|
713
|
-
|
|
714
|
-
for (const line of vcardLines) {
|
|
715
|
-
// 拆分 KEY 和 VALUE(按第一个冒号分割,避免 VALUE 中包含冒号的情况)
|
|
716
|
-
const colonIndex = line.indexOf(':');
|
|
717
|
-
if (colonIndex === -1) continue;
|
|
718
|
-
|
|
719
|
-
const fieldKey = line.substring(0, colonIndex).trim().toUpperCase();
|
|
720
|
-
const fieldValue = line.substring(colonIndex + 1).trim();
|
|
721
|
-
|
|
722
|
-
// 映射到结构化对象字段
|
|
723
|
-
const targetField = vcardFieldMap[fieldKey];
|
|
724
|
-
if (targetField) {
|
|
725
|
-
contactInfo[targetField] = fieldValue;
|
|
726
|
-
} else {
|
|
727
|
-
// 支持扩展字段(直接挂载到对象上)
|
|
728
|
-
contactInfo[fieldKey.toLowerCase()] = fieldValue;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return contactInfo;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* 格式化 vCard 联系人信息
|
|
737
|
-
*
|
|
738
|
-
* @export
|
|
739
|
-
* @param {NFC.VCardContactInfo} cardInfo
|
|
740
|
-
* @return {*}
|
|
741
|
-
*/
|
|
742
|
-
export function formatCardInfo(cardInfo: NFC.VCardContactInfo) {
|
|
743
|
-
const list = Object.keys(cardInfo)
|
|
744
|
-
.filter((key) => {
|
|
745
|
-
return cardInfo[key];
|
|
746
|
-
})
|
|
747
|
-
.map((key) => {
|
|
748
|
-
return `${reverseVcardFieldMap[key]}:${cardInfo[key]}`;
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
list.unshift('BEGIN:VCARD');
|
|
752
|
-
list.push('END:VCARD');
|
|
753
|
-
return list.join('\n');
|
|
754
|
-
}
|
|
1
|
+
// 创建一个 ArrayBuffer 并查看其值
|
|
2
|
+
export function transformArrayBuffer(buffer: ArrayBuffer): NFC.TransformId {
|
|
3
|
+
if (!buffer) {
|
|
4
|
+
return {
|
|
5
|
+
byteArray: [],
|
|
6
|
+
hexArray: [],
|
|
7
|
+
hexString: '',
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 将 ArrayBuffer 转为 Uint8Array(按字节读取)
|
|
12
|
+
const uint8Array = new Uint8Array(buffer);
|
|
13
|
+
|
|
14
|
+
// 方式1:转为普通数组
|
|
15
|
+
const byteArray = Array.from(uint8Array);
|
|
16
|
+
// console.log('ArrayBuffer 字节值(十进制):', byteArray);
|
|
17
|
+
|
|
18
|
+
// 方式2:转为十六进制(NFC 场景常用,因为 NFC 数据多以十六进制展示)
|
|
19
|
+
const hexArray = byteArray.map((byte) =>
|
|
20
|
+
byte.toString(16).padStart(2, '0').toUpperCase(),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const hexString = hexArray.join(':');
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
byteArray,
|
|
27
|
+
hexArray,
|
|
28
|
+
|
|
29
|
+
hexString,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 将字节数组转换为字符串
|
|
35
|
+
*
|
|
36
|
+
* @export
|
|
37
|
+
* @param {number[]} bytes
|
|
38
|
+
* @return {*}
|
|
39
|
+
*/
|
|
40
|
+
export function byteToString(bytes: number[]) {
|
|
41
|
+
return bytes.map((byte) => String.fromCharCode(byte)).join('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 将字符串转换为字节数组
|
|
46
|
+
*
|
|
47
|
+
* @export
|
|
48
|
+
* @param {string} str
|
|
49
|
+
* @return {*} {number[]}
|
|
50
|
+
*/
|
|
51
|
+
export function stringToBytes(str: string): number[] {
|
|
52
|
+
return Array.from(str).map((char) => char.charCodeAt(0));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function stringToArrayBuffer(str: string): ArrayBuffer {
|
|
56
|
+
const bytes = stringToBytes(str); // 使用现有工具函数
|
|
57
|
+
const uint8Array = new Uint8Array(bytes);
|
|
58
|
+
return uint8Array.buffer.slice(
|
|
59
|
+
uint8Array.byteOffset,
|
|
60
|
+
uint8Array.byteOffset + uint8Array.byteLength,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function arrayNumberToArrayBuffer(numbers: number[]): ArrayBuffer {
|
|
65
|
+
const buffer = new ArrayBuffer(numbers.length);
|
|
66
|
+
const view = new DataView(buffer);
|
|
67
|
+
numbers.forEach((number, index) => {
|
|
68
|
+
view.setUint8(index, number);
|
|
69
|
+
});
|
|
70
|
+
return buffer;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 可以添加解析函数来解析 ATQA 信息
|
|
74
|
+
export function parseAtqa(atqaBuffer: ArrayBuffer) {
|
|
75
|
+
const uint8Array = new Uint8Array(atqaBuffer);
|
|
76
|
+
if (uint8Array.length < 2) return null;
|
|
77
|
+
|
|
78
|
+
const byte1 = uint8Array[0];
|
|
79
|
+
const byte2 = uint8Array[1];
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
uidType: (byte1 >> 7) & 0x01, // UID 类型
|
|
83
|
+
storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
|
|
84
|
+
manufacturer: byte1 & 0x0f, // 制造商代码
|
|
85
|
+
cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
|
|
86
|
+
reserved: byte2 & 0x0f, // 保留位
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseAtqaAndroid(atqa: number[]) {
|
|
91
|
+
const byte1 = atqa[0];
|
|
92
|
+
const byte2 = atqa[1];
|
|
93
|
+
return {
|
|
94
|
+
uidType: (byte1 >> 7) & 0x01, // UID 类型
|
|
95
|
+
storageSize: (byte1 >> 4) & 0x07, // 存储大小编码
|
|
96
|
+
manufacturer: byte1 & 0x0f, // 制造商代码
|
|
97
|
+
cardFeatures: (byte2 >> 4) & 0x0f, // 功能特性
|
|
98
|
+
reserved: byte2 & 0x0f, // 保留位
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 获取 SAK 类型
|
|
104
|
+
*
|
|
105
|
+
* @export
|
|
106
|
+
* @param {number} sak
|
|
107
|
+
* @return {*}
|
|
108
|
+
*/
|
|
109
|
+
export function getSakType(sak: number) {
|
|
110
|
+
switch (sak) {
|
|
111
|
+
case 0x00:
|
|
112
|
+
return '未指定类型';
|
|
113
|
+
case 0x08:
|
|
114
|
+
return 'Mifare Classic 1K';
|
|
115
|
+
case 0x09:
|
|
116
|
+
return 'Mifare Classic Mini';
|
|
117
|
+
case 0x10:
|
|
118
|
+
return 'Mifare Plus';
|
|
119
|
+
case 0x11:
|
|
120
|
+
return 'Mifare Classic 4K';
|
|
121
|
+
case 0x18:
|
|
122
|
+
return 'Mifare Ultralight / Ultralight C';
|
|
123
|
+
case 0x20:
|
|
124
|
+
return 'DESFire';
|
|
125
|
+
default:
|
|
126
|
+
return '未知';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getSakHex(sak: number) {
|
|
131
|
+
return '0x' + (sak < 16 ? '0' : '') + sak.toString(16).toUpperCase();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 获取读写标准 */
|
|
135
|
+
export function getReadWriteStandard(nfcTypes: NFC.NfcType[]) {
|
|
136
|
+
return nfcTypes.map((nfcType) => {
|
|
137
|
+
switch (nfcType) {
|
|
138
|
+
case 'NFC-A':
|
|
139
|
+
case 'MIFARE Classic':
|
|
140
|
+
case 'MIFARE Ultralight':
|
|
141
|
+
return 'ISO 14443-3A';
|
|
142
|
+
case 'NFC-B':
|
|
143
|
+
return 'ISO 14443-3B';
|
|
144
|
+
case 'NFC-F':
|
|
145
|
+
return 'ISO 14443-3F';
|
|
146
|
+
case 'NFC-V':
|
|
147
|
+
return 'ISO 15693';
|
|
148
|
+
case 'Ndef':
|
|
149
|
+
case 'NDEF':
|
|
150
|
+
return 'NDEF';
|
|
151
|
+
case 'ISO-DEP':
|
|
152
|
+
return 'ISO 14443-4';
|
|
153
|
+
case 'Ndef Formatable':
|
|
154
|
+
return 'NDEF Formatable';
|
|
155
|
+
default:
|
|
156
|
+
return '未知';
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getAndroidTechs(techs: string[]) {
|
|
162
|
+
return techs.map((tech) => {
|
|
163
|
+
switch (tech) {
|
|
164
|
+
case 'android.nfc.tech.IsoDep':
|
|
165
|
+
return 'ISO-DEP';
|
|
166
|
+
case 'android.nfc.tech.MifareClassic':
|
|
167
|
+
return 'MIFARE Classic';
|
|
168
|
+
case 'android.nfc.tech.MifareUltralight':
|
|
169
|
+
return 'MIFARE Ultralight';
|
|
170
|
+
case 'android.nfc.tech.NfcA':
|
|
171
|
+
return 'NFC-A';
|
|
172
|
+
case 'android.nfc.tech.NfcB':
|
|
173
|
+
return 'NFC-B';
|
|
174
|
+
case 'android.nfc.tech.NfcF':
|
|
175
|
+
return 'NFC-F';
|
|
176
|
+
case 'android.nfc.tech.NfcV':
|
|
177
|
+
return 'NFC-V';
|
|
178
|
+
case 'android.nfc.tech.Ndef':
|
|
179
|
+
return 'Ndef';
|
|
180
|
+
case 'android.nfc.tech.NdefFormatable':
|
|
181
|
+
return 'Ndef Formatable';
|
|
182
|
+
default:
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function getTnfDescription(tnf: string) {
|
|
189
|
+
switch (tnf) {
|
|
190
|
+
case '0':
|
|
191
|
+
return 'TNF_EMPTY';
|
|
192
|
+
case '1':
|
|
193
|
+
return 'TNF_WELL_KNOWN';
|
|
194
|
+
case '2':
|
|
195
|
+
return 'TNF_MIME_MEDIA';
|
|
196
|
+
case '3':
|
|
197
|
+
return 'TNF_ABSOLUTE_URI';
|
|
198
|
+
case '4':
|
|
199
|
+
return 'TNF_EXTERNAL_TYPE';
|
|
200
|
+
case '5':
|
|
201
|
+
return 'TNF_UNKNOWN';
|
|
202
|
+
case '6':
|
|
203
|
+
return 'TNF_UNCHANGED';
|
|
204
|
+
case '7':
|
|
205
|
+
return 'TNF_RESERVED';
|
|
206
|
+
default:
|
|
207
|
+
return '未知';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 获取URI记录的具体类型描述
|
|
213
|
+
* @param prefixCode - URI前缀代码(payload的第一个字节)
|
|
214
|
+
* @returns URI类型描述
|
|
215
|
+
*/
|
|
216
|
+
function getUriTypeDescription(prefixCode: number): string {
|
|
217
|
+
const uriPrefixes: { [key: number]: string } = {
|
|
218
|
+
0x00: '未指定(纯URI)',
|
|
219
|
+
0x01: 'http://www.', // http://www.
|
|
220
|
+
0x02: 'https://www.', // https://www.
|
|
221
|
+
0x03: 'http://', // http://
|
|
222
|
+
0x04: 'https://', // https://
|
|
223
|
+
0x05: 'tel:', // tel: (电话号码)
|
|
224
|
+
0x06: 'mailto:', // mailto: (邮箱地址)
|
|
225
|
+
0x07: 'ftp://anonymous:anonymous@', // ftp://anonymous:anonymous@
|
|
226
|
+
0x08: 'ftp://ftp.', // ftp://ftp.
|
|
227
|
+
0x09: 'ftps://', // ftps://
|
|
228
|
+
0x0a: 'sftp://', // sftp://
|
|
229
|
+
0x0b: 'smb://', // smb://
|
|
230
|
+
0x0c: 'nfs://', // nfs://
|
|
231
|
+
0x0d: 'ftp://', // ftp://
|
|
232
|
+
0x0e: 'dav://', // dav://
|
|
233
|
+
0x0f: 'news:', // news:
|
|
234
|
+
0x10: 'telnet://', // telnet://
|
|
235
|
+
0x11: 'imap:', // imap:
|
|
236
|
+
0x12: 'rtsp://', // rtsp://
|
|
237
|
+
0x13: 'urn:', // urn:
|
|
238
|
+
0x14: 'pop:', // pop:
|
|
239
|
+
0x15: 'sip:', // sip:
|
|
240
|
+
0x16: 'sips:', // sips:
|
|
241
|
+
0x17: 'tftp:', // tftp:
|
|
242
|
+
0x18: 'btspp://', // btspp:// (Bluetooth)
|
|
243
|
+
0x19: 'btl2cap://', // btl2cap:// (Bluetooth)
|
|
244
|
+
0x1a: 'btgoep://', // btgoep:// (Bluetooth)
|
|
245
|
+
0x1b: 'tcpobex://', // tcpobex://
|
|
246
|
+
0x1c: 'irdaobex://', // irdaobex://
|
|
247
|
+
0x1d: 'file://', // file://
|
|
248
|
+
0x1e: 'urn:epc:id:', // urn:epc:id:
|
|
249
|
+
0x1f: 'urn:epc:tag:', // urn:epc:tag:
|
|
250
|
+
0x20: 'urn:epc:pat:', // urn:epc:pat:
|
|
251
|
+
0x21: 'urn:epc:raw:', // urn:epc:raw:
|
|
252
|
+
0x22: 'urn:epc:', // urn:epc:
|
|
253
|
+
0x23: 'urn:nfc:', // urn:nfc:
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return uriPrefixes[prefixCode] || '未知';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 定义 URI 前缀映射表 */
|
|
260
|
+
const uriPrefixMap: { [key: string]: number } = {
|
|
261
|
+
'': 0x00, // 未指定协议
|
|
262
|
+
'http://www.': 0x01,
|
|
263
|
+
'https://www.': 0x02,
|
|
264
|
+
'http://': 0x03,
|
|
265
|
+
'https://': 0x04,
|
|
266
|
+
'tel:': 0x05, // 电话号码
|
|
267
|
+
'mailto:': 0x06, // 邮箱地址
|
|
268
|
+
'ftp://anonymous:anonymous@': 0x07,
|
|
269
|
+
'ftp://ftp.': 0x08,
|
|
270
|
+
'ftps://': 0x09,
|
|
271
|
+
'sftp://': 0x0a,
|
|
272
|
+
'smb://': 0x0b,
|
|
273
|
+
'nfs://': 0x0c,
|
|
274
|
+
'ftp://': 0x0d,
|
|
275
|
+
'dav://': 0x0e,
|
|
276
|
+
'news:': 0x0f,
|
|
277
|
+
'telnet://': 0x10,
|
|
278
|
+
'imap:': 0x11,
|
|
279
|
+
'rtsp://': 0x12,
|
|
280
|
+
'urn:': 0x13,
|
|
281
|
+
'pop:': 0x14,
|
|
282
|
+
'sip:': 0x15,
|
|
283
|
+
'sips:': 0x16,
|
|
284
|
+
'tftp:': 0x17,
|
|
285
|
+
'btspp://': 0x18,
|
|
286
|
+
'btl2cap://': 0x19,
|
|
287
|
+
'btgoep://': 0x1a,
|
|
288
|
+
'tcpobex://': 0x1b,
|
|
289
|
+
'irdaobex://': 0x1c,
|
|
290
|
+
'file://': 0x1d,
|
|
291
|
+
'urn:epc:id:': 0x1e,
|
|
292
|
+
'urn:epc:tag:': 0x1f,
|
|
293
|
+
'urn:epc:pat:': 0x20,
|
|
294
|
+
'urn:epc:raw:': 0x21,
|
|
295
|
+
'urn:epc:': 0x22,
|
|
296
|
+
'urn:nfc:': 0x23,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 创建 URI 类型的 NDEF 记录
|
|
301
|
+
* 根据 NFC Forum URI Record Type Definition 规范
|
|
302
|
+
*
|
|
303
|
+
* @param uri 完整的 URI 字符串,如 "tel:18888888888", "https://www.example.com"
|
|
304
|
+
* @returns NDEF 记录对象
|
|
305
|
+
*/
|
|
306
|
+
export function createUriNdefRecord(uri: string): NFC.NdefRecord {
|
|
307
|
+
let prefix = '';
|
|
308
|
+
let suffix = uri;
|
|
309
|
+
|
|
310
|
+
// 查找匹配的前缀
|
|
311
|
+
for (const [protocol, _code] of Object.entries(uriPrefixMap)) {
|
|
312
|
+
if (uri.startsWith(protocol) && protocol.length > prefix.length) {
|
|
313
|
+
prefix = protocol;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 提取 URI 后缀部分
|
|
318
|
+
if (prefix) {
|
|
319
|
+
suffix = uri.substring(prefix.length);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 获取前缀代码
|
|
323
|
+
const prefixCode = uriPrefixMap[prefix] || 0x00;
|
|
324
|
+
|
|
325
|
+
// 构建 payload:前缀代码 + 剩余 URI
|
|
326
|
+
const payloadArray = [prefixCode];
|
|
327
|
+
if (suffix) {
|
|
328
|
+
// 将后缀字符串转换为字节数组
|
|
329
|
+
const suffixBytes = stringToBytes(suffix);
|
|
330
|
+
payloadArray.push(...suffixBytes);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
tnf: 1, // TNF_WELL_KNOWN
|
|
335
|
+
type: 'U', // URI 类型
|
|
336
|
+
payload: arrayNumberToArrayBuffer(payloadArray),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function getTypeDescription(type: string, payload?: number[]) {
|
|
341
|
+
switch (type) {
|
|
342
|
+
case 'T':
|
|
343
|
+
return '文本记录';
|
|
344
|
+
case 'U':
|
|
345
|
+
// URI记录需要根据payload的第一个字节确定具体类型
|
|
346
|
+
if (payload && payload.length > 0) {
|
|
347
|
+
const uriPrefixCode = payload[0]; // 第一个字节是URI前缀代码
|
|
348
|
+
return getUriTypeDescription(uriPrefixCode);
|
|
349
|
+
}
|
|
350
|
+
return 'URI记录';
|
|
351
|
+
case 'Sp':
|
|
352
|
+
return 'Smart Poster记录';
|
|
353
|
+
case 'Sig':
|
|
354
|
+
return '签名记录';
|
|
355
|
+
case 'application/vnd.wfa.wsc':
|
|
356
|
+
return 'Wi-Fi Simple Configuration记录';
|
|
357
|
+
case 'android.com:pkg':
|
|
358
|
+
return 'Android 包名记录';
|
|
359
|
+
case 'text/vcard':
|
|
360
|
+
return 'VCard 记录';
|
|
361
|
+
default:
|
|
362
|
+
// 如果是 MIME 类型格式,可以返回更具体的描述
|
|
363
|
+
if (type.startsWith('application/')) {
|
|
364
|
+
return `应用数据类型: ${type}`;
|
|
365
|
+
} else if (type.startsWith('text/')) {
|
|
366
|
+
return `文本数据类型: ${type}`;
|
|
367
|
+
} else if (
|
|
368
|
+
type.startsWith('image/') ||
|
|
369
|
+
type.startsWith('video/') ||
|
|
370
|
+
type.startsWith('audio/')
|
|
371
|
+
) {
|
|
372
|
+
return `媒体数据类型: ${type}`;
|
|
373
|
+
}
|
|
374
|
+
return '未知';
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 通用 TLV 解析配置项接口
|
|
380
|
+
* 支持自定义各类 TLV 解析规则,适配不同格式数据
|
|
381
|
+
*/
|
|
382
|
+
interface TLVParseConfig {
|
|
383
|
+
// 合法 TLV 的 Type 高字节标识(默认:0x10,对应 NFC WiFi 数据)
|
|
384
|
+
legalTypeHigh?: number;
|
|
385
|
+
// 需要跳过的无效 TLV 类型(低字节)列表
|
|
386
|
+
skipTlvTypes?: number[];
|
|
387
|
+
// 长度解析策略(支持自定义,适配标准大端/低字节有效/高字节有效等)
|
|
388
|
+
lengthParseStrategy: (lengthHigh: number, lengthLow: number) => number;
|
|
389
|
+
// 目标 TLV 类型映射(键:TLV 低字节类型,值:字段名称 + 数据转换函数)
|
|
390
|
+
targetTlvMap: Record<
|
|
391
|
+
number,
|
|
392
|
+
{
|
|
393
|
+
fieldName: string;
|
|
394
|
+
converter: (valueBytes: number[]) => any;
|
|
395
|
+
}
|
|
396
|
+
>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 通用 TLV 解析工具函数
|
|
401
|
+
* 可适配 NFC 门禁卡、公交卡、WiFi 数据等各类 TLV 格式数据
|
|
402
|
+
* @param tlvBytes 原始 TLV 字节数组
|
|
403
|
+
* @param config 自定义解析配置项
|
|
404
|
+
* @returns 解析后的结果对象(键:配置中定义的 fieldName,值:转换后的数据)
|
|
405
|
+
*/
|
|
406
|
+
export function parseUniversalTLV(
|
|
407
|
+
tlvBytes: number[],
|
|
408
|
+
config: TLVParseConfig,
|
|
409
|
+
): Record<string, any> {
|
|
410
|
+
// 配置项默认值填充
|
|
411
|
+
const finalConfig = {
|
|
412
|
+
legalTypeHigh: 0x10,
|
|
413
|
+
skipTlvTypes: [],
|
|
414
|
+
...config,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// 存储解析结果
|
|
418
|
+
const parseResult: Record<string, any> = {
|
|
419
|
+
raw: tlvBytes,
|
|
420
|
+
};
|
|
421
|
+
const byteLength = tlvBytes.length;
|
|
422
|
+
let index = 0;
|
|
423
|
+
|
|
424
|
+
// 循环解析 TLV 数据(核心:按配置项灵活解析)
|
|
425
|
+
while (index < byteLength) {
|
|
426
|
+
// 1. 校验是否有足够字节读取 Type(2 字节)
|
|
427
|
+
if (index + 1 >= byteLength) {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const typeHigh = tlvBytes[index];
|
|
432
|
+
const typeLow = tlvBytes[index + 1];
|
|
433
|
+
const currentType = typeLow;
|
|
434
|
+
|
|
435
|
+
// 2. 过滤非法 TLV(非配置指定的高字节标识)
|
|
436
|
+
if (typeHigh !== finalConfig.legalTypeHigh) {
|
|
437
|
+
index++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 3. 跳过配置中指定的无效 TLV(直接跳过头部 4 字节:Type2 + Length2)
|
|
442
|
+
if (finalConfig.skipTlvTypes?.includes(currentType)) {
|
|
443
|
+
index += 4;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 4. 步进 Type 字节(2 字节),准备读取 Length
|
|
448
|
+
index += 2;
|
|
449
|
+
if (index + 1 >= byteLength) {
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 5. 按自定义策略解析 Length(灵活适配不同格式)
|
|
454
|
+
const lengthHigh = tlvBytes[index];
|
|
455
|
+
const lengthLow = tlvBytes[index + 1];
|
|
456
|
+
const currentLength = finalConfig.lengthParseStrategy(
|
|
457
|
+
lengthHigh,
|
|
458
|
+
lengthLow,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// 6. 步进 Length 字节(2 字节),准备读取 Value
|
|
462
|
+
index += 2;
|
|
463
|
+
if (index + currentLength > byteLength) {
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 7. 读取 Value 部分(按自定义长度截取)
|
|
468
|
+
const currentValue = tlvBytes.slice(index, index + currentLength);
|
|
469
|
+
|
|
470
|
+
// 8. 强制步进 Value 字节(避免索引卡死,确保遍历完整性)
|
|
471
|
+
index += currentLength;
|
|
472
|
+
|
|
473
|
+
// 9. 按自定义映射转换目标 TLV 数据
|
|
474
|
+
const targetTlvConfig = finalConfig.targetTlvMap[currentType];
|
|
475
|
+
if (targetTlvConfig) {
|
|
476
|
+
try {
|
|
477
|
+
const convertedData = targetTlvConfig.converter(currentValue);
|
|
478
|
+
parseResult[targetTlvConfig.fieldName] = convertedData;
|
|
479
|
+
} catch (_e) {
|
|
480
|
+
parseResult[targetTlvConfig.fieldName] = null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return parseResult;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 辅助工具:修正 authType 转换逻辑,适配 32(0x20)等新值
|
|
490
|
+
*/
|
|
491
|
+
const TLVConverterTools = {
|
|
492
|
+
// 1. 字节数组 → UTF-8 字符串(保持原有)
|
|
493
|
+
bytesToUtf8String: (bytes: number[]): string => {
|
|
494
|
+
try {
|
|
495
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(
|
|
496
|
+
new Uint8Array(bytes),
|
|
497
|
+
);
|
|
498
|
+
} catch (_e) {
|
|
499
|
+
return bytes.map((byte) => String.fromCharCode(byte)).join('');
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
// 2. 字节数组 → WIFI 认证类型(核心修正:添加 32(0x20)的映射)
|
|
504
|
+
bytesToWifiAuthType: (bytes: number[]): string => {
|
|
505
|
+
// 适配当前字节数组的 [0, 32],取有效数据(低字节优先,与格式一致)
|
|
506
|
+
const authValue = bytes.length >= 2 ? bytes[1] || bytes[0] : bytes[0];
|
|
507
|
+
console.log(
|
|
508
|
+
`[authType 解析] 原始标识值:${authValue}(十六进制:0x${authValue.toString(16).padStart(2, '0')}`,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
switch (authValue) {
|
|
512
|
+
case 1:
|
|
513
|
+
return 'OPEN (无认证)';
|
|
514
|
+
case 2:
|
|
515
|
+
case 34: // 0x22:原有 WPA2-Personal
|
|
516
|
+
return 'WPA2-Personal';
|
|
517
|
+
case 32: // 0x20:新增,适配当前字节数组的 authType 值
|
|
518
|
+
return 'WPA-Personal (兼容 WPA2)';
|
|
519
|
+
case 3:
|
|
520
|
+
return 'WPA-Personal';
|
|
521
|
+
default:
|
|
522
|
+
return `未知认证类型(0x${authValue.toString(16).padStart(4, '0')}`;
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
// 3. 其他工具函数(保持原有,可选)
|
|
527
|
+
bytesToHexString: (bytes: number[], separator = ''): string => {
|
|
528
|
+
return bytes
|
|
529
|
+
.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase())
|
|
530
|
+
.join(separator);
|
|
531
|
+
},
|
|
532
|
+
bytesToDecimal: (bytes: number[], isBigEndian = true): number => {
|
|
533
|
+
let result = 0;
|
|
534
|
+
if (isBigEndian) {
|
|
535
|
+
bytes.forEach((byte) => {
|
|
536
|
+
result = (result << 8) | byte;
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
bytes.reverse().forEach((byte) => {
|
|
540
|
+
result = (result << 8) | byte;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
},
|
|
545
|
+
bytesToWifiEncryptionType: (bytes: number[]): string => {
|
|
546
|
+
const isAllFf = bytes.every((byte) => byte === 255);
|
|
547
|
+
if (isAllFf) {
|
|
548
|
+
return 'AES/TKIP (WPA2 兼容)';
|
|
549
|
+
}
|
|
550
|
+
const encryptValue = TLVConverterTools.bytesToDecimal(bytes);
|
|
551
|
+
return encryptValue === 0
|
|
552
|
+
? 'NONE (无加密)'
|
|
553
|
+
: `WEP(0x${encryptValue.toString(16).padStart(4, '0')}`;
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// 定义 WiFi 数据的自定义解析配置
|
|
558
|
+
export const wifiTLVConfig: TLVParseConfig = {
|
|
559
|
+
legalTypeHigh: 0x10, // 合法 TLV 高字节为 16
|
|
560
|
+
skipTlvTypes: [14], // 跳过无效 TLV 类型 14
|
|
561
|
+
// 长度解析策略:低字节有效(适配非标准 WiFi 数据)
|
|
562
|
+
lengthParseStrategy: (lengthHigh, lengthLow) => lengthLow,
|
|
563
|
+
// 目标 TLV 映射(按需扩展)
|
|
564
|
+
targetTlvMap: {
|
|
565
|
+
69: {
|
|
566
|
+
// SSID 类型
|
|
567
|
+
fieldName: 'ssid',
|
|
568
|
+
converter: TLVConverterTools.bytesToUtf8String,
|
|
569
|
+
},
|
|
570
|
+
39: {
|
|
571
|
+
// 密码类型
|
|
572
|
+
fieldName: 'password',
|
|
573
|
+
converter: TLVConverterTools.bytesToUtf8String,
|
|
574
|
+
},
|
|
575
|
+
3: {
|
|
576
|
+
// 认证类型
|
|
577
|
+
fieldName: 'authType',
|
|
578
|
+
converter: TLVConverterTools.bytesToWifiAuthType,
|
|
579
|
+
},
|
|
580
|
+
32: {
|
|
581
|
+
// 加密类型
|
|
582
|
+
fieldName: 'encryptionType',
|
|
583
|
+
converter: TLVConverterTools.bytesToWifiEncryptionType,
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// WSC 配置接口
|
|
589
|
+
|
|
590
|
+
// 生成 WSC 数据 (Android/Windows 更兼容的格式)
|
|
591
|
+
export function generateWSCBytesForAndroid(config: NFC.WSCConfig): number[] {
|
|
592
|
+
const bytes: number[] = [];
|
|
593
|
+
|
|
594
|
+
// 这是 Android 和 Windows 都接受的简化格式
|
|
595
|
+
// 参考: Wi-Fi Alliance WSC Specification 2.0
|
|
596
|
+
|
|
597
|
+
// 1. Credential 属性 (0x100e)
|
|
598
|
+
// 这是一个容器属性,包含其他属性
|
|
599
|
+
|
|
600
|
+
// 先收集所有属性
|
|
601
|
+
const credentialAttrs: number[] = [];
|
|
602
|
+
|
|
603
|
+
// SSID
|
|
604
|
+
const ssidBytes = Array.from(config.ssid, (c) => c.charCodeAt(0));
|
|
605
|
+
credentialAttrs.push(0x10, 0x45); // SSID
|
|
606
|
+
credentialAttrs.push(0x00, ssidBytes.length);
|
|
607
|
+
credentialAttrs.push(...ssidBytes);
|
|
608
|
+
|
|
609
|
+
// 认证类型
|
|
610
|
+
let authValue = 0x0020; // 默认 WPA2-Personal
|
|
611
|
+
if (config.authType === 'OPEN') authValue = 0x0001;
|
|
612
|
+
if (config.authType === 'WPA') authValue = 0x0022;
|
|
613
|
+
|
|
614
|
+
credentialAttrs.push(0x10, 0x03); // Authentication Type
|
|
615
|
+
credentialAttrs.push(0x00, 0x02);
|
|
616
|
+
credentialAttrs.push((authValue >> 8) & 0xff, authValue & 0xff);
|
|
617
|
+
|
|
618
|
+
// 加密类型
|
|
619
|
+
let encValue = 0x0008; // 默认 AES
|
|
620
|
+
if (config.encryptionType === 'NONE') encValue = 0x0001;
|
|
621
|
+
if (config.encryptionType === 'TKIP') encValue = 0x0004;
|
|
622
|
+
if (config.encryptionType === 'WEP') encValue = 0x0008;
|
|
623
|
+
|
|
624
|
+
credentialAttrs.push(0x10, 0x0f); // Encryption Type
|
|
625
|
+
credentialAttrs.push(0x00, 0x02);
|
|
626
|
+
credentialAttrs.push((encValue >> 8) & 0xff, encValue & 0xff);
|
|
627
|
+
|
|
628
|
+
// 网络密钥
|
|
629
|
+
if (config.authType !== 'OPEN') {
|
|
630
|
+
const keyBytes = Array.from(config.password, (c) => c.charCodeAt(0));
|
|
631
|
+
credentialAttrs.push(0x10, 0x27); // Network Key
|
|
632
|
+
credentialAttrs.push(0x00, keyBytes.length);
|
|
633
|
+
credentialAttrs.push(...keyBytes);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 网络密钥索引 (WEP 需要)
|
|
637
|
+
if (config.encryptionType === 'WEP') {
|
|
638
|
+
const idx = config.networkKeyIndex || 1;
|
|
639
|
+
credentialAttrs.push(0x10, 0x28); // Network Key Index
|
|
640
|
+
credentialAttrs.push(0x00, 0x01);
|
|
641
|
+
credentialAttrs.push(idx);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 将 Credential 属性添加到主字节数组
|
|
645
|
+
bytes.push(0x10, 0x0e); // Credential
|
|
646
|
+
bytes.push(0x00, credentialAttrs.length);
|
|
647
|
+
bytes.push(...credentialAttrs);
|
|
648
|
+
|
|
649
|
+
return bytes;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* vCard (text/vcard) 数据解析工具
|
|
654
|
+
* 适配 vCard 3.0 格式,支持提取核心联系人字段,具备容错性
|
|
655
|
+
*/
|
|
656
|
+
|
|
657
|
+
const vcardFieldMap: Record<string, string> = {
|
|
658
|
+
FN: 'fullName',
|
|
659
|
+
VERSION: 'version',
|
|
660
|
+
ORG: 'organization',
|
|
661
|
+
TEL: 'telephone',
|
|
662
|
+
ADR: 'address',
|
|
663
|
+
URL: 'url',
|
|
664
|
+
};
|
|
665
|
+
const reverseVcardFieldMap: Record<string, string> = Object.entries(
|
|
666
|
+
vcardFieldMap,
|
|
667
|
+
).reduce(
|
|
668
|
+
(acc, [key, value]) => {
|
|
669
|
+
acc[value] = key;
|
|
670
|
+
return acc;
|
|
671
|
+
},
|
|
672
|
+
{} as Record<string, string>,
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* 解析 vCard 格式的 payload 字符串,提取联系人信息
|
|
677
|
+
* @param vcardPayload NFC 读取到的 vCard 原始字符串(含 \n 分隔符)
|
|
678
|
+
* BEGIN:VCARD\nVERSION:3.0\nFN:any\nORG:any\nTEL:18888888888\nADR:杭州\nURL:www.baidu.com\nEND:VCARD\n
|
|
679
|
+
* @returns 结构化的联系人信息对象
|
|
680
|
+
*/
|
|
681
|
+
export function parseVCardPayload(vcardPayload: string): NFC.VCardContactInfo {
|
|
682
|
+
// 初始化默认结果
|
|
683
|
+
const contactInfo: NFC.VCardContactInfo = {
|
|
684
|
+
fullName: '',
|
|
685
|
+
version: '',
|
|
686
|
+
organization: '',
|
|
687
|
+
telephone: '',
|
|
688
|
+
address: '',
|
|
689
|
+
url: '',
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// 步骤1:预处理 payload(去除首尾空白、处理不同分隔符 \n/\r\n/\r,拆分行为数组)
|
|
693
|
+
const processedPayload = vcardPayload
|
|
694
|
+
.trim()
|
|
695
|
+
.replace(/\r\n/g, '\n')
|
|
696
|
+
.replace(/\r/g, '\n');
|
|
697
|
+
const vcardLines = processedPayload
|
|
698
|
+
.split('\n')
|
|
699
|
+
.filter((line) => line.trim() !== '');
|
|
700
|
+
|
|
701
|
+
// 步骤2:验证是否为合法 vCard 格式
|
|
702
|
+
const isLegalVCard =
|
|
703
|
+
vcardLines.some((line) => line.startsWith('BEGIN:VCARD')) &&
|
|
704
|
+
vcardLines.some((line) => line.startsWith('END:VCARD'));
|
|
705
|
+
if (!isLegalVCard) {
|
|
706
|
+
console.warn(
|
|
707
|
+
'[vCard 解析] 无效的 vCard 格式,缺少 BEGIN:VCARD 或 END:VCARD 标记',
|
|
708
|
+
);
|
|
709
|
+
return contactInfo;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 步骤3:遍历每行,解析关键字段(KEY:VALUE 格式)
|
|
713
|
+
|
|
714
|
+
for (const line of vcardLines) {
|
|
715
|
+
// 拆分 KEY 和 VALUE(按第一个冒号分割,避免 VALUE 中包含冒号的情况)
|
|
716
|
+
const colonIndex = line.indexOf(':');
|
|
717
|
+
if (colonIndex === -1) continue;
|
|
718
|
+
|
|
719
|
+
const fieldKey = line.substring(0, colonIndex).trim().toUpperCase();
|
|
720
|
+
const fieldValue = line.substring(colonIndex + 1).trim();
|
|
721
|
+
|
|
722
|
+
// 映射到结构化对象字段
|
|
723
|
+
const targetField = vcardFieldMap[fieldKey];
|
|
724
|
+
if (targetField) {
|
|
725
|
+
contactInfo[targetField] = fieldValue;
|
|
726
|
+
} else {
|
|
727
|
+
// 支持扩展字段(直接挂载到对象上)
|
|
728
|
+
contactInfo[fieldKey.toLowerCase()] = fieldValue;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return contactInfo;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* 格式化 vCard 联系人信息
|
|
737
|
+
*
|
|
738
|
+
* @export
|
|
739
|
+
* @param {NFC.VCardContactInfo} cardInfo
|
|
740
|
+
* @return {*}
|
|
741
|
+
*/
|
|
742
|
+
export function formatCardInfo(cardInfo: NFC.VCardContactInfo) {
|
|
743
|
+
const list = Object.keys(cardInfo)
|
|
744
|
+
.filter((key) => {
|
|
745
|
+
return cardInfo[key];
|
|
746
|
+
})
|
|
747
|
+
.map((key) => {
|
|
748
|
+
return `${reverseVcardFieldMap[key]}:${cardInfo[key]}`;
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
list.unshift('BEGIN:VCARD');
|
|
752
|
+
list.push('END:VCARD');
|
|
753
|
+
return list.join('\n');
|
|
754
|
+
}
|