trackswap 0.0.1 → 0.0.2
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/dist/FIT/base.d.ts +91 -0
- package/dist/FIT/base.js +101 -0
- package/dist/FIT/crc.d.ts +1 -0
- package/dist/FIT/crc.js +16 -0
- package/dist/FIT/decoder.d.ts +83 -0
- package/dist/FIT/decoder.js +323 -0
- package/dist/FIT/encoder.d.ts +142 -0
- package/dist/FIT/encoder.js +445 -0
- package/dist/FIT/index.d.ts +12 -0
- package/dist/FIT/index.js +13 -0
- package/dist/FIT/mesg.d.ts +40 -0
- package/dist/FIT/mesg.js +208 -0
- package/dist/FIT/middleware.d.ts +48 -0
- package/dist/FIT/middleware.js +206 -0
- package/dist/FIT/plugins.d.ts +26 -0
- package/dist/FIT/plugins.js +104 -0
- package/dist/FIT/processor.d.ts +58 -0
- package/dist/FIT/processor.js +170 -0
- package/dist/FIT/types/course_point.d.ts +56 -0
- package/dist/FIT/types/course_point.js +58 -0
- package/dist/FIT/types/event.d.ts +49 -0
- package/dist/FIT/types/event.js +50 -0
- package/dist/FIT/types/event_type.d.ts +18 -0
- package/dist/FIT/types/event_type.js +19 -0
- package/dist/FIT/types/file.d.ts +23 -0
- package/dist/FIT/types/file.js +24 -0
- package/dist/FIT/types/index.d.ts +119 -0
- package/dist/FIT/types/index.js +297 -0
- package/dist/FIT/types/mesg_num.d.ts +105 -0
- package/dist/FIT/types/mesg_num.js +106 -0
- package/dist/FIT/types/sport.d.ts +47 -0
- package/dist/FIT/types/sport.js +48 -0
- package/dist/FIT/types.d.ts +231 -0
- package/dist/FIT/types.js +1 -0
- package/dist/FIT/util.d.ts +1 -0
- package/dist/FIT/util.js +7 -0
- package/dist/GPX/base.d.ts +43 -67
- package/dist/GPX/base.js +40 -67
- package/dist/GPX/converters.d.ts +12 -36
- package/dist/GPX/converters.js +11 -103
- package/dist/GPX/decoder.d.ts +5 -5
- package/dist/GPX/decoder.js +6 -0
- package/dist/GPX/encoder.d.ts +4 -0
- package/dist/GPX/encoder.js +3 -0
- package/dist/GPX/index.d.ts +1 -1
- package/dist/GPX/index.js +1 -1
- package/dist/GPX/middleware.d.ts +27 -0
- package/dist/GPX/middleware.js +93 -0
- package/dist/GPX/processor.d.ts +1 -1
- package/dist/GPX/processor.js +12 -11
- package/dist/GPX/types.d.ts +2 -3
- package/dist/TCX/base.d.ts +52 -0
- package/dist/TCX/base.js +62 -0
- package/dist/TCX/converters.d.ts +75 -0
- package/dist/TCX/converters.js +333 -0
- package/dist/TCX/decoder.d.ts +87 -0
- package/dist/TCX/decoder.js +350 -0
- package/dist/TCX/encoder.d.ts +150 -0
- package/dist/TCX/encoder.js +513 -0
- package/dist/TCX/index.d.ts +10 -0
- package/dist/TCX/index.js +9 -0
- package/dist/TCX/middleware.d.ts +27 -0
- package/dist/TCX/middleware.js +93 -0
- package/dist/TCX/processor.d.ts +48 -0
- package/dist/TCX/processor.js +197 -0
- package/dist/TCX/types.d.ts +203 -0
- package/dist/TCX/types.js +1 -0
- package/dist/core/base.d.ts +119 -0
- package/dist/core/base.js +138 -0
- package/dist/core/decoder.d.ts +66 -0
- package/dist/core/decoder.js +90 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +4 -0
- package/dist/core/processor.d.ts +69 -0
- package/dist/core/processor.js +265 -0
- package/dist/core/xml-utils.d.ts +41 -0
- package/dist/core/xml-utils.js +140 -0
- package/package.json +8 -5
package/dist/FIT/mesg.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { types, encodedStrlen } from "./types/index.js";
|
|
2
|
+
import { MesgNum } from "./types/mesg_num.js";
|
|
3
|
+
const mesgDefns = {
|
|
4
|
+
file_id: {
|
|
5
|
+
mesgNum: MesgNum.FILE_ID,
|
|
6
|
+
fieldDefns: [
|
|
7
|
+
{ name: "type", number: 0, type: "enum_file" },
|
|
8
|
+
{ name: "time_created", number: 4, type: "date_time" },
|
|
9
|
+
],
|
|
10
|
+
},
|
|
11
|
+
session: {
|
|
12
|
+
mesgNum: MesgNum.SESSION,
|
|
13
|
+
fieldDefns: [
|
|
14
|
+
{ name: "timestamp", number: 253, type: "date_time" },
|
|
15
|
+
{ name: "start_time", number: 2, type: "date_time" },
|
|
16
|
+
{ name: "total_elapsed_time", number: 7, type: "seconds" },
|
|
17
|
+
{ name: "total_timer_time", number: 8, type: "seconds" },
|
|
18
|
+
{ name: "start_position_lat", number: 3, type: "semicircles" },
|
|
19
|
+
{ name: "start_position_long", number: 4, type: "semicircles" },
|
|
20
|
+
{ name: "total_distance", number: 9, type: "distance" },
|
|
21
|
+
{ name: "total_ascent", number: 21, type: "uint16" },
|
|
22
|
+
{ name: "total_descent", number: 22, type: "uint16" },
|
|
23
|
+
{ name: "sport", number: 5, type: "enum_sport" },
|
|
24
|
+
{ name: "sub_sport", number: 6, type: "enum_sub_sport" },
|
|
25
|
+
{ name: "first_lap_index", number: 11, type: "uint16" },
|
|
26
|
+
{ name: "num_laps", number: 26, type: "uint16" },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
lap: {
|
|
30
|
+
mesgNum: MesgNum.LAP,
|
|
31
|
+
fieldDefns: [
|
|
32
|
+
{ name: "timestamp", number: 253, type: "date_time" },
|
|
33
|
+
{ name: "start_time", number: 2, type: "date_time" },
|
|
34
|
+
{ name: "start_position_lat", number: 3, type: "semicircles" },
|
|
35
|
+
{ name: "start_position_long", number: 4, type: "semicircles" },
|
|
36
|
+
{ name: "end_position_lat", number: 5, type: "semicircles" },
|
|
37
|
+
{ name: "end_position_long", number: 6, type: "semicircles" },
|
|
38
|
+
{ name: "total_elapsed_time", number: 7, type: "seconds" },
|
|
39
|
+
{ name: "total_timer_time", number: 8, type: "seconds" },
|
|
40
|
+
{ name: "total_distance", number: 9, type: "distance" },
|
|
41
|
+
{ name: "total_ascent", number: 21, type: "uint16" },
|
|
42
|
+
{ name: "total_descent", number: 22, type: "uint16" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
record: {
|
|
46
|
+
mesgNum: MesgNum.RECORD,
|
|
47
|
+
fieldDefns: [
|
|
48
|
+
{ name: "timestamp", number: 253, type: "date_time" },
|
|
49
|
+
{ name: "position_lat", number: 0, type: "semicircles" },
|
|
50
|
+
{ name: "position_long", number: 1, type: "semicircles" },
|
|
51
|
+
{ name: "altitude", number: 2, type: "altitude" },
|
|
52
|
+
{ name: "distance", number: 5, type: "distance" },
|
|
53
|
+
{ name: "heart_rate", number: 3, type: "uint8" },
|
|
54
|
+
{ name: "cadence", number: 4, type: "uint8" },
|
|
55
|
+
{ name: "speed", number: 6, type: "speed" },
|
|
56
|
+
{ name: "power", number: 7, type: "uint16" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
event: {
|
|
60
|
+
mesgNum: MesgNum.EVENT,
|
|
61
|
+
fieldDefns: [
|
|
62
|
+
{ name: "timestamp", number: 253, type: "date_time" },
|
|
63
|
+
{ name: "event", number: 0, type: "enum_event" },
|
|
64
|
+
{ name: "event_type", number: 1, type: "enum_event_type" },
|
|
65
|
+
{ name: "event_group", number: 4, type: "uint8" },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
course: {
|
|
69
|
+
mesgNum: MesgNum.COURSE,
|
|
70
|
+
fieldDefns: [
|
|
71
|
+
{ name: "name", number: 5, type: "string" },
|
|
72
|
+
{ name: "sport", number: 4, type: "enum_sport" },
|
|
73
|
+
{ name: "sub_sport", number: 7, type: "enum_sub_sport" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
activity: {
|
|
77
|
+
mesgNum: MesgNum.ACTIVITY,
|
|
78
|
+
fieldDefns: [
|
|
79
|
+
{ name: "timestamp", number: 253, type: "date_time" },
|
|
80
|
+
{ name: "total_timer_time", number: 0, type: "seconds" },
|
|
81
|
+
{ name: "num_sessions", number: 1, type: "uint16" },
|
|
82
|
+
{ name: "type", number: 2, type: "enum_activity" },
|
|
83
|
+
{ name: "event", number: 3, type: "enum_event" },
|
|
84
|
+
{ name: "event_type", number: 4, type: "enum_event_type" },
|
|
85
|
+
{ name: "local_timestamp", number: 5, type: "date_time" },
|
|
86
|
+
{ name: "event_group", number: 6, type: "uint8" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
course_point: {
|
|
90
|
+
mesgNum: MesgNum.COURSE_POINT,
|
|
91
|
+
fieldDefns: [
|
|
92
|
+
{ name: "timestamp", number: 1, type: "date_time" },
|
|
93
|
+
{ name: "position_lat", number: 2, type: "semicircles" },
|
|
94
|
+
{ name: "position_long", number: 3, type: "semicircles" },
|
|
95
|
+
{ name: "distance", number: 4, type: "distance" },
|
|
96
|
+
{ name: "type", number: 5, type: "enum_course_point" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* 将 fieldDefns 和 fieldValues 转换为 fields 对象
|
|
102
|
+
*/
|
|
103
|
+
const fields = (fieldDefns, fieldValues) => {
|
|
104
|
+
return fieldDefns
|
|
105
|
+
.map((fieldDefn) => ({ ...fieldDefn, value: fieldValues[fieldDefn.name] }))
|
|
106
|
+
.filter(({ value }) => value !== undefined);
|
|
107
|
+
};
|
|
108
|
+
export class Mesg {
|
|
109
|
+
static check(mesgName, mesgNum, fieldDefns, values) {
|
|
110
|
+
if (mesgNum === undefined) {
|
|
111
|
+
throw new Error(`Message '${mesgName}' not known`);
|
|
112
|
+
}
|
|
113
|
+
if (fieldDefns === undefined) {
|
|
114
|
+
throw new Error(`Message '${mesgName}' has no field definitions`);
|
|
115
|
+
}
|
|
116
|
+
const fieldNames = fieldDefns.map((fieldDefn) => fieldDefn.name);
|
|
117
|
+
const unknownFields = Object.keys(values).filter((fieldName) => !fieldNames.includes(fieldName));
|
|
118
|
+
if (unknownFields.length) {
|
|
119
|
+
throw new Error(`Message '${mesgName}' has no field definitions named '${unknownFields}'`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
constructor(localNum, mesgName, values) {
|
|
123
|
+
const mesgDefn = mesgDefns[mesgName];
|
|
124
|
+
if (!mesgDefn) {
|
|
125
|
+
throw new Error(`Unknown message type: ${mesgName}`);
|
|
126
|
+
}
|
|
127
|
+
const { mesgNum, fieldDefns } = mesgDefn;
|
|
128
|
+
Mesg.check(mesgName, mesgNum, fieldDefns, values);
|
|
129
|
+
this.localNum = localNum;
|
|
130
|
+
this.mesgNum = mesgNum;
|
|
131
|
+
this.fields = fields(fieldDefns, values);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 获取 defns,并获取了 size
|
|
135
|
+
*/
|
|
136
|
+
get mesgDefn() {
|
|
137
|
+
const fieldDefns = this.fields.map(({ number, type, value }) => {
|
|
138
|
+
const typeInfo = types[type];
|
|
139
|
+
if (!typeInfo) {
|
|
140
|
+
throw new Error(`Unknown type: ${type}`);
|
|
141
|
+
}
|
|
142
|
+
const { size, baseType } = typeInfo;
|
|
143
|
+
if (type === "string") {
|
|
144
|
+
return { number, size: encodedStrlen(value), baseType };
|
|
145
|
+
}
|
|
146
|
+
return { number, size, baseType };
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
localNum: this.localNum,
|
|
150
|
+
mesgNum: this.mesgNum,
|
|
151
|
+
fieldDefns,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
isSameDefn(mesgDefn) {
|
|
155
|
+
const isSameFieldDefn = (defn1, defn2) => defn1.number === defn2.number &&
|
|
156
|
+
defn1.size === defn2.size &&
|
|
157
|
+
defn1.baseType === defn2.baseType;
|
|
158
|
+
const areSameFieldDefns = (defns1, defns2) => defns1.length === defns2.length &&
|
|
159
|
+
defns1.every((defn1, i) => isSameFieldDefn(defn1, defns2[i]));
|
|
160
|
+
const { localNum, mesgNum, fieldDefns } = this.mesgDefn;
|
|
161
|
+
return (mesgNum === mesgDefn.mesgNum &&
|
|
162
|
+
localNum === mesgDefn.localNum &&
|
|
163
|
+
areSameFieldDefns(fieldDefns, mesgDefn.fieldDefns));
|
|
164
|
+
}
|
|
165
|
+
get defnRecord() {
|
|
166
|
+
const { localNum, mesgNum, fieldDefns } = this.mesgDefn;
|
|
167
|
+
const recordLen = 6 + 3 * fieldDefns.length;
|
|
168
|
+
const dv = new DataView(new ArrayBuffer(recordLen));
|
|
169
|
+
dv.setUint8(0, 0x40 | localNum);
|
|
170
|
+
dv.setUint8(2, 1); // big endian
|
|
171
|
+
dv.setUint16(3, mesgNum);
|
|
172
|
+
dv.setUint8(5, fieldDefns.length);
|
|
173
|
+
let offset = 6;
|
|
174
|
+
for (const fieldDefn of fieldDefns) {
|
|
175
|
+
dv.setUint8(offset++, fieldDefn.number);
|
|
176
|
+
dv.setUint8(offset++, fieldDefn.size);
|
|
177
|
+
dv.setUint8(offset++, fieldDefn.baseType);
|
|
178
|
+
}
|
|
179
|
+
return dv.buffer;
|
|
180
|
+
}
|
|
181
|
+
get dataRecord() {
|
|
182
|
+
const { fieldDefns } = this.mesgDefn;
|
|
183
|
+
const recordLen = 1 +
|
|
184
|
+
fieldDefns.reduce((len, { size }) => len + size, 0);
|
|
185
|
+
const dv = new DataView(new ArrayBuffer(recordLen));
|
|
186
|
+
dv.setUint8(0, this.localNum);
|
|
187
|
+
let offset = 1;
|
|
188
|
+
for (const { number, type, value } of this.fields) {
|
|
189
|
+
const typeInfo = types[type];
|
|
190
|
+
if (!typeInfo) {
|
|
191
|
+
throw new Error(`Unknown type: ${type}`);
|
|
192
|
+
}
|
|
193
|
+
const { size, setValue } = typeInfo;
|
|
194
|
+
let mappedValue;
|
|
195
|
+
if ("mapValue" in typeInfo) {
|
|
196
|
+
mappedValue = typeInfo.mapValue(value);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
mappedValue = value;
|
|
200
|
+
}
|
|
201
|
+
if (setValue) {
|
|
202
|
+
setValue.call(dv, offset, mappedValue);
|
|
203
|
+
}
|
|
204
|
+
offset += size;
|
|
205
|
+
}
|
|
206
|
+
return dv.buffer;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseFITMiddleware } from "./base.js";
|
|
2
|
+
import { FITDecoderMesgs, FITFileType, FITContext } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 性能监控中间件
|
|
5
|
+
*/
|
|
6
|
+
export declare class PerformanceMiddleware extends BaseFITMiddleware {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
onParse(buffer: Buffer, context: FITContext): Promise<Buffer>;
|
|
10
|
+
onExtractMessages(messages: FITDecoderMesgs, context: FITContext): Promise<FITDecoderMesgs>;
|
|
11
|
+
onStructure(result: Partial<FITFileType>, context: FITContext): Promise<Partial<FITFileType>>;
|
|
12
|
+
onComplete(result: FITFileType, context: FITContext): Promise<FITFileType>;
|
|
13
|
+
onError(error: Error, context: FITContext): Promise<void>;
|
|
14
|
+
private countMessages;
|
|
15
|
+
private analyzeStructure;
|
|
16
|
+
private getSummary;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 数据验证中间件
|
|
20
|
+
*/
|
|
21
|
+
export declare class ValidationMiddleware extends BaseFITMiddleware {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
onComplete(result: FITFileType, context: FITContext): Promise<FITFileType>;
|
|
25
|
+
private validateFIT;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 数据清理中间件
|
|
29
|
+
*/
|
|
30
|
+
export declare class DataCleanupMiddleware extends BaseFITMiddleware {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
onStructure(result: Partial<FITFileType>, context: FITContext): Promise<Partial<FITFileType>>;
|
|
34
|
+
private cleanupData;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 文件信息提取中间件
|
|
38
|
+
*/
|
|
39
|
+
export declare class FileInfoMiddleware extends BaseFITMiddleware {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
onExtractMessages(messages: FITDecoderMesgs, context: FITContext): Promise<FITDecoderMesgs>;
|
|
43
|
+
private extractFileInfo;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 注册所有默认中间件
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerDefaultMiddlewares(decoder: any): Promise<void>;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { BaseFITMiddleware } from "./base.js";
|
|
2
|
+
// ============ 示例中间件 ============
|
|
3
|
+
/**
|
|
4
|
+
* 性能监控中间件
|
|
5
|
+
*/
|
|
6
|
+
export class PerformanceMiddleware extends BaseFITMiddleware {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.name = "performance-middleware";
|
|
10
|
+
this.description = "监控FIT解析性能";
|
|
11
|
+
}
|
|
12
|
+
async onParse(buffer, context) {
|
|
13
|
+
console.log(`🔧 开始解析FIT文件,大小: ${buffer.length} bytes`);
|
|
14
|
+
return buffer;
|
|
15
|
+
}
|
|
16
|
+
async onExtractMessages(messages, context) {
|
|
17
|
+
const messageCount = this.countMessages(messages);
|
|
18
|
+
console.log(`📋 消息提取完成,包含 ${messageCount} 条消息`);
|
|
19
|
+
// 显示消息类型统计
|
|
20
|
+
const messageTypes = Object.keys(messages).filter((key) => Array.isArray(messages[key]) && messages[key].length > 0);
|
|
21
|
+
console.log(` 消息类型: ${messageTypes.join(", ")}`);
|
|
22
|
+
return messages;
|
|
23
|
+
}
|
|
24
|
+
async onStructure(result, context) {
|
|
25
|
+
const structureInfo = this.analyzeStructure(result);
|
|
26
|
+
console.log(`🏗️ 数据结构化完成: ${structureInfo}`);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
async onComplete(result, context) {
|
|
30
|
+
const performance = context.metadata.get("performance");
|
|
31
|
+
console.log("⚡ FIT解析性能统计:", performance);
|
|
32
|
+
// 显示最终结果统计
|
|
33
|
+
console.log(`✅ 解析完成: ${this.getSummary(result)}`);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
async onError(error, context) {
|
|
37
|
+
console.error(`❌ FIT解析出错: ${error.message}`);
|
|
38
|
+
console.error(` 错误详情:`, error.stack);
|
|
39
|
+
}
|
|
40
|
+
countMessages(messages) {
|
|
41
|
+
let count = 0;
|
|
42
|
+
Object.values(messages).forEach((messageArray) => {
|
|
43
|
+
if (Array.isArray(messageArray)) {
|
|
44
|
+
count += messageArray.length;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return count;
|
|
48
|
+
}
|
|
49
|
+
analyzeStructure(result) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (result.sessionMesgs?.length) {
|
|
52
|
+
parts.push(`${result.sessionMesgs.length} sessions`);
|
|
53
|
+
}
|
|
54
|
+
if (result.fileIdMesgs?.length) {
|
|
55
|
+
parts.push(`文件ID`);
|
|
56
|
+
}
|
|
57
|
+
if (result.eventMesgs?.length) {
|
|
58
|
+
parts.push(`${result.eventMesgs.length} events`);
|
|
59
|
+
}
|
|
60
|
+
return parts.join(", ") || "无结构化数据";
|
|
61
|
+
}
|
|
62
|
+
getSummary(result) {
|
|
63
|
+
const parts = [];
|
|
64
|
+
if (result.sessionMesgs?.length) {
|
|
65
|
+
parts.push(`${result.sessionMesgs.length}个会话`);
|
|
66
|
+
}
|
|
67
|
+
if (result.fileIdMesgs?.length) {
|
|
68
|
+
const fileId = result.fileIdMesgs[0];
|
|
69
|
+
parts.push(`类型:${fileId.type}, 厂商:${fileId.manufacturer}`);
|
|
70
|
+
}
|
|
71
|
+
return parts.join(", ") || "空结果";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 数据验证中间件
|
|
76
|
+
*/
|
|
77
|
+
export class ValidationMiddleware extends BaseFITMiddleware {
|
|
78
|
+
constructor() {
|
|
79
|
+
super(...arguments);
|
|
80
|
+
this.name = "validation-middleware";
|
|
81
|
+
this.description = "验证FIT数据的完整性";
|
|
82
|
+
}
|
|
83
|
+
async onComplete(result, context) {
|
|
84
|
+
const errors = this.validateFIT(result);
|
|
85
|
+
if (errors.length > 0) {
|
|
86
|
+
console.warn("🚨 FIT验证发现问题:", errors);
|
|
87
|
+
context.metadata.set("validation-errors", errors);
|
|
88
|
+
// 将验证错误添加到上下文
|
|
89
|
+
errors.forEach((error) => context.warnings.push(error));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log("✅ FIT数据验证通过");
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
validateFIT(fit) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
// 验证文件ID
|
|
99
|
+
if (!fit.fileIdMesgs || fit.fileIdMesgs.length === 0) {
|
|
100
|
+
errors.push("缺少文件ID信息");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const fileId = fit.fileIdMesgs[0];
|
|
104
|
+
if (!fileId.type) {
|
|
105
|
+
errors.push("文件类型未指定");
|
|
106
|
+
}
|
|
107
|
+
if (!fileId.manufacturer) {
|
|
108
|
+
errors.push("制造商信息缺失");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 验证会话数据
|
|
112
|
+
if (fit.sessionMesgs && fit.sessionMesgs.length > 0) {
|
|
113
|
+
fit.sessionMesgs.forEach((session, index) => {
|
|
114
|
+
if (!session.startTime) {
|
|
115
|
+
errors.push(`会话 ${index + 1} 缺少开始时间`);
|
|
116
|
+
}
|
|
117
|
+
if (!session.timestamp) {
|
|
118
|
+
errors.push(`会话 ${index + 1} 缺少结束时间`);
|
|
119
|
+
}
|
|
120
|
+
if (session.totalTimerTime === undefined) {
|
|
121
|
+
errors.push(`会话 ${index + 1} 缺少计时时间`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return errors;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 数据清理中间件
|
|
130
|
+
*/
|
|
131
|
+
export class DataCleanupMiddleware extends BaseFITMiddleware {
|
|
132
|
+
constructor() {
|
|
133
|
+
super(...arguments);
|
|
134
|
+
this.name = "data-cleanup-middleware";
|
|
135
|
+
this.description = "清理和优化FIT数据";
|
|
136
|
+
}
|
|
137
|
+
async onStructure(result, context) {
|
|
138
|
+
// 清理空数组和无效数据
|
|
139
|
+
return this.cleanupData(result);
|
|
140
|
+
}
|
|
141
|
+
cleanupData(result) {
|
|
142
|
+
const cleaned = { ...result };
|
|
143
|
+
// 移除空数组
|
|
144
|
+
Object.keys(cleaned).forEach((key) => {
|
|
145
|
+
const value = cleaned[key];
|
|
146
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
147
|
+
delete cleaned[key];
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// 清理会话数据中的无效值
|
|
151
|
+
if (cleaned.sessionMesgs) {
|
|
152
|
+
cleaned.sessionMesgs = cleaned.sessionMesgs.filter((session) => session.startTime && session.timestamp);
|
|
153
|
+
}
|
|
154
|
+
console.log("🧹 数据清理完成");
|
|
155
|
+
return cleaned;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 文件信息提取中间件
|
|
160
|
+
*/
|
|
161
|
+
export class FileInfoMiddleware extends BaseFITMiddleware {
|
|
162
|
+
constructor() {
|
|
163
|
+
super(...arguments);
|
|
164
|
+
this.name = "file-info-middleware";
|
|
165
|
+
this.description = "提取和显示文件基本信息";
|
|
166
|
+
}
|
|
167
|
+
async onExtractMessages(messages, context) {
|
|
168
|
+
this.extractFileInfo(messages, context);
|
|
169
|
+
return messages;
|
|
170
|
+
}
|
|
171
|
+
extractFileInfo(messages, context) {
|
|
172
|
+
if (messages.fileIdMesgs && messages.fileIdMesgs.length > 0) {
|
|
173
|
+
const fileId = messages.fileIdMesgs[0];
|
|
174
|
+
const fileInfo = {
|
|
175
|
+
type: fileId.type,
|
|
176
|
+
manufacturer: fileId.manufacturer,
|
|
177
|
+
product: fileId.product,
|
|
178
|
+
serialNumber: fileId.serialNumber,
|
|
179
|
+
timeCreated: fileId.timeCreated,
|
|
180
|
+
};
|
|
181
|
+
context.metadata.set("file-info", fileInfo);
|
|
182
|
+
console.log("📄 文件信息:", {
|
|
183
|
+
类型: fileInfo.type,
|
|
184
|
+
制造商: fileInfo.manufacturer,
|
|
185
|
+
产品: fileInfo.product,
|
|
186
|
+
序列号: fileInfo.serialNumber,
|
|
187
|
+
创建时间: fileInfo.timeCreated,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ============ 默认中间件注册函数 ============
|
|
193
|
+
/**
|
|
194
|
+
* 注册所有默认中间件
|
|
195
|
+
*/
|
|
196
|
+
export async function registerDefaultMiddlewares(decoder) {
|
|
197
|
+
const middlewares = [
|
|
198
|
+
new PerformanceMiddleware(),
|
|
199
|
+
new ValidationMiddleware(),
|
|
200
|
+
new DataCleanupMiddleware(),
|
|
201
|
+
new FileInfoMiddleware(),
|
|
202
|
+
];
|
|
203
|
+
for (const middleware of middlewares) {
|
|
204
|
+
await decoder.registerMiddleware(middleware);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseFITStructurePlugin } from "./base.js";
|
|
2
|
+
import { FITDecoderMesgs, FITFileType, FITContext } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Session 数据结构化插件
|
|
5
|
+
*/
|
|
6
|
+
export declare class SessionStructurePlugin extends BaseFITStructurePlugin {
|
|
7
|
+
name: string;
|
|
8
|
+
priority: number;
|
|
9
|
+
structureData(messages: FITDecoderMesgs, context: FITContext): Partial<FITFileType>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Course 数据结构化插件
|
|
13
|
+
*/
|
|
14
|
+
export declare class CourseStructurePlugin extends BaseFITStructurePlugin {
|
|
15
|
+
name: string;
|
|
16
|
+
priority: number;
|
|
17
|
+
structureData(messages: FITDecoderMesgs, context: FITContext): Partial<FITFileType>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 文件头信息提取插件
|
|
21
|
+
*/
|
|
22
|
+
export declare class FileHeaderPlugin extends BaseFITStructurePlugin {
|
|
23
|
+
name: string;
|
|
24
|
+
priority: number;
|
|
25
|
+
structureData(messages: FITDecoderMesgs, context: FITContext): Partial<FITFileType>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import { BaseFITStructurePlugin } from "./base.js";
|
|
3
|
+
/**
|
|
4
|
+
* Session 数据结构化插件
|
|
5
|
+
*/
|
|
6
|
+
export class SessionStructurePlugin extends BaseFITStructurePlugin {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.name = "SessionStructurePlugin";
|
|
10
|
+
this.priority = 10;
|
|
11
|
+
}
|
|
12
|
+
structureData(messages, context) {
|
|
13
|
+
const { sessionMesgs, lapMesgs, recordMesgs } = messages;
|
|
14
|
+
if (!sessionMesgs?.length) {
|
|
15
|
+
console.warn("未发现结构化 session,数据结构不完整");
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
// 将每个 Session 的 Lap 和 Record 关联起来
|
|
19
|
+
const sessionStructureMesgs = sessionMesgs.map((session) => {
|
|
20
|
+
const sessionStartTime = dayjs(session.startTime).valueOf();
|
|
21
|
+
const sessionEndTime = dayjs(session.timestamp).valueOf();
|
|
22
|
+
// 找出属于当前 Session 的 Laps
|
|
23
|
+
const sessionLaps = lapMesgs?.filter((lap) => {
|
|
24
|
+
const lapStartTime = dayjs(lap.startTime).valueOf();
|
|
25
|
+
const lapEndTime = dayjs(lap.timestamp).valueOf();
|
|
26
|
+
return (lapStartTime >= sessionStartTime && lapEndTime <= sessionEndTime);
|
|
27
|
+
}) || [];
|
|
28
|
+
console.log("当前 session 下共有 Lap", sessionLaps.length);
|
|
29
|
+
// 为每个 Lap 找出对应的 Records
|
|
30
|
+
const lapsWithRecords = sessionLaps.map((lap) => {
|
|
31
|
+
const lapStartTime = dayjs(lap.startTime).valueOf();
|
|
32
|
+
const lapEndTime = dayjs(lap.timestamp).valueOf();
|
|
33
|
+
const lapRecords = recordMesgs?.filter((record) => {
|
|
34
|
+
const recordTime = dayjs(record.timestamp).valueOf();
|
|
35
|
+
return recordTime >= lapStartTime && recordTime <= lapEndTime;
|
|
36
|
+
}) || [];
|
|
37
|
+
console.log("找到当前 Lap 下的 records", lapRecords.length, recordMesgs?.length, dayjs(lapStartTime).format("YYYY-MM-DD HH:mm:ss"), dayjs(lapEndTime).format("YYYY-MM-DD HH:mm:ss"));
|
|
38
|
+
return { ...lap, recordMesgs: lapRecords };
|
|
39
|
+
});
|
|
40
|
+
// 返回重新组织后的 Session
|
|
41
|
+
return { ...session, lapMesgs: lapsWithRecords };
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
sessionMesgs: sessionStructureMesgs,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Course 数据结构化插件
|
|
50
|
+
*/
|
|
51
|
+
export class CourseStructurePlugin extends BaseFITStructurePlugin {
|
|
52
|
+
constructor() {
|
|
53
|
+
super(...arguments);
|
|
54
|
+
this.name = "CourseStructurePlugin";
|
|
55
|
+
this.priority = 10;
|
|
56
|
+
}
|
|
57
|
+
structureData(messages, context) {
|
|
58
|
+
const { courseMesgs, lapMesgs, recordMesgs } = messages;
|
|
59
|
+
if (!courseMesgs?.length) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
// 时间容错,默认2秒
|
|
63
|
+
const TIME_TOLERANCE = 2000;
|
|
64
|
+
const courseStructureMesgs = courseMesgs.map((course) => {
|
|
65
|
+
const lapsWithRecords = lapMesgs?.map((lap) => {
|
|
66
|
+
const lapStartTime = dayjs(lap.startTime).valueOf();
|
|
67
|
+
const lapEndTime = dayjs(lap.timestamp).valueOf();
|
|
68
|
+
// 添加时间容错,因为时间戳可能不太准确
|
|
69
|
+
const lapRecords = recordMesgs?.filter((record) => {
|
|
70
|
+
const recordTime = dayjs(record.timestamp).valueOf();
|
|
71
|
+
return (recordTime >= lapStartTime - TIME_TOLERANCE &&
|
|
72
|
+
recordTime <= lapEndTime + TIME_TOLERANCE);
|
|
73
|
+
}) || [];
|
|
74
|
+
return { ...lap, recordMesgs: lapRecords };
|
|
75
|
+
}) || [];
|
|
76
|
+
return { ...course, lapMesgs: lapsWithRecords };
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
courseMesgs: courseStructureMesgs,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 文件头信息提取插件
|
|
85
|
+
*/
|
|
86
|
+
export class FileHeaderPlugin extends BaseFITStructurePlugin {
|
|
87
|
+
constructor() {
|
|
88
|
+
super(...arguments);
|
|
89
|
+
this.name = "FileHeaderPlugin";
|
|
90
|
+
this.priority = 1; // 高优先级,先执行
|
|
91
|
+
}
|
|
92
|
+
structureData(messages, context) {
|
|
93
|
+
const { fileIdMesgs } = messages;
|
|
94
|
+
if (fileIdMesgs?.length && fileIdMesgs[0]) {
|
|
95
|
+
const fileId = fileIdMesgs[0];
|
|
96
|
+
context.fileHeader = {
|
|
97
|
+
type: fileId.type,
|
|
98
|
+
manufacturer: fileId.manufacturer,
|
|
99
|
+
product: fileId.product,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { IFITMessageConverter, IFITStructurePlugin } from "./base.js";
|
|
2
|
+
import { FITContext } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* FIT 流水线阶段
|
|
5
|
+
*/
|
|
6
|
+
export declare enum FITPipelineStage {
|
|
7
|
+
PARSE = "parse",
|
|
8
|
+
EXTRACT = "extract",
|
|
9
|
+
STRUCTURE = "structure",
|
|
10
|
+
COMPLETE = "complete"
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* FIT 流水线处理器接口
|
|
14
|
+
*/
|
|
15
|
+
export interface IFITPipelineProcessor {
|
|
16
|
+
stage: FITPipelineStage;
|
|
17
|
+
process(context: FITContext): Promise<FITContext>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse处理器 - 解析二进制FIT数据为消息
|
|
21
|
+
*/
|
|
22
|
+
export declare class ParseProcessor implements IFITPipelineProcessor {
|
|
23
|
+
stage: FITPipelineStage;
|
|
24
|
+
process(context: FITContext): Promise<FITContext>;
|
|
25
|
+
/**
|
|
26
|
+
* 使用 FIT SDK 解析二进制数据
|
|
27
|
+
*/
|
|
28
|
+
private parseWithFitSDK;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract处理器 - 提取和转换消息
|
|
32
|
+
*/
|
|
33
|
+
export declare class ExtractProcessor implements IFITPipelineProcessor {
|
|
34
|
+
private getConverters;
|
|
35
|
+
stage: FITPipelineStage;
|
|
36
|
+
constructor(getConverters: (messageType: string) => IFITMessageConverter[]);
|
|
37
|
+
process(context: FITContext): Promise<FITContext>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Structure处理器 - 数据结构化
|
|
41
|
+
*/
|
|
42
|
+
export declare class StructureProcessor implements IFITPipelineProcessor {
|
|
43
|
+
private getStructurePlugins;
|
|
44
|
+
stage: FITPipelineStage;
|
|
45
|
+
constructor(getStructurePlugins: () => IFITStructurePlugin[]);
|
|
46
|
+
process(context: FITContext): Promise<FITContext>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Complete处理器 - 最终处理和清理
|
|
50
|
+
*/
|
|
51
|
+
export declare class CompleteProcessor implements IFITPipelineProcessor {
|
|
52
|
+
stage: FITPipelineStage;
|
|
53
|
+
process(context: FITContext): Promise<FITContext>;
|
|
54
|
+
/**
|
|
55
|
+
* 计算消息总数
|
|
56
|
+
*/
|
|
57
|
+
private countTotalMessages;
|
|
58
|
+
}
|