【Java】利用Garmin-Fit-SDK 对码表记录文件截取片段
已经很长时间不写随笔了,因为现在也是大AI时代了,很多问题AI来搞定
一、需求背景
本来是看有没有APP能实现的,Insta360和大疆都只支持剪自己家设备录制的
于是问AI看怎么处理,最开始的方案是装Dart和依赖来完成

二、用Dart无法满足需求
但是Dart只能读取Fit一些基本信息

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

三、Python也存在缺陷
接下来是用python了



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

Garmin 旧版 FIT SDK 的直接下载链接确实会出现 404,但官方现在把 SDK 托管在了 GitHub 组织
garmin 下,各语言版本都是独立仓库,可以直接 clone 或下载 ZIP。官方 GitHub 仓库地址(当前有效)
|
语言
|
GitHub 仓库
|
|---|---|
|
Java(推荐,含 Encode 示例)
|
|
|
C++
|
|
|
C#
|
|
|
JavaScript
|
|
|
Python(仅解码)
|
|
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

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


浙公网安备 33010602011771号