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.
Files changed (204) hide show
  1. {rowingdata-3.8.0/rowingdata.egg-info → rowingdata-3.8.2}/PKG-INFO +4 -2
  2. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/data/fit_export_spec.json +10 -5
  3. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fitwrite.py +178 -87
  4. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fitwrite_spec.py +8 -3
  5. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/otherparsers.py +89 -7
  6. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/rowingdata.py +11 -3
  7. {rowingdata-3.8.0 → rowingdata-3.8.2/rowingdata.egg-info}/PKG-INFO +4 -2
  8. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/requires.txt +3 -1
  9. {rowingdata-3.8.0 → rowingdata-3.8.2}/setup.py +2 -2
  10. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testtcx.tcx +1 -1
  11. {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_fit_instroke_export.py +16 -72
  12. {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_fit_standard.py +100 -12
  13. {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_rowingdata.py +16 -19
  14. {rowingdata-3.8.0 → rowingdata-3.8.2}/LICENSE +0 -0
  15. {rowingdata-3.8.0 → rowingdata-3.8.2}/MANIFEST.in +0 -0
  16. {rowingdata-3.8.0 → rowingdata-3.8.2}/README.rst +0 -0
  17. {rowingdata-3.8.0 → rowingdata-3.8.2}/bin/crewnerddata.csv +0 -0
  18. {rowingdata-3.8.0 → rowingdata-3.8.2}/bin/crewnerddata.tcx +0 -0
  19. {rowingdata-3.8.0 → rowingdata-3.8.2}/bin/testdata.csv +0 -0
  20. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/.buildinfo +0 -0
  21. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/2x20min.png +0 -0
  22. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/editrower.JPG +0 -0
  23. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/greghoc.png +0 -0
  24. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/image001.png +0 -0
  25. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/otwlogbook.JPG +0 -0
  26. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/otwscreenshot.JPG +0 -0
  27. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/screenshot.JPG +0 -0
  28. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/screenshotlogbook.JPG +0 -0
  29. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_images/woensdag.png +0 -0
  30. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_sources/index.txt +0 -0
  31. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_sources/modules.txt +0 -0
  32. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_sources/rowingdata.txt +0 -0
  33. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/ajax-loader.gif +0 -0
  34. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/alabaster.css +0 -0
  35. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/basic.css +0 -0
  36. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/comment-bright.png +0 -0
  37. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/comment-close.png +0 -0
  38. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/comment.png +0 -0
  39. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/custom.css +0 -0
  40. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/doctools.js +0 -0
  41. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/down-pressed.png +0 -0
  42. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/down.png +0 -0
  43. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/file.png +0 -0
  44. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/jquery-1.11.1.js +0 -0
  45. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/jquery.js +0 -0
  46. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/minus.png +0 -0
  47. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/plus.png +0 -0
  48. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/pygments.css +0 -0
  49. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/searchtools.js +0 -0
  50. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/underscore-1.3.1.js +0 -0
  51. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/underscore.js +0 -0
  52. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/up-pressed.png +0 -0
  53. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/up.png +0 -0
  54. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/_static/websupport.js +0 -0
  55. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/genindex.html +0 -0
  56. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/index.html +0 -0
  57. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/modules.html +0 -0
  58. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/objects.inv +0 -0
  59. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/py-modindex.html +0 -0
  60. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/rowingdata.html +0 -0
  61. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/search.html +0 -0
  62. {rowingdata-3.8.0 → rowingdata-3.8.2}/docs/_build/html/searchindex.js +0 -0
  63. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/__init__.py +0 -0
  64. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/__main__.py +0 -0
  65. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/boatedit.py +0 -0
  66. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/checkdatafiles.py +0 -0
  67. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/copystats.py +0 -0
  68. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/crewnerdplot.py +0 -0
  69. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/crewnerdplottime.py +0 -0
  70. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/csvparsers.py +0 -0
  71. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergdataplot.py +0 -0
  72. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergdataplottime.py +0 -0
  73. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergdatatotcx.py +0 -0
  74. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergstickplot.py +0 -0
  75. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergstickplottime.py +0 -0
  76. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/ergsticktotcx.py +0 -0
  77. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fit_garmin_bridge.py +0 -0
  78. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/fit_transcode.py +0 -0
  79. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/gpxtools.py +0 -0
  80. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/gpxwrite.py +0 -0
  81. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/konkatenaadje.py +0 -0
  82. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/laptesting.py +0 -0
  83. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/obsolete.py +0 -0
  84. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsled_desktop_plot.py +0 -0
  85. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsled_desktop_plottime.py +0 -0
  86. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsled_desktop_toc2.py +0 -0
  87. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsledplot.py +0 -0
  88. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsledplottime.py +0 -0
  89. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/painsledtoc2.py +0 -0
  90. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/roweredit.py +0 -0
  91. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/rowproplot.py +0 -0
  92. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/rowproplottime.py +0 -0
  93. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/speedcoachplot.py +0 -0
  94. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/speedcoachplottime.py +0 -0
  95. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/speedcoachtoc2.py +0 -0
  96. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplot.py +0 -0
  97. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplot_nogeo.py +0 -0
  98. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplottime.py +0 -0
  99. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxplottime_nogeo.py +0 -0
  100. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxtoc2.py +0 -0
  101. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/tcxtools.py +0 -0
  102. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/trainingparser.py +0 -0
  103. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/utils.py +0 -0
  104. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/windcorrected.py +0 -0
  105. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata/writetcx.py +0 -0
  106. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/SOURCES.txt +0 -0
  107. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/dependency_links.txt +0 -0
  108. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/not-zip-safe +0 -0
  109. {rowingdata-3.8.0 → rowingdata-3.8.2}/rowingdata.egg-info/top_level.txt +0 -0
  110. {rowingdata-3.8.0 → rowingdata-3.8.2}/setup.cfg +0 -0
  111. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2016-03-25-0758.CSV +0 -0
  112. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2016-03-25-0758_data.CSV +0 -0
  113. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2x20min_o.csv +0 -0
  114. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/2x2km.tcx +0 -0
  115. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/3km_cd_o.csv +0 -0
  116. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/3x250m.tcx +0 -0
  117. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/EUBoatCoach.csv +0 -0
  118. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/EmpowerSpeedCoachForce.csv +0 -0
  119. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/NKEmporfromgreg.csv +0 -0
  120. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/NKLiNKv130.csv +0 -0
  121. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/NoHR.tcx +0 -0
  122. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/PainsledForce.csv +0 -0
  123. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/RP_interval.csv +0 -0
  124. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/RP_interval.csv_o.CSV +0 -0
  125. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/RP_testdata.csv +0 -0
  126. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpdCoach2_imp_inconsistent.csv +0 -0
  127. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpdCoachAmbiguous.csv +0 -0
  128. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach GPS Workout.csv +0 -0
  129. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach2Link_interval.csv +0 -0
  130. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach2Linkv1.27.csv +0 -0
  131. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/SpeedCoach2v2.12.csv +0 -0
  132. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/Speedcoach2example.csv +0 -0
  133. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/aritmo.csv +0 -0
  134. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/bc1.csv +0 -0
  135. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/bc2.csv +0 -0
  136. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/bc3.csv +0 -0
  137. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/boatcoach.csv +0 -0
  138. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/boatcoach_fixed_distance.csv +0 -0
  139. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/boatcoach_otw.csv +0 -0
  140. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/correctedpainsled.csv +0 -0
  141. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/cottwich.csv +0 -0
  142. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/coxmate.csv +0 -0
  143. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerd_interval.csv +0 -0
  144. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerddata.CSV +0 -0
  145. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerddata.tcx +0 -0
  146. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/crewnerddata_o.CSV +0 -0
  147. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/cumvalues.csv +0 -0
  148. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergdata_example.csv +0 -0
  149. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergdata_example.csv_o.CSV +0 -0
  150. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick.csv +0 -0
  151. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick.csv_o.CSV +0 -0
  152. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick2.csv +0 -0
  153. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ergstick_o.csv +0 -0
  154. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/example.csv +0 -0
  155. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/example_data.csv +0 -0
  156. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/faketcx.tcx +0 -0
  157. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/float.csv +0 -0
  158. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/herodata.csv +0 -0
  159. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/humon.csv +0 -0
  160. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/impeller_empower.csv +0 -0
  161. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/invalidchar.tcx +0 -0
  162. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/kinomap.csv +0 -0
  163. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/mystery.csv +0 -0
  164. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/nk_logbook.csv +0 -0
  165. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled.tcx +0 -0
  166. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled_desktop_example.csv +0 -0
  167. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled_out_data.csv +0 -0
  168. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/painsled_to_csv_20160221-105218_erg-400150318_760465m.csv +0 -0
  169. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_left.csv +0 -0
  170. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_new.csv +0 -0
  171. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_right.csv +0 -0
  172. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/quiske_per_stroke_seat.csv +0 -0
  173. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/recover.tcx +0 -0
  174. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/ritmointervals.csv +0 -0
  175. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowingdata_eth.csv +0 -0
  176. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowinginmotionexample.TCX +0 -0
  177. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowinginmotionexample.TCX_o.CSV +0 -0
  178. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowperfect.csv +0 -0
  179. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowpro5.csv +0 -0
  180. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rowpro_carrick.csv +0 -0
  181. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp3_curve.csv +0 -0
  182. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp3intervals.csv +0 -0
  183. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp3intervals2.csv +0 -0
  184. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/rp_out.csv +0 -0
  185. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/smartrow.csv +0 -0
  186. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/smartrow_intervals.csv +0 -0
  187. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/spdcoach2noheader.csv +0 -0
  188. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoach2test2.csv +0 -0
  189. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoach3test3.csv +0 -0
  190. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoachexample.csv +0 -0
  191. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/speedcoachexample.csv_o.CSV +0 -0
  192. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/strava_export.tcx +0 -0
  193. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/summarytest.csv +0 -0
  194. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdata.csv +0 -0
  195. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdata_part1.csv +0 -0
  196. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdata_part2.csv +0 -0
  197. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testdatasummary.csv +0 -0
  198. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testlapidx.csv +0 -0
  199. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testtcs_210614.tcx +0 -0
  200. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/testtcx_210614.tcx +0 -0
  201. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata/tim.csv +0 -0
  202. {rowingdata-3.8.0 → rowingdata-3.8.2}/testdata//345/210/222/350/210/271.csv" +0 -0
  203. {rowingdata-3.8.0 → rowingdata-3.8.2}/tests/test_fit_transcode.py +0 -0
  204. {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.0
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": 2,
3
- "_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.",
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": "rowing", "fit_name": "AverageDriveForceLbs", "base_type": "UINT16", "size": 2, "scale": 10, "units": "lbs", "df_columns": [" AverageDriveForce (lbs)"]},
52
- {"field_id": 5, "group": "rowing", "fit_name": "PeakDriveForceLbs", "base_type": "UINT16", "size": 2, "scale": 10, "units": "lbs", "df_columns": [" PeakDriveForce (lbs)"]},
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": 93, "group": "rowing", "fit_name": "StrokeRate", "base_type": "UINT16", "size": 2, "scale": 100, "units": "spm", "df_columns": [" Cadence (stokes/min)"]},
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) and array (60-89) IDs are unallocated in the
68
- # registry, so they travel under a private application ID and cannot collide
69
- # with a future committee allocation. Axis metadata 90-92 is assigned and stays
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 (integer cadence, fractional_cadence, stroke_rate_spm) per row.
270
- stroke_rate_spm preserves fractional strokes/min for StrokeRate developer field.
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
- z = np.zeros(nr_rows, dtype=int)
277
- return z, z.copy(), np.zeros(nr_rows), np.zeros(nr_rows, dtype=np.float64)
278
- cadence_int = np.floor(raw).astype(int)
279
- frac = raw - np.floor(raw)
280
- fractional = np.round(frac * CADENCE_FRACTIONAL_SCALE).astype(int)
281
- fractional = np.clip(fractional, 0, 255)
282
- cadence_int = np.where(raw > 0, cadence_int, 0)
283
- fractional = np.where(raw > 0, fractional, 0)
284
- # Physical fractional spm (0-1) for fit-tool fractional_cadence field (scale 1/128)
285
- fractional_physical = np.where(raw > 0, frac, 0.0)
286
- return cadence_int, fractional, fractional_physical, raw
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
- instroke_would_write_companion = (
597
- instroke_export == 'companion' and
598
- [c for c in curve_cols_for_export if c in df.columns]
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
- try:
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 == RECORDING_STRATEGY_GPS_UPDATE:
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 instroke_curve_cols and use_dev:
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 instroke_curve_cols:
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 instroke_curve_cols]
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 (RecordingStrategy)
927
- if use_dev and recording_strategy > 0:
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
- # Field description for RecordingStrategy (session-level)
935
- fd_msg = FieldDescriptionMessage()
936
- fd_msg.developer_data_index = DEV_DATA_IDX
937
- fd_msg.field_definition_number = 10
938
- fd_msg.fit_base_type_id = BaseType.UINT8.value
939
- fd_msg.field_name = 'RecordingStrategy'
940
- fd_msg.scale = 1
941
- fd_msg.offset = 0
942
- fd_msg.units = ''
943
- builder.add(fd_msg)
944
-
945
- # Create developer field for Session
946
- session_dev_field = DeveloperField(
947
- developer_data_index=DEV_DATA_IDX,
948
- field_id=10,
949
- size=1,
950
- name='RecordingStrategy',
951
- base_type=BaseType.UINT8,
952
- scale=1,
953
- offset=0,
954
- units=''
955
- )
956
- session_dev_field.set_value(0, int(recording_strategy))
957
- session_dev_fields.append(session_dev_field)
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
- if lap_col is not None:
1089
+ intensity_arr = np.full(nr_rows, INTENSITY_ACTIVE, dtype=int)
1090
+ if ws_col is not None:
1011
1091
  try:
1012
- ws = df[' WorkoutState'].values if ' WorkoutState' in df.columns else df['WorkoutState'].values
1013
- work_mask = np.isin(ws.astype(int), WORKOUT_STATES_WORK)
1014
- except (KeyError, TypeError):
1015
- work_mask = np.ones(nr_rows, dtype=bool)
1016
-
1017
- # Determine if we have multiple intervals (per-interval Lap messages)
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 lap_col is not None:
1020
- unique_laps = np.unique(df[lap_col].values)
1021
- if len(unique_laps) > 1:
1022
- interval_summaries = _compute_interval_summaries(
1023
- df, lap_col, unixtimes, distance_m, heart_rate, stroke_rate_spm, power, work_mask
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 fractional_cadence[i] > 0 and hasattr(rec, 'fractional_cadence'):
1078
- rec.fractional_cadence = float(fractional_cadence[i])
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 instroke_export == 'companion' and instroke_curve_cols:
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 instroke_curve_cols:
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 instroke_curve_cols]
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 2)' % version,
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
- # Metadata fields (e.g., RecordingStrategy) are handled manually in fitwrite.py
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