rowingdata 3.8.0__tar.gz → 3.8.2__tar.gz
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.
- {rowingdata-3.8.0/rowingdata.egg-info → rowingdata-3.8.2}/PKG-INFO +4 -2
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/data/fit_export_spec.json +10 -5
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fitwrite.py +178 -87
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fitwrite_spec.py +8 -3
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/otherparsers.py +89 -7
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/rowingdata.py +11 -3
- {rowingdata-3.8.0 → rowingdata-3.8.2/rowingdata.egg-info}/PKG-INFO +4 -2
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/requires.txt +3 -1
- {rowingdata-3.8.0 → rowingdata-3.8.2}/setup.py +2 -2
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testtcx.tcx +1 -1
- {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_fit_instroke_export.py +16 -72
- {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_fit_standard.py +100 -12
- {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_rowingdata.py +16 -19
- {rowingdata-3.8.0 → rowingdata-3.8.2}/LICENSE +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/MANIFEST.in +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/README.rst +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/bin/crewnerddata.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/bin/crewnerddata.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/bin/testdata.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/.buildinfo +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/2x20min.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/editrower.JPG +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/greghoc.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/image001.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/otwlogbook.JPG +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/otwscreenshot.JPG +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/screenshot.JPG +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/screenshotlogbook.JPG +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/woensdag.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_sources/index.txt +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_sources/modules.txt +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_sources/rowingdata.txt +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/ajax-loader.gif +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/alabaster.css +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/basic.css +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/comment-bright.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/comment-close.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/comment.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/custom.css +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/doctools.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/down-pressed.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/down.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/file.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/jquery-1.11.1.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/jquery.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/minus.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/plus.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/pygments.css +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/searchtools.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/underscore-1.3.1.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/underscore.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/up-pressed.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/up.png +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/websupport.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/genindex.html +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/index.html +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/modules.html +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/objects.inv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/py-modindex.html +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/rowingdata.html +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/search.html +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/searchindex.js +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/__init__.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/__main__.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/boatedit.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/checkdatafiles.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/copystats.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/crewnerdplot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/crewnerdplottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/csvparsers.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergdataplot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergdataplottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergdatatotcx.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergstickplot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergstickplottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergsticktotcx.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fit_garmin_bridge.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fit_transcode.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/gpxtools.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/gpxwrite.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/konkatenaadje.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/laptesting.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/obsolete.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsled_desktop_plot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsled_desktop_plottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsled_desktop_toc2.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsledplot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsledplottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsledtoc2.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/roweredit.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/rowproplot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/rowproplottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/speedcoachplot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/speedcoachplottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/speedcoachtoc2.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplot.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplot_nogeo.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplottime.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplottime_nogeo.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxtoc2.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxtools.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/trainingparser.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/utils.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/windcorrected.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/writetcx.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/SOURCES.txt +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/dependency_links.txt +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/not-zip-safe +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/top_level.txt +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/setup.cfg +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2016-03-25-0758.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2016-03-25-0758_data.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2x20min_o.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2x2km.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/3km_cd_o.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/3x250m.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/EUBoatCoach.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/EmpowerSpeedCoachForce.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/NKEmporfromgreg.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/NKLiNKv130.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/NoHR.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/PainsledForce.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/RP_interval.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/RP_interval.csv_o.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/RP_testdata.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpdCoach2_imp_inconsistent.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpdCoachAmbiguous.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach GPS Workout.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach2Link_interval.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach2Linkv1.27.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach2v2.12.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/Speedcoach2example.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/aritmo.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/bc1.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/bc2.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/bc3.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/boatcoach.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/boatcoach_fixed_distance.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/boatcoach_otw.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/correctedpainsled.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/cottwich.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/coxmate.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerd_interval.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerddata.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerddata.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerddata_o.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/cumvalues.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergdata_example.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergdata_example.csv_o.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick.csv_o.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick2.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick_o.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/example.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/example_data.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/faketcx.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/float.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/herodata.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/humon.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/impeller_empower.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/invalidchar.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/kinomap.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/mystery.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/nk_logbook.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled_desktop_example.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled_out_data.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled_to_csv_20160221-105218_erg-400150318_760465m.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_left.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_new.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_right.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_seat.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/recover.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ritmointervals.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowingdata_eth.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowinginmotionexample.TCX +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowinginmotionexample.TCX_o.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowperfect.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowpro5.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowpro_carrick.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp3_curve.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp3intervals.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp3intervals2.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp_out.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/smartrow.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/smartrow_intervals.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/spdcoach2noheader.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoach2test2.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoach3test3.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoachexample.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoachexample.csv_o.CSV +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/strava_export.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/summarytest.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdata.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdata_part1.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdata_part2.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdatasummary.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testlapidx.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testtcs_210614.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testtcx_210614.tcx +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/tim.csv +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata//345/210/222/350/210/271.csv" +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_fit_transcode.py +0 -0
- {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/testparser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rowingdata
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.2
|
|
4
4
|
Summary: The rowingdata library to create colorful plots from CrewNerd, Painsled and other rowing data tools
|
|
5
5
|
Home-page: https://github.com/sanderroosendaal/rowingdata
|
|
6
6
|
Author: Sander Roosendaal
|
|
@@ -22,13 +22,15 @@ Requires-Dist: docopt
|
|
|
22
22
|
Requires-Dist: tqdm
|
|
23
23
|
Requires-Dist: rowingphysics>=0.2.3
|
|
24
24
|
Requires-Dist: iso8601
|
|
25
|
-
Requires-Dist: lxml
|
|
25
|
+
Requires-Dist: lxml>=6.1.0
|
|
26
26
|
Requires-Dist: xmltodict
|
|
27
27
|
Requires-Dist: nose_parameterized
|
|
28
28
|
Requires-Dist: timezonefinder
|
|
29
29
|
Requires-Dist: pycairo
|
|
30
30
|
Requires-Dist: tk
|
|
31
31
|
Requires-Dist: requests
|
|
32
|
+
Requires-Dist: polars
|
|
33
|
+
Requires-Dist: pytz
|
|
32
34
|
|
|
33
35
|
**************
|
|
34
36
|
Rowingdata
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
3
|
-
"
|
|
2
|
+
"version": 3,
|
|
3
|
+
"protocol_version": 1,
|
|
4
|
+
"handle_force_curve_field_id": 60,
|
|
5
|
+
"_comment": "Authoritative FIT developer field definitions for rowingdata fitwrite. Field IDs, scales and units must match the Rowing Data Standard registry at https://github.com/MoveLab-Studio/rowing-data-standard (registry/field-ids.md); that repository is the source of truth, this file is only rowingdata's machine-readable copy of the subset it emits. v2: mm precision for DriveLength/PeakForcePositionAbs (scale 1000); StrokeRate dev field (93); instroke curve Y-scale registry. v3: standard draft 0.1: WorkoutState holds FIT intensity values; StrokeState (96); SlipThreshold/WashThreshold (94/95); HandleForceCurve fixed at 60; fields 4, 5 and 93 are not in the standard and are read-only (group legacy).",
|
|
4
6
|
"instroke_dynamic": {
|
|
5
7
|
"summary_start": 20,
|
|
6
8
|
"summary_id_padding": "ceil_to_10",
|
|
@@ -48,15 +50,18 @@
|
|
|
48
50
|
{"field_id": 1, "group": "rowing", "fit_name": "StrokeDriveTime", "base_type": "UINT16", "size": 2, "scale": 1, "units": "ms", "df_columns": [" DriveTime (ms)"]},
|
|
49
51
|
{"field_id": 2, "group": "rowing", "fit_name": "DragFactor", "base_type": "UINT16", "size": 2, "scale": 1, "units": "", "df_columns": [" DragFactor"]},
|
|
50
52
|
{"field_id": 3, "group": "rowing", "fit_name": "StrokeRecoveryTime", "base_type": "UINT16", "size": 2, "scale": 1, "units": "ms", "df_columns": [" StrokeRecoveryTime (ms)"]},
|
|
51
|
-
{"field_id": 4, "group": "
|
|
52
|
-
{"field_id": 5, "group": "
|
|
53
|
+
{"field_id": 4, "group": "legacy", "fit_name": "AverageDriveForceLbs", "base_type": "UINT16", "size": 2, "scale": 10, "units": "lbs", "df_columns": [" AverageDriveForce (lbs)"]},
|
|
54
|
+
{"field_id": 5, "group": "legacy", "fit_name": "PeakDriveForceLbs", "base_type": "UINT16", "size": 2, "scale": 10, "units": "lbs", "df_columns": [" PeakDriveForce (lbs)"]},
|
|
53
55
|
{"field_id": 6, "group": "rowing", "fit_name": "AverageDriveForceN", "base_type": "UINT16", "size": 2, "scale": 10, "units": "N", "df_columns": [" AverageDriveForce (N)"]},
|
|
54
56
|
{"field_id": 7, "group": "rowing", "fit_name": "PeakDriveForceN", "base_type": "UINT16", "size": 2, "scale": 10, "units": "N", "df_columns": [" PeakDriveForce (N)"]},
|
|
55
57
|
{"field_id": 8, "group": "rowing", "fit_name": "AverageBoatSpeed", "base_type": "UINT16", "size": 2, "scale": 255, "units": "m/s", "df_columns": [" AverageBoatSpeed (m/s)"]},
|
|
56
58
|
{"field_id": 9, "group": "rowing", "fit_name": "WorkoutState", "base_type": "UINT8", "size": 1, "scale": 1, "units": "", "df_columns": [" WorkoutState"]},
|
|
57
59
|
{"field_id": 10, "group": "metadata", "fit_name": "RecordingStrategy", "base_type": "UINT8", "size": 1, "scale": 1, "units": "", "df_columns": [], "message_type": "session"},
|
|
60
|
+
{"field_id": 94, "group": "metadata", "fit_name": "SlipThreshold", "base_type": "UINT16", "size": 2, "scale": 1, "units": "N", "df_columns": [], "message_type": "session"},
|
|
61
|
+
{"field_id": 95, "group": "metadata", "fit_name": "WashThreshold", "base_type": "UINT16", "size": 2, "scale": 1, "units": "N", "df_columns": [], "message_type": "session"},
|
|
58
62
|
{"field_id": 19, "group": "rowing", "fit_name": "StrokeWork", "base_type": "UINT16", "size": 2, "scale": 1, "units": "J", "df_columns": [" WorkPerStroke (joules)", "driveenergy"]},
|
|
59
|
-
{"field_id":
|
|
63
|
+
{"field_id": 96, "group": "rowing", "fit_name": "StrokeState", "base_type": "UINT8", "size": 1, "scale": 1, "units": "", "df_columns": [" StrokeState"]},
|
|
64
|
+
{"field_id": 93, "group": "legacy", "fit_name": "StrokeRate", "base_type": "UINT16", "size": 2, "scale": 100, "units": "spm", "df_columns": [" Cadence (stokes/min)"]},
|
|
60
65
|
|
|
61
66
|
{"field_id": 11, "group": "oarlock", "fit_name": "Catch", "base_type": "SINT16", "size": 2, "scale": 10, "units": "deg", "df_columns": ["catch", " catch", "catchAngle"]},
|
|
62
67
|
{"field_id": 12, "group": "oarlock", "fit_name": "Finish", "base_type": "SINT16", "size": 2, "scale": 10, "units": "deg", "df_columns": ["finish", " finish", "finishAngle"]},
|
|
@@ -32,7 +32,7 @@ try:
|
|
|
32
32
|
from fit_tool.profile.messages.event_message import EventMessage
|
|
33
33
|
from fit_tool.profile.profile_type import (
|
|
34
34
|
FileType, Manufacturer, Sport, SubSport,
|
|
35
|
-
Event, EventType, Activity,
|
|
35
|
+
Event, EventType, Activity, Intensity,
|
|
36
36
|
)
|
|
37
37
|
FIT_TOOL_AVAILABLE = True
|
|
38
38
|
except ImportError:
|
|
@@ -56,6 +56,28 @@ INSTROKE_MAX_POINTS = 127
|
|
|
56
56
|
RECORDING_STRATEGY_UNKNOWN = 0
|
|
57
57
|
RECORDING_STRATEGY_STROKE_BOUNDARY = 1
|
|
58
58
|
RECORDING_STRATEGY_GPS_UPDATE = 2
|
|
59
|
+
RECORDING_STRATEGY_TIME_SAMPLED = 3
|
|
60
|
+
|
|
61
|
+
# Protocol version (standard §1.5): written in application_version of the
|
|
62
|
+
# DeveloperDataId message that carries the standard's application ID.
|
|
63
|
+
PROTOCOL_VERSION = int(_FIT_EXPORT_RAW.get('protocol_version', 1))
|
|
64
|
+
|
|
65
|
+
# Field IDs of session-level fields and of the one curve the standard puts in FIT
|
|
66
|
+
RECORDING_STRATEGY_FIELD_ID = 10
|
|
67
|
+
SLIP_THRESHOLD_FIELD_ID = 94
|
|
68
|
+
WASH_THRESHOLD_FIELD_ID = 95
|
|
69
|
+
STROKE_STATE_FIELD_ID = 96
|
|
70
|
+
HANDLE_FORCE_CURVE_FIELD_ID = int(_FIT_EXPORT_RAW.get('handle_force_curve_field_id', 60))
|
|
71
|
+
HANDLE_FORCE_CURVE_NAME = 'HandleForceCurve'
|
|
72
|
+
|
|
73
|
+
# WorkoutState (field 9) holds the values of the native FIT ``intensity`` enum (§5.1)
|
|
74
|
+
INTENSITY_ACTIVE = 0
|
|
75
|
+
INTENSITY_REST = 1
|
|
76
|
+
INTENSITY_WARMUP = 2
|
|
77
|
+
INTENSITY_COOLDOWN = 3
|
|
78
|
+
INTENSITY_RECOVERY = 4
|
|
79
|
+
INTENSITY_INTERVAL = 5
|
|
80
|
+
INTENSITY_OTHER = 6
|
|
59
81
|
|
|
60
82
|
# Developer fields that must be emitted even when value is 0 (semantic zero / unknown).
|
|
61
83
|
ALWAYS_EMIT_DEV_FIELD_IDS = frozenset(_FIT_EXPORT_RAW['always_emit_field_ids'])
|
|
@@ -64,10 +86,11 @@ ALWAYS_EMIT_DEV_FIELD_IDS = frozenset(_FIT_EXPORT_RAW['always_emit_field_ids'])
|
|
|
64
86
|
# UUID v5 generated from DNS namespace with name "rowingdata" for deterministic, unique ID.
|
|
65
87
|
ROWINGDATA_APP_ID = uuid.uuid5(uuid.NAMESPACE_DNS, 'rowingdata').bytes
|
|
66
88
|
|
|
67
|
-
# In-stroke curve summary (20-59)
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# under ROWINGDATA_APP_ID
|
|
89
|
+
# In-stroke curve summary fields (20-59) are unallocated in the registry, so
|
|
90
|
+
# they travel under a private application ID and cannot collide with a future
|
|
91
|
+
# committee allocation. HandleForceCurve (60) and axis metadata 90-92 are
|
|
92
|
+
# assigned and stay under ROWINGDATA_APP_ID; other curves go to the companion
|
|
93
|
+
# JSON file (standard §6.1).
|
|
71
94
|
INSTROKE_APP_ID = uuid.uuid5(uuid.NAMESPACE_DNS, 'rowingdata.instroke').bytes
|
|
72
95
|
INSTROKE_DEV_DATA_IDX = 1
|
|
73
96
|
|
|
@@ -75,8 +98,6 @@ INSTROKE_DEV_DATA_IDX = 1
|
|
|
75
98
|
INSTROKE_COLUMN_MAP = dict(_FIT_EXPORT_RAW['instroke_column_map'])
|
|
76
99
|
INSTROKE_CURVE_TYPES = dict(_FIT_EXPORT_RAW.get('instroke_curve_types') or {})
|
|
77
100
|
|
|
78
|
-
# Native cadence fractional part scale (Garmin FIT profile: fractional_cadence scale 1/128 spm)
|
|
79
|
-
CADENCE_FRACTIONAL_SCALE = 128
|
|
80
101
|
|
|
81
102
|
def _parse_instroke_curve(df, col):
|
|
82
103
|
"""Parse curve column to DataFrame of numeric values. Same format as rowingdata get_instroke_data."""
|
|
@@ -266,24 +287,38 @@ def _values_for_fit_field(arr, units, scale, base_type):
|
|
|
266
287
|
|
|
267
288
|
def _split_cadence_arrays(df, nr_rows):
|
|
268
289
|
"""
|
|
269
|
-
Return (
|
|
270
|
-
|
|
290
|
+
Return (cadence, cadence256, stroke_rate_spm) per row.
|
|
291
|
+
|
|
292
|
+
``cadence`` is the stroke rate rounded half up to an integer spm (native
|
|
293
|
+
``cadence``); ``cadence256`` is the fractional rate in spm for the native
|
|
294
|
+
``cadence256`` field (scale 256). Standard §4.
|
|
271
295
|
"""
|
|
272
296
|
try:
|
|
273
297
|
raw = pd.to_numeric(df[' Cadence (stokes/min)'].values, errors='coerce')
|
|
274
298
|
raw = np.nan_to_num(raw, nan=0.0, posinf=0.0, neginf=0.0)
|
|
275
299
|
except KeyError:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
300
|
+
return (np.zeros(nr_rows, dtype=int), np.zeros(nr_rows, dtype=np.float64),
|
|
301
|
+
np.zeros(nr_rows, dtype=np.float64))
|
|
302
|
+
raw = np.where(raw > 0, raw, 0.0)
|
|
303
|
+
cadence_int = np.floor(raw + 0.5).astype(int)
|
|
304
|
+
cadence256 = np.clip(raw, 0.0, 65535.0 / 256.0)
|
|
305
|
+
return cadence_int, cadence256, raw
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _workout_state_to_intensity(values):
|
|
309
|
+
"""
|
|
310
|
+
Map rowingdata ``WorkoutState`` values (Concept2 PM codes) to FIT ``intensity``.
|
|
311
|
+
|
|
312
|
+
1 (rowing) -> Active; 3 (interval rest), 0 (waiting) and 2 (countdown pause)
|
|
313
|
+
-> Rest; 4-9 (interval work states) -> Interval; anything else -> Other.
|
|
314
|
+
"""
|
|
315
|
+
arr = np.nan_to_num(np.asarray(values, dtype=np.float64), nan=1.0, posinf=1.0, neginf=1.0)
|
|
316
|
+
arr = arr.astype(int)
|
|
317
|
+
out = np.full(arr.shape, INTENSITY_OTHER, dtype=int)
|
|
318
|
+
out[arr == 1] = INTENSITY_ACTIVE
|
|
319
|
+
out[np.isin(arr, (0, 2, 3))] = INTENSITY_REST
|
|
320
|
+
out[np.isin(arr, (4, 5, 6, 7, 8, 9))] = INTENSITY_INTERVAL
|
|
321
|
+
return out
|
|
287
322
|
|
|
288
323
|
|
|
289
324
|
def _default_instroke_abscissa_type(df, curve_canonical_names):
|
|
@@ -398,11 +433,11 @@ MIN_FIT_LAP_ELAPSED_S = 1e-3
|
|
|
398
433
|
|
|
399
434
|
|
|
400
435
|
def _compute_interval_summaries(df, lap_col, unixtimes, distance_m, heart_rate, stroke_rate_spm,
|
|
401
|
-
power, work_mask=None):
|
|
436
|
+
power, work_mask=None, intensity=None):
|
|
402
437
|
"""
|
|
403
438
|
Compute per-interval summary stats for Lap messages.
|
|
404
439
|
Returns list of dicts with keys: start_time_ms, total_elapsed_s, total_distance,
|
|
405
|
-
total_calories, avg_heart_rate, max_heart_rate, avg_cadence, avg_power, indices.
|
|
440
|
+
total_calories, avg_heart_rate, max_heart_rate, avg_cadence, avg_power, intensity, indices.
|
|
406
441
|
Uses work strokes only for avg HR, cadence, power (matches intervalstats).
|
|
407
442
|
|
|
408
443
|
Per Garmin FIT semantics, Lap **total_elapsed_time** / **total_timer_time** use **wall-clock**
|
|
@@ -487,6 +522,7 @@ def _compute_interval_summaries(df, lap_col, unixtimes, distance_m, heart_rate,
|
|
|
487
522
|
'max_heart_rate': max_hr,
|
|
488
523
|
'avg_cadence': avg_cad,
|
|
489
524
|
'avg_power': avg_pw,
|
|
525
|
+
'intensity': int(intensity[indices[0]]) if intensity is not None else INTENSITY_ACTIVE,
|
|
490
526
|
'indices': indices,
|
|
491
527
|
})
|
|
492
528
|
|
|
@@ -517,7 +553,8 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
517
553
|
instroke_export='off', instroke_columns=None, instroke_column_map=None,
|
|
518
554
|
instroke_downsample_points=16, overwrite=True,
|
|
519
555
|
instroke_abscissa_type=None, instroke_sample_interval_ms=None,
|
|
520
|
-
garmin_parity_source_fit=None, recording_strategy=RECORDING_STRATEGY_STROKE_BOUNDARY
|
|
556
|
+
garmin_parity_source_fit=None, recording_strategy=RECORDING_STRATEGY_STROKE_BOUNDARY,
|
|
557
|
+
slip_threshold=None, wash_threshold=None):
|
|
521
558
|
"""
|
|
522
559
|
Write rowingdata DataFrame to a FIT activity file.
|
|
523
560
|
|
|
@@ -571,10 +608,17 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
571
608
|
Per-stroke data still comes from ``df`` and rowingdata developer field definitions.
|
|
572
609
|
recording_strategy : int
|
|
573
610
|
Recording strategy indicator (RecordingStrategy developer field, ID 10).
|
|
574
|
-
Use constants RECORDING_STRATEGY_* (0=unknown, 1=stroke-boundary, 2=gps-update
|
|
611
|
+
Use constants RECORDING_STRATEGY_* (0=unknown, 1=stroke-boundary, 2=gps-update,
|
|
612
|
+
3=time-sampled). Time-sampled files may carry the StrokeState field (96) from a
|
|
613
|
+
`` StrokeState`` column; with other strategies that column is not written.
|
|
575
614
|
Default: RECORDING_STRATEGY_STROKE_BOUNDARY (1). Omitted from export when 0 (unknown)
|
|
576
615
|
or when use_developer_fields=False. Note: in-stroke curve data requires stroke-boundary.
|
|
577
616
|
See docs/FIT_EXPORT.md "Record message frequency".
|
|
617
|
+
slip_threshold, wash_threshold : int or None
|
|
618
|
+
Handle force in N above/below which the blade counts as entered/exited (Session
|
|
619
|
+
fields SlipThreshold 94 / WashThreshold 95, standard §5.4). Written when given
|
|
620
|
+
and use_developer_fields is True; the standard recommends writing them whenever
|
|
621
|
+
Slip, Wash or EffectiveLength are written.
|
|
578
622
|
|
|
579
623
|
Returns
|
|
580
624
|
-------
|
|
@@ -593,10 +637,20 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
593
637
|
curve_cols_for_export = (
|
|
594
638
|
instroke_columns if instroke_columns is not None else detected_instroke_cols
|
|
595
639
|
)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
640
|
+
# The standard puts only HandleForceCurve in the FIT file (§6.1); every other
|
|
641
|
+
# curve goes to the companion JSON file.
|
|
642
|
+
_early_col_map = instroke_column_map if instroke_column_map is not None else INSTROKE_COLUMN_MAP
|
|
643
|
+
_existing_curve_cols = [c for c in curve_cols_for_export if c in df.columns]
|
|
644
|
+
if instroke_export == 'companion':
|
|
645
|
+
companion_curve_cols = list(_existing_curve_cols)
|
|
646
|
+
elif instroke_export in ('downsampled', 'full'):
|
|
647
|
+
companion_curve_cols = [
|
|
648
|
+
c for c in _existing_curve_cols
|
|
649
|
+
if _early_col_map.get(c, c) != HANDLE_FORCE_CURVE_NAME
|
|
650
|
+
]
|
|
651
|
+
else:
|
|
652
|
+
companion_curve_cols = []
|
|
653
|
+
instroke_would_write_companion = bool(companion_curve_cols)
|
|
600
654
|
companion_path = None
|
|
601
655
|
if instroke_would_write_companion:
|
|
602
656
|
base, _ = os.path.splitext(file_name)
|
|
@@ -646,12 +700,7 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
646
700
|
except KeyError:
|
|
647
701
|
distance_m = df[' Horizontal (meters)'].values
|
|
648
702
|
|
|
649
|
-
|
|
650
|
-
cadence, _fractional_encoded, fractional_cadence, stroke_rate_spm = _split_cadence_arrays(df, nr_rows)
|
|
651
|
-
except KeyError:
|
|
652
|
-
cadence = np.zeros(nr_rows, dtype=int)
|
|
653
|
-
fractional_cadence = np.zeros(nr_rows, dtype=np.float64)
|
|
654
|
-
stroke_rate_spm = np.zeros(nr_rows, dtype=np.float64)
|
|
703
|
+
cadence, cadence256, stroke_rate_spm = _split_cadence_arrays(df, nr_rows)
|
|
655
704
|
|
|
656
705
|
try:
|
|
657
706
|
heart_rate = df[' HRCur (bpm)'].values.astype(int)
|
|
@@ -701,6 +750,13 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
701
750
|
except KeyError:
|
|
702
751
|
stroke_number = np.arange(1, nr_rows + 1, dtype=int) # 1-based row index
|
|
703
752
|
|
|
753
|
+
# The standard defines drive force in N only (fields 6, 7); convert lbs columns
|
|
754
|
+
for _lbs, _n in ((' AverageDriveForce (lbs)', ' AverageDriveForce (N)'),
|
|
755
|
+
(' PeakDriveForce (lbs)', ' PeakDriveForce (N)')):
|
|
756
|
+
if _lbs in df.columns and _n not in df.columns:
|
|
757
|
+
df = df.copy()
|
|
758
|
+
df[_n] = pd.to_numeric(df[_lbs], errors='coerce') * 4.4482216
|
|
759
|
+
|
|
704
760
|
# Developer fields: which columns exist and their arrays (definitions from fit_export_spec.json)
|
|
705
761
|
use_dev = use_developer_fields and FIT_TOOL_AVAILABLE
|
|
706
762
|
dev_arrays = {}
|
|
@@ -724,10 +780,14 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
724
780
|
field_id, possible_cols, name, base_type, size, scale, units = fd
|
|
725
781
|
if not isinstance(possible_cols, (list, tuple)):
|
|
726
782
|
possible_cols = [possible_cols]
|
|
783
|
+
if field_id == STROKE_STATE_FIELD_ID and recording_strategy != RECORDING_STRATEGY_TIME_SAMPLED:
|
|
784
|
+
continue # §5.1: StrokeState is meaningful only for time-sampled files
|
|
727
785
|
col = next((c for c in possible_cols if c in df.columns), None)
|
|
728
786
|
if col is not None:
|
|
729
787
|
arr = df[col].values
|
|
730
788
|
arr = _values_for_fit_field(arr, units, scale, base_type)
|
|
789
|
+
if field_id == 9:
|
|
790
|
+
arr = _workout_state_to_intensity(arr)
|
|
731
791
|
dev_arrays[field_id] = arr
|
|
732
792
|
dev_specs.append((field_id, col, name, base_type, size, scale, units))
|
|
733
793
|
for fd in OARLOCK_DEV_FIELDS:
|
|
@@ -795,9 +855,11 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
795
855
|
curve_cols = instroke_columns if instroke_columns is not None else _detect_instroke_columns(df)
|
|
796
856
|
instroke_curve_cols = [c for c in curve_cols if c in df.columns]
|
|
797
857
|
# Validate: in-stroke data requires stroke-boundary recording
|
|
798
|
-
if instroke_curve_cols and recording_strategy
|
|
858
|
+
if instroke_curve_cols and recording_strategy in (
|
|
859
|
+
RECORDING_STRATEGY_GPS_UPDATE, RECORDING_STRATEGY_TIME_SAMPLED):
|
|
799
860
|
raise ValueError(
|
|
800
|
-
"In-stroke curve data cannot be exported with recording_strategy=RECORDING_STRATEGY_GPS_UPDATE
|
|
861
|
+
"In-stroke curve data cannot be exported with recording_strategy=RECORDING_STRATEGY_GPS_UPDATE "
|
|
862
|
+
"or RECORDING_STRATEGY_TIME_SAMPLED. "
|
|
801
863
|
"Curves are inherently per-stroke and require stroke-boundary recording. "
|
|
802
864
|
"Use recording_strategy=RECORDING_STRATEGY_STROKE_BOUNDARY (default) or RECORDING_STRATEGY_UNKNOWN."
|
|
803
865
|
)
|
|
@@ -823,9 +885,13 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
823
885
|
base_id += 1
|
|
824
886
|
base_id = (base_id // 10 + 1) * 10
|
|
825
887
|
elif instroke_export in ('downsampled', 'full') and instroke_curve_cols and use_dev:
|
|
826
|
-
base_id = _FIT_EXPORT_RAW['instroke_dynamic']['curve_start']
|
|
827
888
|
for col in instroke_curve_cols:
|
|
828
889
|
canonical = col_map.get(col, col)
|
|
890
|
+
if canonical != HANDLE_FORCE_CURVE_NAME:
|
|
891
|
+
continue # companion JSON file (§6.1)
|
|
892
|
+
base_id = HANDLE_FORCE_CURVE_FIELD_ID
|
|
893
|
+
if base_id in dev_arrays:
|
|
894
|
+
continue
|
|
829
895
|
curve_meta = INSTROKE_CURVE_TYPES.get(canonical) or {}
|
|
830
896
|
y_scale = int(curve_meta.get('y_scale', 1))
|
|
831
897
|
y_units = curve_meta.get('y_units', '') or ''
|
|
@@ -841,13 +907,12 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
841
907
|
size = n_points * 2
|
|
842
908
|
dev_arrays[base_id] = arr_2d
|
|
843
909
|
dev_specs.append((base_id, col, canonical, BaseType.UINT16, size, y_scale, y_units))
|
|
844
|
-
instroke_private_field_ids.add(base_id)
|
|
845
910
|
instroke_downsampled_arrays[col] = base_id
|
|
846
|
-
base_id += 1
|
|
847
911
|
|
|
848
|
-
if instroke_export in ('downsampled', 'full') and
|
|
912
|
+
if instroke_export in ('downsampled', 'full') and instroke_downsampled_arrays and use_dev:
|
|
913
|
+
fit_curve_cols = list(instroke_downsampled_arrays)
|
|
849
914
|
nmax = 0
|
|
850
|
-
for col in
|
|
915
|
+
for col in fit_curve_cols:
|
|
851
916
|
try:
|
|
852
917
|
_, np_ = _get_instroke_curve_for_export(
|
|
853
918
|
df, col, instroke_export, instroke_downsample_points
|
|
@@ -858,7 +923,7 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
858
923
|
if nmax >= 2:
|
|
859
924
|
axis_abscissa = instroke_abscissa_type
|
|
860
925
|
if axis_abscissa is None:
|
|
861
|
-
canonicals = [col_map.get(c, c) for c in
|
|
926
|
+
canonicals = [col_map.get(c, c) for c in fit_curve_cols]
|
|
862
927
|
axis_abscissa = _default_instroke_abscissa_type(df, canonicals)
|
|
863
928
|
axis_tuples = _compute_instroke_axis_arrays(
|
|
864
929
|
df, nmax, axis_abscissa, instroke_sample_interval_ms)
|
|
@@ -923,38 +988,50 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
923
988
|
session_dev_fields = []
|
|
924
989
|
dev_id_emitted = False
|
|
925
990
|
|
|
926
|
-
# Emit developer data ID and field descriptions for session-level fields
|
|
927
|
-
|
|
991
|
+
# Emit developer data ID and field descriptions for session-level fields
|
|
992
|
+
# (RecordingStrategy, SlipThreshold, WashThreshold)
|
|
993
|
+
session_values = []
|
|
994
|
+
if recording_strategy > 0:
|
|
995
|
+
session_values.append((RECORDING_STRATEGY_FIELD_ID, 'RecordingStrategy',
|
|
996
|
+
BaseType.UINT8, 1, int(recording_strategy)))
|
|
997
|
+
if slip_threshold is not None:
|
|
998
|
+
session_values.append((SLIP_THRESHOLD_FIELD_ID, 'SlipThreshold', BaseType.UINT16, 2,
|
|
999
|
+
int(np.clip(round(float(slip_threshold)), 0, 65534))))
|
|
1000
|
+
if wash_threshold is not None:
|
|
1001
|
+
session_values.append((WASH_THRESHOLD_FIELD_ID, 'WashThreshold', BaseType.UINT16, 2,
|
|
1002
|
+
int(np.clip(round(float(wash_threshold)), 0, 65534))))
|
|
1003
|
+
if use_dev and (session_values or dev_specs):
|
|
928
1004
|
dev_id_msg = DeveloperDataIdMessage()
|
|
929
1005
|
dev_id_msg.application_id = ROWINGDATA_APP_ID
|
|
1006
|
+
dev_id_msg.application_version = PROTOCOL_VERSION
|
|
930
1007
|
dev_id_msg.developer_data_index = DEV_DATA_IDX
|
|
931
1008
|
builder.add(dev_id_msg)
|
|
932
1009
|
dev_id_emitted = True
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1010
|
+
|
|
1011
|
+
if use_dev:
|
|
1012
|
+
for field_id, name, base_type, size, value in session_values:
|
|
1013
|
+
fd_msg = FieldDescriptionMessage()
|
|
1014
|
+
fd_msg.developer_data_index = DEV_DATA_IDX
|
|
1015
|
+
fd_msg.field_definition_number = field_id
|
|
1016
|
+
fd_msg.fit_base_type_id = base_type.value
|
|
1017
|
+
fd_msg.field_name = name
|
|
1018
|
+
fd_msg.scale = 1
|
|
1019
|
+
fd_msg.offset = 0
|
|
1020
|
+
fd_msg.units = 'N' if field_id in (SLIP_THRESHOLD_FIELD_ID, WASH_THRESHOLD_FIELD_ID) else ''
|
|
1021
|
+
builder.add(fd_msg)
|
|
1022
|
+
|
|
1023
|
+
session_dev_field = DeveloperField(
|
|
1024
|
+
developer_data_index=DEV_DATA_IDX,
|
|
1025
|
+
field_id=field_id,
|
|
1026
|
+
size=size,
|
|
1027
|
+
name=name,
|
|
1028
|
+
base_type=base_type,
|
|
1029
|
+
scale=1,
|
|
1030
|
+
offset=0,
|
|
1031
|
+
units=fd_msg.units
|
|
1032
|
+
)
|
|
1033
|
+
session_dev_field.set_value(0, value)
|
|
1034
|
+
session_dev_fields.append(session_dev_field)
|
|
958
1035
|
|
|
959
1036
|
# Session message with optional developer fields
|
|
960
1037
|
session = SessionMessage(developer_fields=session_dev_fields) if session_dev_fields else SessionMessage()
|
|
@@ -1006,22 +1083,34 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
1006
1083
|
|
|
1007
1084
|
# Lap column: rowingdata uses ' lapIdx'; some CSVs use 'lapIdx'
|
|
1008
1085
|
lap_col = ' lapIdx' if ' lapIdx' in df.columns else ('lapIdx' if 'lapIdx' in df.columns else None)
|
|
1086
|
+
ws_col = ' WorkoutState' if ' WorkoutState' in df.columns else (
|
|
1087
|
+
'WorkoutState' if 'WorkoutState' in df.columns else None)
|
|
1009
1088
|
work_mask = None
|
|
1010
|
-
|
|
1089
|
+
intensity_arr = np.full(nr_rows, INTENSITY_ACTIVE, dtype=int)
|
|
1090
|
+
if ws_col is not None:
|
|
1011
1091
|
try:
|
|
1012
|
-
ws = df[
|
|
1013
|
-
work_mask = np.isin(ws
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1092
|
+
ws = df[ws_col].values.astype(int)
|
|
1093
|
+
work_mask = np.isin(ws, WORKOUT_STATES_WORK)
|
|
1094
|
+
intensity_arr = _workout_state_to_intensity(ws)
|
|
1095
|
+
except (KeyError, TypeError, ValueError):
|
|
1096
|
+
work_mask = None
|
|
1097
|
+
elif lap_col is not None:
|
|
1098
|
+
work_mask = np.ones(nr_rows, dtype=bool)
|
|
1099
|
+
|
|
1100
|
+
# Laps follow lapIdx and are split wherever the intensity changes, so a lap
|
|
1101
|
+
# with intensity=active never contains rest (standard §4.1).
|
|
1102
|
+
lap_values = df[lap_col].values if lap_col is not None else np.zeros(nr_rows, dtype=int)
|
|
1103
|
+
changed = np.zeros(nr_rows, dtype=bool)
|
|
1104
|
+
if nr_rows > 1:
|
|
1105
|
+
changed[1:] = (lap_values[1:] != lap_values[:-1]) | (intensity_arr[1:] != intensity_arr[:-1])
|
|
1106
|
+
segment_ids = np.cumsum(changed)
|
|
1018
1107
|
interval_summaries = None
|
|
1019
|
-
if
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1108
|
+
if segment_ids[-1] > 0:
|
|
1109
|
+
df = df.assign(_fit_segment=segment_ids)
|
|
1110
|
+
interval_summaries = _compute_interval_summaries(
|
|
1111
|
+
df, '_fit_segment', unixtimes, distance_m, heart_rate, stroke_rate_spm, power,
|
|
1112
|
+
work_mask, intensity_arr
|
|
1113
|
+
)
|
|
1025
1114
|
|
|
1026
1115
|
def _emit_record(i):
|
|
1027
1116
|
"""Emit a single Record message for row index i."""
|
|
@@ -1074,8 +1163,8 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
1074
1163
|
rec.distance = float(distance_m[i])
|
|
1075
1164
|
rec.heart_rate = heart_rate[i] if heart_rate[i] > 0 else None
|
|
1076
1165
|
rec.cadence = cadence[i] if cadence[i] > 0 else None
|
|
1077
|
-
if
|
|
1078
|
-
rec.
|
|
1166
|
+
if cadence256[i] > 0 and hasattr(rec, 'cadence256'):
|
|
1167
|
+
rec.cadence256 = float(cadence256[i]) # fit-tool applies scale 256
|
|
1079
1168
|
rec.power = power[i] if power[i] > 0 else None
|
|
1080
1169
|
rec.enhanced_speed = float(enhanced_speed[i]) if enhanced_speed[i] > 0 else None
|
|
1081
1170
|
if hasattr(rec, 'total_cycles'):
|
|
@@ -1106,6 +1195,7 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
1106
1195
|
lap.total_calories = summ['total_calories']
|
|
1107
1196
|
lap.sport = _sport_to_fit(sport)
|
|
1108
1197
|
lap.sub_sport = _sub_sport_for_sport(sport, has_gps)
|
|
1198
|
+
lap.intensity = Intensity(summ['intensity'])
|
|
1109
1199
|
if summ['avg_heart_rate'] > 0:
|
|
1110
1200
|
lap.avg_heart_rate = summ['avg_heart_rate']
|
|
1111
1201
|
if summ['max_heart_rate'] > 0:
|
|
@@ -1130,6 +1220,7 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
1130
1220
|
lap.total_calories = total_calories
|
|
1131
1221
|
lap.sport = _sport_to_fit(sport)
|
|
1132
1222
|
lap.sub_sport = _sub_sport_for_sport(sport, has_gps)
|
|
1223
|
+
lap.intensity = Intensity(int(intensity_arr[0]))
|
|
1133
1224
|
if avg_hr > 0:
|
|
1134
1225
|
lap.avg_heart_rate = avg_hr
|
|
1135
1226
|
if max_hr > 0:
|
|
@@ -1155,12 +1246,12 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
1155
1246
|
|
|
1156
1247
|
# Companion export: write .instroke.json sidecar when instroke_export='companion'
|
|
1157
1248
|
companion_written_path = None
|
|
1158
|
-
if
|
|
1249
|
+
if companion_curve_cols and instroke_curve_cols:
|
|
1159
1250
|
col_map = instroke_column_map if instroke_column_map is not None else INSTROKE_COLUMN_MAP
|
|
1160
1251
|
base, _ = os.path.splitext(file_name)
|
|
1161
1252
|
companion_path = base + '.instroke.json'
|
|
1162
1253
|
curves = {}
|
|
1163
|
-
for col in
|
|
1254
|
+
for col in companion_curve_cols:
|
|
1164
1255
|
canonical = col_map.get(col, col)
|
|
1165
1256
|
try:
|
|
1166
1257
|
parsed = _parse_instroke_curve(df, col)
|
|
@@ -1179,7 +1270,7 @@ def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdat
|
|
|
1179
1270
|
for row in strokes:
|
|
1180
1271
|
max_pts = max(max_pts, len(row))
|
|
1181
1272
|
max_pts = max(max_pts, 2)
|
|
1182
|
-
canonicals = [col_map.get(c, c) for c in
|
|
1273
|
+
canonicals = [col_map.get(c, c) for c in companion_curve_cols]
|
|
1183
1274
|
axis_abscissa = instroke_abscissa_type
|
|
1184
1275
|
if axis_abscissa is None:
|
|
1185
1276
|
axis_abscissa = _default_instroke_abscissa_type(df, canonicals)
|
|
@@ -78,9 +78,9 @@ def load_fit_spec_raw():
|
|
|
78
78
|
|
|
79
79
|
def _validate_spec(raw):
|
|
80
80
|
version = raw.get('version', 0)
|
|
81
|
-
if version not in (1, 2):
|
|
81
|
+
if version not in (1, 2, 3):
|
|
82
82
|
warnings.warn(
|
|
83
|
-
'fit_export_spec.json version %s may be unsupported (expected 1 or
|
|
83
|
+
'fit_export_spec.json version %s may be unsupported (expected 1, 2 or 3)' % version,
|
|
84
84
|
UserWarning,
|
|
85
85
|
stacklevel=2,
|
|
86
86
|
)
|
|
@@ -144,7 +144,10 @@ def _materialize_dev_field_tuples(raw):
|
|
|
144
144
|
elif group == 'instroke_axis':
|
|
145
145
|
instroke_axis.append((fid, name, bt, size, scale, units))
|
|
146
146
|
elif group == 'metadata':
|
|
147
|
-
#
|
|
147
|
+
# Session-level fields (RecordingStrategy, thresholds) are handled in fitwrite.py
|
|
148
|
+
pass
|
|
149
|
+
elif group == 'legacy':
|
|
150
|
+
# Not in the standard: never written, still read from older rowingdata files
|
|
148
151
|
pass
|
|
149
152
|
else:
|
|
150
153
|
raise ValueError('Unknown developer_fields group: %s' % group)
|
|
@@ -193,6 +196,8 @@ def load_fit_spec():
|
|
|
193
196
|
'instroke_axis_field_ids': tuple(raw['instroke_axis_field_ids']),
|
|
194
197
|
'always_emit_field_ids': frozenset(raw['always_emit_field_ids']),
|
|
195
198
|
'instroke_dynamic': dict(raw['instroke_dynamic']),
|
|
199
|
+
'protocol_version': int(raw.get('protocol_version', 1)),
|
|
200
|
+
'handle_force_curve_field_id': int(raw.get('handle_force_curve_field_id', 60)),
|
|
196
201
|
**mats,
|
|
197
202
|
}
|
|
198
203
|
return _SPEC_CACHE
|