Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions lib/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ extension Concept2DateExtension on DateTime {
}

Uint8List toBytes() {
List<int> bytes = [];
int yearOffset = year - 2000;

// int yearoffset = (year - 2000);
// bytes.add( (day & 0x0F)<<4 | month & 0x0F);
int dateValue = (yearOffset << 9) | (day << 4) | month;

// int day_year_byte = (day & 0x1F) | ;
Uint8List dateBytes =
dateValue.toBytes(fillBytes: 2, endian: Endian.little);

return Uint8List.fromList(bytes);
return Uint8List.fromList([
...dateBytes,
minute,
hour,
]);
}
}

Expand Down
48 changes: 37 additions & 11 deletions lib/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ import 'models/workout.dart';
/// Concept2's structure is date LO, Date HI, Time LO, Time HI
/// see also https://www.c2forum.com/viewtopic.php?f=15&t=200769
DateTime timeFromBytes(Uint8List bytes) {
int date = CsafeIntExtension.fromBytes(bytes.sublist(0, 2));
int date =
CsafeIntExtension.fromBytes(bytes.sublist(0, 2), endian: Endian.little);

int month = date & 0x0F;
int day = (date >> 4) & 0x1F;
int year = (date >> 9) & 0x7f;

int minutes = CsafeIntExtension.fromBytes(bytes.sublist(2, 3));
int hours = CsafeIntExtension.fromBytes(bytes.sublist(3, 4));
int minutes =
CsafeIntExtension.fromBytes(bytes.sublist(2, 3), endian: Endian.little);
int hours =
CsafeIntExtension.fromBytes(bytes.sublist(3, 4), endian: Endian.little);

return DateTime(year + 2000, month, day, hours, minutes);
}
Expand All @@ -44,17 +47,34 @@ WorkoutGoal? guessReasonableSplit(WorkoutGoal goal) {

//https://stackoverflow.com/questions/54852585/how-to-convert-a-duration-like-string-to-a-real-duration-in-flutter
Duration parseDuration(String s) {
final parts = s.split(':');
if (parts.length < 2 || parts.length > 3) {
throw const FormatException('Invalid duration format');
}

final secString = parts.last;
final sec = double.tryParse(secString);
if (sec == null) {
throw const FormatException('Invalid seconds value');
}

final minString = parts[parts.length - 2];
final minutes = int.tryParse(minString);
if (minutes == null) {
throw const FormatException('Invalid minutes value');
}

int hours = 0;
int minutes = 0;
int micros;
List<String> parts = s.split(':');
if (parts.length > 2) {
hours = int.parse(parts[parts.length - 3]);
if (parts.length == 3) {
hours = int.tryParse(parts.first) ??
(throw const FormatException('Invalid hours value'));
}
if (parts.length > 1) {
minutes = int.parse(parts[parts.length - 2]);

if (hours < 0 || minutes < 0 || sec < 0) {
throw RangeError('Duration values must be non-negative');
}
micros = (double.parse(parts[parts.length - 1]) * 1000000).toInt();

final micros = (sec * Duration.microsecondsPerSecond).round();
return Duration(hours: hours, minutes: minutes, microseconds: micros);
}

Expand All @@ -73,6 +93,9 @@ String durationToSplit(Duration d) {
// source: https://www.concept2.com/indoor-rowers/training/calculators/watts-calculator

double splitToWatts(Duration split) {
if (split.inMilliseconds <= 0) {
throw RangeError('split must be positive');
}
double secondsperMeter =
split.inMilliseconds / Duration.millisecondsPerSecond;
var rawWatts = (2.8 / pow(secondsperMeter / 500, 3));
Expand All @@ -82,6 +105,9 @@ double splitToWatts(Duration split) {
// pace = ³√(2.80/watts); pace = seconds per meter
// source: https://www.concept2.com/indoor-rowers/training/calculators/watts-calculator
String wattsToSplit(double watts) {
if (watts < 0) {
throw RangeError('watts must be non-negative');
}
if (watts == 0) {
return "0:00.0";
}
Expand Down
37 changes: 19 additions & 18 deletions test/extensions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ void main() {
group("Concept2DateExtension - ", () {
test('converting a time from bytes', () {
final bytes = Uint8List.fromList([156, 42, 1, 14]); //date 10908
expect(
Concept2DateExtension.fromBytes(bytes), DateTime(2021, 12, 9, 14, 1));
// expect(CsafeIntExtension.fromBytes(bytes, Endian.little), 2147483648);
final date = Concept2DateExtension.fromBytes(bytes);
expect(date, DateTime(2021, 12, 9, 14, 1));
expect(date.toBytes(), bytes);
});

test('converting another time from bytes', () {
final bytes = Uint8List.fromList([241, 45, 1, 14]);
expect(
Concept2DateExtension.fromBytes(bytes), DateTime(2022, 1, 31, 14, 1));
// expect(CsafeIntExtension.fromBytes(bytes, Endian.little), 2147483648);
final date = Concept2DateExtension.fromBytes(bytes);
expect(date, DateTime(2022, 1, 31, 14, 1));
expect(date.toBytes(), bytes);
});

test('converting a date from a verifiable source from bytes', () {
Expand All @@ -25,26 +25,27 @@ void main() {
// 00011101 10010100 00010000 00001001
// final bytes = Uint8List.fromList([29, 148, 16, 9]);
final bytes = Uint8List.fromList([148, 29, 9, 16]);

expect(Concept2DateExtension.fromBytes(bytes),
DateTime(2014, 4, 25, 16, 09));
final date = Concept2DateExtension.fromBytes(bytes);
expect(date, DateTime(2014, 4, 25, 16, 09));
// 25th April 2014 @ 16:09 (4:09pm)
expect(date.toBytes(), bytes);
});

test('converting a date in 2022 from bytes', () {
final bytes = Uint8List.fromList([133, 44, 14, 14]);
expect(
Concept2DateExtension.fromBytes(bytes), DateTime(2022, 5, 8, 14, 14));
final date = Concept2DateExtension.fromBytes(bytes);
expect(date, DateTime(2022, 5, 8, 14, 14));
expect(date.toBytes(), bytes);
});
});

group("Concept2DurationExtension - ", () {
// TODO: need bytes to use as test case
// test('converting a duration from bytes', () {
// // TODO: fixme, my endianness is backwards
// final bytes = Uint8List.fromList([42, 156, 1, 14]); //date 10908
// expect(
// Concept2DurationExtension.fromBytes(bytes), DateTime(2021, 12, 9, 14, 1));
// });
test('converts a duration from bytes and back again', () {
// 1 minute, 30.5 seconds -> 90500 ms -> 9050 hundred of a second
final bytes = Uint8List.fromList([90, 35, 0]);
final duration = Concept2DurationExtension.fromBytes(bytes);
expect(duration, Duration(minutes: 1, seconds: 30, milliseconds: 500));
expect(duration.toBytes(), bytes);
});
});
}
54 changes: 44 additions & 10 deletions test/helpers_test.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'dart:typed_data';

import 'package:c2bluetooth/helpers.dart';
import 'package:c2bluetooth/models/workout.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {


group("guessReasonableSplit - ", () {
test("test with reasonable distance", () {
expect(guessReasonableSplit(WorkoutGoal.meters(2000)),
Expand Down Expand Up @@ -35,10 +35,13 @@ void main() {
test('3:00.2', () {
expect(parseDuration("3:00.2"), Duration(minutes: 3, milliseconds: 200));
});
// TODO: Update errors for splitToWatts()
// test('When invalid split format, should throw error', () {
// expect(() => splitToWatts('0'), throwsFormatException);
// });
test('throws FormatException on malformed string', () {
expect(() => parseDuration('foo'), throwsFormatException);
});

test('throws RangeError on negative values', () {
expect(() => parseDuration('-1:00.0'), throwsRangeError);
});
});

group('splitToWatts -', () {
Expand All @@ -59,10 +62,12 @@ void main() {
var result = splitToWatts(Duration(minutes: 3));
expect(result, 60);
});
// TODO: Update errors for splitToWatts()
// test('When invalid split format, should throw error', () {
// expect(() => splitToWatts('0'), throwsFormatException);
// });
test('throws RangeError when split is non-positive', () {
expect(() => splitToWatts(Duration.zero), throwsRangeError);
});
test('throws RangeError when split is negative', () {
expect(() => splitToWatts(Duration(seconds: -5)), throwsRangeError);
});
});
group('wattsToSplit -', () {
test('When watts is 179, should be 2:05.0', () {
Expand All @@ -85,5 +90,34 @@ void main() {
var result = wattsToSplit(55555);
expect(result, '0:18.5');
});
test('throws RangeError when watts is negative', () {
expect(() => wattsToSplit(-10), throwsRangeError);
});
});
group('timeFromBytes -', () {
test('should convert PM date bytes to DateTime', () {
final bytes = Uint8List.fromList([156, 42, 1, 14]);
expect(timeFromBytes(bytes), DateTime(2021, 12, 9, 14, 1));
});

test('should convert another valid byte sequence', () {
final bytes = Uint8List.fromList([241, 45, 1, 14]);
expect(timeFromBytes(bytes), DateTime(2022, 1, 31, 14, 1));
});
});

group('durationToSplit -', () {
test('converts a 2:05 duration', () {
expect(durationToSplit(Duration(minutes: 2, seconds: 5)), '2:05.0');
});

test('handles seconds only', () {
expect(durationToSplit(Duration(seconds: 30)), '0:30.0');
});

test('handles fractional seconds', () {
expect(
durationToSplit(Duration(minutes: 1, milliseconds: 900)), '1:00.9');
});
});
}