【Java】利用Garmin-Fit-SDK 对码表记录文件截取片段

已经很长时间不写随笔了,因为现在也是大AI时代了,很多问题AI来搞定

 

 

一、需求背景

本来是看有没有APP能实现的,Insta360和大疆都只支持剪自己家设备录制的

于是问AI看怎么处理,最开始的方案是装Dart和依赖来完成

image

 

二、用Dart无法满足需求

但是Dart只能读取Fit一些基本信息

21426a78332a2b326a911d96969c0231

 

感觉就挺无语😅,但是还得继续

image

 

三、Python也存在缺陷

接下来是用python了

image

 

image

 

image

显然提供的脚本漏洞百出,也没办法实现效果

 

四、Garmin Fit SDK 实现

image

 

 

 

Garmin 旧版 FIT SDK 的直接下载链接确实会出现 404,但官方现在把 SDK 托管在了 GitHub 组织 garmin​ 下,各语言版本都是独立仓库,可以直接 clone 或下载 ZIP。

官方 GitHub 仓库地址(当前有效)

 
语言
GitHub 仓库
Java(推荐,含 Encode 示例)
https://github.com/garmin/fit-java-sdk
C++
https://github.com/garmin/fit-cpp-sdk
C#
https://github.com/garmin/fit-csharp-sdk
JavaScript
https://github.com/garmin/fit-javascript-sdk
Python(仅解码)
https://github.com/garmin/fit-python-sdk
FIT SDK 配套工具(FitCSVTool、Profile.xlsx、ActivityRepairTool 等)在另一个仓库:
https://github.com/garmin/fit-sdk-tools
以上均为 Garmin 官方维护的仓库。

 

这里我用的是Java, 配套JDK8,Maven版本 3.9.x

 

这里不展示跟AI反复修正的代码逻辑过程,贴上最后可用的源码:

TimeCutter.java

  1 import com.garmin.fit.*;
  2 import java.io.*;
  3 import java.util.*;
  4 import java.util.concurrent.atomic.AtomicInteger;
  5 
  6 /**
  7  * 按时间裁剪 FIT 活动文件
  8  * 用法: java TimeCutter <输入.fit> <输出.fit> <开始时间(HH:mm)> <结束时间(HH:mm)>
  9  * 示例: java TimeCutter activity.fit cut.fit 19:50 20:20
 10  */
 11 public class TimeCutter {
 12 
 13     // 时间戳字段编号(FIT 标准)
 14     private static final int TIMESTAMP_FIELD_NUM = 253;
 15 
 16     public static void main(String[] args) {
 17         if (args.length != 4) {
 18             System.out.println("用法: java TimeCutter <输入.fit> <输出.fit> <开始时间(HH:mm)> <结束时间(HH:mm)>");
 19             System.out.println("示例: java TimeCutter activity.fit cut.fit 19:50 20:20");
 20             return;
 21         }
 22 
 23         String inputFile = args[0];
 24         String outputFile = args[1];
 25         String startTimeStr = args[2];
 26         String endTimeStr = args[3];
 27 
 28         try {
 29             System.out.println("当前目录: " + new java.io.File(".").getAbsolutePath());
 30             System.out.println("输入文件: " + inputFile);
 31             
 32             java.io.File inputFileObj = new java.io.File(inputFile);
 33             if (!inputFileObj.exists()) {
 34                 System.out.println("❌ 文件不存在: " + inputFile);
 35                 return;
 36             }
 37             System.out.println("文件大小: " + inputFileObj.length() + " 字节");
 38 
 39             DateTime firstTimestamp = getFirstTimestamp(inputFile);
 40             if (firstTimestamp == null) {
 41                 System.out.println("❌ 错误: 无法读取文件开始时间");
 42                 System.out.println("   请确认文件是有效的 FIT 活动文件");
 43                 return;
 44             }
 45 
 46             // 解析用户输入的时间
 47             DateTime startDateTime = parseTimeOfDay(startTimeStr, firstTimestamp);
 48             DateTime endDateTime = parseTimeOfDay(endTimeStr, firstTimestamp);
 49 
 50             System.out.println("文件开始时间: " + formatDateTime(firstTimestamp));
 51             System.out.println("目标区间: " + formatDateTime(startDateTime) + " 到 " + formatDateTime(endDateTime));
 52 
 53             cutFitFile(inputFile, outputFile, startDateTime, endDateTime);
 54             System.out.println("✅ 裁剪完成!输出文件: " + outputFile);
 55 
 56         } catch (Exception e) {
 57             System.err.println("❌ 错误: " + e.getMessage());
 58             e.printStackTrace();
 59         }
 60     }
 61 
 62     /**
 63      * 从 Mesg 中提取时间戳
 64      * 尝试多种方式获取时间戳值
 65      */
 66     private static DateTime extractTimestamp(Mesg mesg) {
 67         // 方法1: 尝试使用 RecordMesg 专用方法
 68         if (mesg instanceof RecordMesg) {
 69             try {
 70                 DateTime ts = ((RecordMesg) mesg).getTimestamp();
 71                 if (ts != null) {
 72                     return ts;
 73                 }
 74             } catch (Exception e) {
 75                 // 忽略,尝试其他方法
 76             }
 77         }
 78 
 79         // 方法2: 通过字段编号获取原始值
 80         Field field = mesg.getField(TIMESTAMP_FIELD_NUM);
 81         if (field != null) {
 82             Object value = field.getValue();
 83             if (value instanceof Number) {
 84                 long seconds = ((Number) value).longValue();
 85                 // FIT 时间戳是从 1989-12-31 00:00:00 UTC 开始的秒数
 86                 return new DateTime(seconds);
 87             } else if (value instanceof DateTime) {
 88                 return (DateTime) value;
 89             } else if (value instanceof Date) {
 90                 return new DateTime((Date) value);
 91             } else if (value instanceof String) {
 92                 try {
 93                     long seconds = Long.parseLong((String) value);
 94                     return new DateTime(seconds);
 95                 } catch (NumberFormatException e) {
 96                     // 忽略
 97                 }
 98             }
 99         }
100 
101         // 方法3: 通过字段名获取
102         Field nameField = mesg.getField("timestamp");
103         if (nameField != null && nameField != field) {
104             Object value = nameField.getValue();
105             if (value instanceof Number) {
106                 long seconds = ((Number) value).longValue();
107                 return new DateTime(seconds);
108             } else if (value instanceof DateTime) {
109                 return (DateTime) value;
110             }
111         }
112 
113         return null;
114     }
115 
116     /**
117      * 获取 FIT 文件中第一条记录的时间戳
118      */
119     private static DateTime getFirstTimestamp(String inputFile) throws Exception {
120         java.io.File file = new java.io.File(inputFile);
121         Decode decode = new Decode();
122         MesgBroadcaster broadcaster = new MesgBroadcaster();
123         
124         List<DateTime> firstTime = new ArrayList<>();
125         AtomicInteger recordCount = new AtomicInteger(0);
126         
127         broadcaster.addListener((MesgListener) mesg -> {
128             if (mesg.getNum() == MesgNum.RECORD) {
129                 int count = recordCount.incrementAndGet();
130                 if (count <= 3) {
131                     System.out.println("  第 " + count + " 条 Record 消息");
132                 }
133                 
134                 if (firstTime.isEmpty()) {
135                     DateTime ts = extractTimestamp(mesg);
136                     if (ts != null) {
137                         firstTime.add(ts);
138                         System.out.println("  ✅ 提取到时间戳: " + ts.getDate());
139                     } else if (count == 1) {
140                         // 如果第一条记录失败,打印调试信息
141                         System.out.println("  ⚠️ 第一条记录无法提取时间戳");
142                         for (Field f : mesg.getFields()) {
143                             System.out.println("    字段: " + f.getName() + 
144                                                " (编号: " + f.getNum() + 
145                                                ", 值: " + f.getValue() + 
146                                                ", 类型: " + f.getType() + ")");
147                         }
148                     }
149                 }
150             }
151         });
152 
153         try (FileInputStream in = new FileInputStream(file)) {
154             decode.read(in, broadcaster);
155         }
156         
157         System.out.println("总共处理了 " + recordCount.get() + " 条 Record 消息");
158         
159         if (firstTime.isEmpty()) {
160             return null;
161         }
162         return firstTime.get(0);
163     }
164 
165     /**
166      * 解析 "HH:mm" 格式的时间
167      */
168     private static DateTime parseTimeOfDay(String timeStr, DateTime baseDateTime) {
169         String[] parts = timeStr.split(":");
170         if (parts.length != 2) {
171             throw new IllegalArgumentException("时间格式错误,请使用 HH:mm");
172         }
173         int hours = Integer.parseInt(parts[0]);
174         int minutes = Integer.parseInt(parts[1]);
175         
176         Date baseDate = baseDateTime.getDate();
177         @SuppressWarnings("deprecation")
178         int year = baseDate.getYear() + 1900;
179         @SuppressWarnings("deprecation")
180         int month = baseDate.getMonth();
181         @SuppressWarnings("deprecation")
182         int day = baseDate.getDate();
183         
184         System.out.println("基准日期: " + year + "-" + (month+1) + "-" + day);
185         System.out.println("目标时间: " + hours + ":" + minutes);
186         
187         @SuppressWarnings("deprecation")
188         Date targetDate = new Date(year - 1900, month, day, hours, minutes, 0);
189         return new DateTime(targetDate);
190     }
191 
192     /**
193      * 核心裁剪逻辑
194      */
195     private static void cutFitFile(String inputFile, String outputFile, 
196                                    DateTime startTime, DateTime endTime) throws Exception {
197         java.io.File file = new java.io.File(inputFile);
198         Decode decode = new Decode();
199         MesgBroadcaster broadcaster = new MesgBroadcaster();
200         
201         List<Mesg> filteredRecords = new ArrayList<>();
202         List<Mesg> otherMessages = new ArrayList<>();
203         
204         broadcaster.addListener((MesgListener) mesg -> {
205             if (mesg.getNum() == MesgNum.RECORD) {
206                 DateTime ts = extractTimestamp(mesg);
207                 if (ts != null && ts.compareTo(startTime) >= 0 && ts.compareTo(endTime) <= 0) {
208                     filteredRecords.add(mesg);
209                 }
210             } else {
211                 otherMessages.add(mesg);
212             }
213         });
214 
215         try (FileInputStream in = new FileInputStream(file)) {
216             decode.read(in, broadcaster);
217         }
218         
219         System.out.println("解码完成: 共 " + filteredRecords.size() + " 条记录在时间区间内");
220 
221         if (filteredRecords.isEmpty()) {
222             throw new Exception("在指定的时间区间内没有找到任何记录");
223         }
224 
225         java.io.File outputFileObj = new java.io.File(outputFile);
226         FileEncoder encoder = new FileEncoder(outputFileObj, Fit.ProtocolVersion.V2_0);
227         
228         // 写入 FileId
229         boolean hasFileId = false;
230         for (Mesg msg : otherMessages) {
231             if (msg.getNum() == MesgNum.FILE_ID) {
232                 encoder.write(msg);
233                 hasFileId = true;
234                 break;
235             }
236         }
237         if (!hasFileId) {
238             FileIdMesg fileId = new FileIdMesg();
239             fileId.setType(com.garmin.fit.File.ACTIVITY);
240             fileId.setManufacturer(1);
241             encoder.write(fileId);
242         }
243 
244         // 写入设备信息
245         for (Mesg msg : otherMessages) {
246             int num = msg.getNum();
247             if (num == MesgNum.DEVICE_INFO || num == MesgNum.SOFTWARE) {
248                 encoder.write(msg);
249                 break;
250             }
251         }
252 
253         // 写入筛选后的记录
254         for (Mesg record : filteredRecords) {
255             encoder.write(record);
256         }
257 
258         // 写入 Session 和 Lap
259         for (Mesg msg : otherMessages) {
260             int num = msg.getNum();
261             if (num == MesgNum.SESSION || num == MesgNum.LAP || num == MesgNum.EVENT) {
262                 encoder.write(msg);
263             }
264         }
265 
266         encoder.close();
267         
268         System.out.println("编码完成,输出文件大小: " + outputFileObj.length() + " 字节");
269     }
270 
271     private static String formatDateTime(DateTime dt) {
272         if (dt == null) return "null";
273         return dt.getDate().toString();
274     }
275 }

 

我是直接拉取SDK源码仓库然后打包项目,再把脚本文件丢在源码里面编译执行的

打包SDK命令:

D:\apache-maven-3.9.16\bin\mvn package -Pfitsdk

编译源码文件

javac -cp "target\fit-21.214.0.jar" TimeCutter.java

执行脚本

java -cp "target\fit-21.214.0.jar;." TimeCutter MAGENE_C506SE_2026-08-25_181425_1272912.fit NEW_CUT.fit 20:00 20:21

  

 

image

 

 

用Garmin VIRB Edit 打开剪切的Fit可以校验结果:

image

 

posted @ 2026-08-26 14:01  emdzz  阅读(24)  评论(0)    收藏  举报