Coding4Fun–GPS心跳表記錄檔轉TCX
| | 4 | | ![]() |
RunKeeper是個有趣的健身社群網站,在WP7、iPhone、Android等手機平台都有App,利用手機GPS定位邊慢跑邊記錄,能算出時速、里程、消耗卡路里等,之後還可上傳到網站,產生精美報告,結合Google Map呈現跑步路線,提供坡度爬升、平均速度的曲線圖,最後還可將結果分享到Facebook、Twitter等社群平台,平添運動健身的樂趣! 除了透過App記錄,RunKeeper也支援GPS記錄檔上傳,換句話說,GPS手錶或其他導航裝置也可當作RunKeeper的記錄工具,甚至比App再多出心跳記錄功能。
之前試過抓著HD7執行RunKeeper App跑完政大環山道,邊跑邊看速度讓慢跑更有趣,只是抓著有點重量的手機跑步,總要擔心一個手滑摔機,會淚灑跑道,難免礙手礙腳,無法盡情奔跑。之前看過有人用手機臂套把iPhone綁在手臂上,不過手臂多掛162公克的重物,多少會影響平衡,而臂套必須束緊防滑,會造成一些不適感,也不算很完美的方案。現在有了GPS手錶,就方便多了。
話說新入手的心跳錶雖然便宜大碗,但畢竟難比國際大廠,整合應用上不如Garmin產品吃香。以RunKeeper的GPS記錄檔匯入功能為例,它支援GPX、TCX兩種格式(TCX為Garmin發展的規格,除地理資訊外,還可包含心跳率、自行車踏頻等資料),Gramin裝置可直接匯出整合。而GH-625M搭配的是自家的GS-Sport Traning Gym軟體,可由手錶載入記錄、繪製Google Map路線圖及速度、高度、心跳曲線圖,並能依日期歸檔,功能尚可,但不如RunKeeper報表來得精美,且少了社群分享功能也少了幾分趣味。
GS-Sport Training Gym可將跑步歷程匯出成GPX格式,但心跳率部分使用自創的XML Schema儲存,RunKeeper無法識別。而在Garmin的規格中,TCX才支援心跳,於是我想到若能將其轉為TCX格式,就可實現連同心跳資料一起匯入RunKeeper的夢想。
做了簡單的研究:
- 關於TCX,Garmin提供了很詳細的規格,還有Sample(TCX Fitness Courses Detail)可參考,要做出符合規格的XML不難。
- GS-Sport Training Gym除了可將運動路線匯出成GPX外,還有一個更有趣的匯出格式ACT,用Notepad++打開ACT檔一看:
有沒有"他鄉遇故知"的激動感呀? 這不就是.NET DataSet的XML儲存格式嗎? 小試之後,確認用DataSet.LoadXml()就能將它還原回內含三個DataTable的.NET DataSet物件,處理起來比GPX還方便。(猜想GS-Sport Training Gym應該是用.NET開發的)
此等天時地利人和,還不寫個轉檔程式自用,豈不人神共憤,天地難容,沒資格被稱作資深.NET開發人員?
所以,不到150行,TCX轉檔程式就完成囉! 編譯成WinForm型式,就仿照【潛盾機】避免Excel開啟CSV時截掉左補零的小工具原理,執行後選取ACT檔案,轉成TCX後存於同一目錄下。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.IO;
using System.Windows.Forms;
namespace Act2Tcx
{
class Program
{
[STAThread]
static void Main(string[] args)
{
OpenFileDialog openFile = new OpenFileDialog();
openFile.Filter = "GS-Sport ACT File|*.act";
if (openFile.ShowDialog() == DialogResult.OK)
{
Course c = Load(openFile.FileName);
string tcxFile = Path.Combine(
Path.GetDirectoryName(openFile.FileName),
c.Name.Replace(":", "") + ".tcx");
File.WriteAllText(tcxFile, c.ToXml());
MessageBox.Show("Done! - " + tcxFile);
}
}
public static Course Load(string file)
{
DataSet ds = new DataSet();
ds.ReadXml(file);
DataTable trackmaster = ds.Tables["trackmaster"];
DataTable trackPoints = ds.Tables["TrackPoints"];
DataRow row = trackmaster.Rows[0];
DateTime startTime = DateTime.ParseExact(
row["TrackName"].ToString() + " " + row["StartTime"].ToString(),
"yyyy-M-dd HH:mm:ss", null);
Course c = new Course()
{
Name = string.Format("{0:yyyy-MM-dd HH:mm:ss} {1:N0}m", startTime,
decimal.Parse(row["Distence"].ToString()))
};
DateTime currTime = startTime;
foreach (DataRow tp in trackPoints.Rows)
{
c.TrackPoints.Add(new TrackPoint()
{
Time = currTime,
AltitudeMeters = decimal.Parse(tp["Altitude"].ToString()),
HeartRateBpm = decimal.Parse(tp["HeartRate"].ToString()),
LatitudeDegrees = decimal.Parse(tp["Latitude"].ToString()),
LongitudeDegrees = decimal.Parse(tp["Longitude"].ToString())
});
currTime = currTime.AddSeconds(double.Parse(tp["IntervalTime"].ToString()));
}
return c;
}
}
public class Course
{
public string Name;
public List<TrackPoint> TrackPoints = new List<TrackPoint>();
public string ToXml()
{
StringBuilder sb = new StringBuilder();
TrackPoint st = TrackPoints.First(), ed = TrackPoints.Last();
sb.AppendFormat(@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""no"" ?>
<TrainingCenterDatabase xmlns=""http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:schemaLocation=""http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd"">
<Folders/>
<Courses>
<Course>
<Name>{0}</Name>
<Lap>
<TotalTimeSeconds>{1}</TotalTimeSeconds>
<DistanceMeters>{2}</DistanceMeters>
<BeginPosition>
<LatitudeDegrees>{3}</LatitudeDegrees>
<LongitudeDegrees>{4}</LongitudeDegrees>
</BeginPosition>
<EndPosition>
<LatitudeDegrees>{5}</LatitudeDegrees>
<LongitudeDegrees>{6}</LongitudeDegrees>
</EndPosition>
<AverageHeartRateBpm xsi:type=""HeartRateInBeatsPerMinute_t"">
<Value>{7}</Value>
</AverageHeartRateBpm>
<MaximumHeartRateBpm xsi:type=""HeartRateInBeatsPerMinute_t"">
<Value>{8}</Value>
</MaximumHeartRateBpm>
<Intensity>Active</Intensity>
</Lap>
<Track>
", Name,
(ed.Time - st.Time).TotalSeconds,
0, //DistanceMeters, RunKeeper不需,省略
st.LatitudeDegrees, st.LongitudeDegrees,
ed.LatitudeDegrees, ed.LongitudeDegrees,
0, //AverageHeartRateBpm, RunKeeper不需,省略
TrackPoints.Select(o => o.HeartRateBpm).Max() //MaximumHeartRateBpm
);
foreach (TrackPoint tp in TrackPoints)
sb.AppendLine(tp.ToXml());
sb.AppendLine(@" </Track>
</Course>
</Courses>
</TrainingCenterDatabase>");
return sb.ToString();
}
}
public class TrackPoint
{
public DateTime Time;
public decimal LatitudeDegrees;
public decimal LongitudeDegrees;
public decimal AltitudeMeters;
public decimal DistanceMeters;
public decimal HeartRateBpm;
public string ToXml()
{
return string.Format(@"<Trackpoint>
<Time>{0:yyyy-MM-ddTHH:mm:ssZ}</Time>
<Position>
<LatitudeDegrees>{1}</LatitudeDegrees>
<LongitudeDegrees>{2}</LongitudeDegrees>
</Position>
<AltitudeMeters>{3}</AltitudeMeters>
<DistanceMeters>{4}</DistanceMeters>
<HeartRateBpm xsi:type=""HeartRateInBeatsPerMinute_t"">
<Value>{5}</Value>
</HeartRateBpm>
<SensorState>Absent</SensorState>
</Trackpoint>", Time, LatitudeDegrees, LongitudeDegrees,
AltitudeMeters, DistanceMeters, HeartRateBpm);
}
}
}
就這樣,我的GH-625M記錄也能上傳RunKeeper囉! 會寫程式真好~~
Comments
# by Jason
您好,最近也入手了625M,也有想要把資料輸出到Endomondo,google了一下看到您的文章,不曉得您是否可以把這個轉檔軟體分享出來造福大眾呢?如不方便沒問題,還是感謝。
# by Jeffrey
to Jason, 我做了一個ACT->TCX的線上服務,請享用: http://blog.darkthread.net/post-2012-08-17-act2tcx-online.aspx
# by MJ
Jeffery tks. I use 625 also. And your software helps a lot.
# by Alan
請問有新的網址?