@jjlmoya/utils-drones 1.39.0 → 1.40.0

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -0
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/fpv-drone-lap-timer/audio.ts +160 -0
  8. package/src/tool/fpv-drone-lap-timer/bibliography.astro +6 -0
  9. package/src/tool/fpv-drone-lap-timer/bibliography.ts +14 -0
  10. package/src/tool/fpv-drone-lap-timer/bootstrap.ts +20 -0
  11. package/src/tool/fpv-drone-lap-timer/component.astro +136 -0
  12. package/src/tool/fpv-drone-lap-timer/contract.test.ts +24 -0
  13. package/src/tool/fpv-drone-lap-timer/controller.ts +304 -0
  14. package/src/tool/fpv-drone-lap-timer/dom-views.ts +213 -0
  15. package/src/tool/fpv-drone-lap-timer/entry.ts +27 -0
  16. package/src/tool/fpv-drone-lap-timer/export.ts +41 -0
  17. package/src/tool/fpv-drone-lap-timer/fpv-drone-lap-timer.css +705 -0
  18. package/src/tool/fpv-drone-lap-timer/i18n/de.ts +216 -0
  19. package/src/tool/fpv-drone-lap-timer/i18n/en.ts +216 -0
  20. package/src/tool/fpv-drone-lap-timer/i18n/es.ts +216 -0
  21. package/src/tool/fpv-drone-lap-timer/i18n/fr.ts +216 -0
  22. package/src/tool/fpv-drone-lap-timer/i18n/id.ts +216 -0
  23. package/src/tool/fpv-drone-lap-timer/i18n/it.ts +216 -0
  24. package/src/tool/fpv-drone-lap-timer/i18n/ja.ts +216 -0
  25. package/src/tool/fpv-drone-lap-timer/i18n/ko.ts +216 -0
  26. package/src/tool/fpv-drone-lap-timer/i18n/nl.ts +216 -0
  27. package/src/tool/fpv-drone-lap-timer/i18n/pl.ts +216 -0
  28. package/src/tool/fpv-drone-lap-timer/i18n/pt.ts +216 -0
  29. package/src/tool/fpv-drone-lap-timer/i18n/ru.ts +216 -0
  30. package/src/tool/fpv-drone-lap-timer/i18n/sv.ts +216 -0
  31. package/src/tool/fpv-drone-lap-timer/i18n/tr.ts +216 -0
  32. package/src/tool/fpv-drone-lap-timer/i18n/zh.ts +216 -0
  33. package/src/tool/fpv-drone-lap-timer/index.ts +10 -0
  34. package/src/tool/fpv-drone-lap-timer/logic.test.ts +152 -0
  35. package/src/tool/fpv-drone-lap-timer/logic.ts +228 -0
  36. package/src/tool/fpv-drone-lap-timer/seo.astro +16 -0
  37. package/src/tool/fpv-drone-lap-timer/storage.ts +52 -0
  38. package/src/tool/fpv-drone-lap-timer/ui.ts +58 -0
  39. package/src/tools.ts +3 -0
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ calculateSpeed,
4
+ estimateBatteryDrain,
5
+ computeLapRecords,
6
+ calculateSessionMetrics,
7
+ formatLapTime,
8
+ formatDelta,
9
+ isDebounceLocked,
10
+ PRESETS,
11
+ } from './logic';
12
+
13
+ describe('fpv-drone-lap-timer logic suite', () => {
14
+ describe('calculateSpeed', () => {
15
+ it('returns zero for invalid or non-positive inputs', () => {
16
+ expect(calculateSpeed(0, 10000)).toEqual({ kmh: 0, mph: 0 });
17
+ expect(calculateSpeed(250, 0)).toEqual({ kmh: 0, mph: 0 });
18
+ expect(calculateSpeed(-250, 10000)).toEqual({ kmh: 0, mph: 0 });
19
+ expect(calculateSpeed(250, -5000)).toEqual({ kmh: 0, mph: 0 });
20
+ });
21
+
22
+ it('calculates correct speed for typical 250m track in 15 seconds', () => {
23
+ const speed = calculateSpeed(250, 15000);
24
+ expect(speed.kmh).toBe(60);
25
+ expect(speed.mph).toBe(37.3);
26
+ });
27
+
28
+ it('calculates high-speed sprint accurately', () => {
29
+ const speed = calculateSpeed(400, 12000);
30
+ expect(speed.kmh).toBe(120);
31
+ expect(speed.mph).toBe(74.6);
32
+ });
33
+ });
34
+
35
+ describe('estimateBatteryDrain', () => {
36
+ it('returns zero for non-positive values', () => {
37
+ expect(estimateBatteryDrain(0, 1300)).toBe(0);
38
+ expect(estimateBatteryDrain(15000, 0)).toBe(0);
39
+ expect(estimateBatteryDrain(-15000, 1300)).toBe(0);
40
+ });
41
+
42
+ it('estimates linear drain proportional to total usable flight capacity', () => {
43
+ const drain = estimateBatteryDrain(15000, 1300);
44
+ expect(drain).toBeGreaterThan(0);
45
+ expect(drain).toBeLessThan(1300);
46
+ const drainDoubleTime = estimateBatteryDrain(30000, 1300);
47
+ expect(Math.abs(drainDoubleTime - drain * 2)).toBeLessThanOrEqual(1);
48
+ });
49
+ });
50
+
51
+ describe('computeLapRecords', () => {
52
+ it('returns empty array when no laps recorded', () => {
53
+ expect(computeLapRecords([], 250, 1300)).toEqual([]);
54
+ });
55
+
56
+ it('identifies fastest lap and computes deltas', () => {
57
+ const laps = [18500, 16200, 17100];
58
+ const records = computeLapRecords(laps, 250, 1300);
59
+
60
+ expect(records).toHaveLength(3);
61
+
62
+ expect(records[0]!.lapIndex).toBe(1);
63
+ expect(records[0]!.durationMs).toBe(18500);
64
+ expect(records[0]!.splitTimeMs).toBe(18500);
65
+ expect(records[0]!.isBest).toBe(false);
66
+ expect(records[0]!.deltaBestMs).toBe(2300);
67
+
68
+ expect(records[1]!.lapIndex).toBe(2);
69
+ expect(records[1]!.durationMs).toBe(16200);
70
+ expect(records[1]!.splitTimeMs).toBe(34700);
71
+ expect(records[1]!.isBest).toBe(true);
72
+ expect(records[1]!.deltaBestMs).toBe(0);
73
+
74
+ expect(records[2]!.lapIndex).toBe(3);
75
+ expect(records[2]!.durationMs).toBe(17100);
76
+ expect(records[2]!.splitTimeMs).toBe(51800);
77
+ expect(records[2]!.isBest).toBe(false);
78
+ expect(records[2]!.deltaBestMs).toBe(900);
79
+ });
80
+ });
81
+
82
+ describe('calculateSessionMetrics', () => {
83
+ it('handles empty session correctly', () => {
84
+ const stats = calculateSessionMetrics([], 250, 1300);
85
+ expect(stats.completedLaps).toBe(0);
86
+ expect(stats.totalDurationMs).toBe(0);
87
+ expect(stats.consistencyIndex).toBe(100);
88
+ expect(stats.consistencyRating).toBe('elite');
89
+ expect(stats.batteryRemainingMah).toBe(1300);
90
+ });
91
+
92
+ it('computes metrics for single lap', () => {
93
+ const stats = calculateSessionMetrics([16500], 250, 1300);
94
+ expect(stats.completedLaps).toBe(1);
95
+ expect(stats.fastestLapMs).toBe(16500);
96
+ expect(stats.fastestLapIndex).toBe(1);
97
+ expect(stats.averageLapMs).toBe(16500);
98
+ expect(stats.standardDeviationMs).toBe(0);
99
+ expect(stats.consistencyIndex).toBe(100);
100
+ });
101
+
102
+ it('computes high consistency for close lap times', () => {
103
+ const stats = calculateSessionMetrics([15000, 15100, 15050], 250, 1300);
104
+ expect(stats.consistencyIndex).toBeGreaterThanOrEqual(95);
105
+ expect(stats.consistencyRating).toBe('elite');
106
+ expect(stats.fastestLapMs).toBe(15000);
107
+ expect(stats.fastestLapIndex).toBe(1);
108
+ });
109
+
110
+ it('computes lower consistency for erratic lap times', () => {
111
+ const stats = calculateSessionMetrics([14000, 22000, 15000, 31000], 250, 1300);
112
+ expect(stats.consistencyIndex).toBeLessThan(80);
113
+ expect(stats.consistencyRating).toBe('novice');
114
+ });
115
+ });
116
+
117
+ describe('formatLapTime', () => {
118
+ it('formats millisecond timestamps into mm:ss.mmm', () => {
119
+ expect(formatLapTime(0)).toBe('00:00.000');
120
+ expect(formatLapTime(14320)).toBe('00:14.320');
121
+ expect(formatLapTime(75450)).toBe('01:15.450');
122
+ expect(formatLapTime(-500)).toBe('00:00.000');
123
+ expect(formatLapTime(NaN)).toBe('00:00.000');
124
+ });
125
+ });
126
+
127
+ describe('formatDelta', () => {
128
+ it('formats delta times cleanly', () => {
129
+ expect(formatDelta(0, true)).toBe('BEST');
130
+ expect(formatDelta(0, false)).toBe('±0.000s');
131
+ expect(formatDelta(450, false)).toBe('+0.450s');
132
+ expect(formatDelta(-320, false)).toBe('-0.320s');
133
+ });
134
+ });
135
+
136
+ describe('isDebounceLocked', () => {
137
+ it('prevents accidental triggers before debounce interval', () => {
138
+ expect(isDebounceLocked(0, 1000, 3000)).toBe(false);
139
+ expect(isDebounceLocked(1000, 2500, 3000)).toBe(true);
140
+ expect(isDebounceLocked(1000, 4001, 3000)).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe('PRESETS', () => {
145
+ it('provides valid presets', () => {
146
+ expect(PRESETS.multigp.trackLengthM).toBe(250);
147
+ expect(PRESETS.whoop.trackLengthM).toBe(65);
148
+ expect(PRESETS.sprint.trackLengthM).toBe(400);
149
+ expect(PRESETS.multigp.debounceMs).toBeGreaterThan(0);
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,228 @@
1
+ export interface LapRecord {
2
+ lapIndex: number;
3
+ durationMs: number;
4
+ splitTimeMs: number;
5
+ speedKmh: number;
6
+ speedMph: number;
7
+ batteryUsedMah: number;
8
+ deltaBestMs: number;
9
+ isBest: boolean;
10
+ }
11
+
12
+ export interface SessionMetrics {
13
+ totalDurationMs: number;
14
+ completedLaps: number;
15
+ fastestLapMs: number;
16
+ fastestLapIndex: number;
17
+ averageLapMs: number;
18
+ standardDeviationMs: number;
19
+ consistencyIndex: number;
20
+ consistencyRating: 'elite' | 'pro' | 'club' | 'novice';
21
+ totalBatteryUsedMah: number;
22
+ batteryRemainingMah: number;
23
+ averageSpeedKmh: number;
24
+ averageSpeedMph: number;
25
+ }
26
+
27
+ export interface SessionConfig {
28
+ trackLengthM: number;
29
+ targetLaps: number;
30
+ batteryCapacityMah: number;
31
+ debounceMs: number;
32
+ soundEnabled: boolean;
33
+ }
34
+
35
+ export const DEFAULT_CONFIG: SessionConfig = {
36
+ trackLengthM: 250,
37
+ targetLaps: 3,
38
+ batteryCapacityMah: 1300,
39
+ debounceMs: 3000,
40
+ soundEnabled: true,
41
+ };
42
+
43
+ export const PRESETS: Record<'multigp' | 'whoop' | 'sprint', SessionConfig> = {
44
+ multigp: {
45
+ trackLengthM: 250,
46
+ targetLaps: 3,
47
+ batteryCapacityMah: 1300,
48
+ debounceMs: 3000,
49
+ soundEnabled: true,
50
+ },
51
+ whoop: {
52
+ trackLengthM: 65,
53
+ targetLaps: 5,
54
+ batteryCapacityMah: 300,
55
+ debounceMs: 2000,
56
+ soundEnabled: true,
57
+ },
58
+ sprint: {
59
+ trackLengthM: 400,
60
+ targetLaps: 2,
61
+ batteryCapacityMah: 1550,
62
+ debounceMs: 4000,
63
+ soundEnabled: true,
64
+ },
65
+ };
66
+
67
+ export function calculateSpeed(trackLengthM: number, durationMs: number): { kmh: number; mph: number } {
68
+ if (trackLengthM <= 0 || durationMs <= 0) {
69
+ return { kmh: 0, mph: 0 };
70
+ }
71
+ const metersPerSecond = trackLengthM / (durationMs / 1000);
72
+ const kmh = metersPerSecond * 3.6;
73
+ const mph = kmh * 0.621371;
74
+ return {
75
+ kmh: Math.round(kmh * 10) / 10,
76
+ mph: Math.round(mph * 10) / 10,
77
+ };
78
+ }
79
+
80
+ export function estimateBatteryDrain(
81
+ durationMs: number,
82
+ batteryCapacityMah: number,
83
+ usableCapacityRatio = 0.8,
84
+ assumedFlightTimeMin = 3.5
85
+ ): number {
86
+ if (durationMs <= 0 || batteryCapacityMah <= 0 || assumedFlightTimeMin <= 0) {
87
+ return 0;
88
+ }
89
+ const totalUsableMah = batteryCapacityMah * usableCapacityRatio;
90
+ const totalAvailableMs = assumedFlightTimeMin * 60 * 1000;
91
+ const estimatedDrain = (durationMs / totalAvailableMs) * totalUsableMah;
92
+ return Math.round(estimatedDrain);
93
+ }
94
+
95
+ export function computeLapRecords(
96
+ lapDurationsMs: number[],
97
+ trackLengthM: number,
98
+ batteryCapacityMah: number
99
+ ): LapRecord[] {
100
+ if (lapDurationsMs.length === 0) {
101
+ return [];
102
+ }
103
+
104
+ const validDurations = lapDurationsMs.map((d) => Math.max(0, d));
105
+ const minDuration = Math.min(...validDurations);
106
+ let cumulativeSplitMs = 0;
107
+
108
+ return validDurations.map((durationMs, idx) => {
109
+ cumulativeSplitMs += durationMs;
110
+ const isBest = durationMs === minDuration && durationMs > 0;
111
+ const deltaBestMs = durationMs - minDuration;
112
+ const speed = calculateSpeed(trackLengthM, durationMs);
113
+ const batteryUsedMah = estimateBatteryDrain(durationMs, batteryCapacityMah);
114
+
115
+ return {
116
+ lapIndex: idx + 1,
117
+ durationMs,
118
+ splitTimeMs: cumulativeSplitMs,
119
+ speedKmh: speed.kmh,
120
+ speedMph: speed.mph,
121
+ batteryUsedMah,
122
+ deltaBestMs,
123
+ isBest,
124
+ };
125
+ });
126
+ }
127
+
128
+ function getEmptySessionMetrics(batteryCapacityMah: number): SessionMetrics {
129
+ return {
130
+ totalDurationMs: 0,
131
+ completedLaps: 0,
132
+ fastestLapMs: 0,
133
+ fastestLapIndex: 0,
134
+ averageLapMs: 0,
135
+ standardDeviationMs: 0,
136
+ consistencyIndex: 100,
137
+ consistencyRating: 'elite',
138
+ totalBatteryUsedMah: 0,
139
+ batteryRemainingMah: batteryCapacityMah,
140
+ averageSpeedKmh: 0,
141
+ averageSpeedMph: 0,
142
+ };
143
+ }
144
+
145
+ function calculateConsistency(durations: number[], avgMs: number) {
146
+ const count = durations.length;
147
+ const variance = durations.reduce((sum, d) => sum + Math.pow(d - avgMs, 2), 0) / count;
148
+ const stdDev = Math.round(Math.sqrt(variance));
149
+ const rawScore = count > 1 && avgMs > 0 ? (1 - stdDev / avgMs) * 100 : 100;
150
+ const index = Math.max(0, Math.min(100, Math.round(rawScore)));
151
+ let rating: 'elite' | 'pro' | 'club' | 'novice' = 'novice';
152
+ if (index >= 95) {
153
+ rating = 'elite';
154
+ } else if (index >= 88) {
155
+ rating = 'pro';
156
+ } else if (index >= 78) {
157
+ rating = 'club';
158
+ }
159
+ return { index, rating, stdDev };
160
+ }
161
+
162
+ export function calculateSessionMetrics(
163
+ lapDurationsMs: number[],
164
+ trackLengthM: number,
165
+ batteryCapacityMah: number
166
+ ): SessionMetrics {
167
+ const count = lapDurationsMs.length;
168
+ if (count === 0) return getEmptySessionMetrics(batteryCapacityMah);
169
+
170
+ const valid = lapDurationsMs.map((d) => Math.max(0, d));
171
+ const totalDurationMs = valid.reduce((sum, d) => sum + d, 0);
172
+ const fastestLapMs = Math.min(...valid);
173
+ const averageLapMs = Math.round(totalDurationMs / count);
174
+ const { index, rating, stdDev } = calculateConsistency(valid, averageLapMs);
175
+ const totalBatteryUsedMah = estimateBatteryDrain(totalDurationMs, batteryCapacityMah);
176
+ const avgSpeed = calculateSpeed(trackLengthM, averageLapMs);
177
+
178
+ return {
179
+ totalDurationMs,
180
+ completedLaps: count,
181
+ fastestLapMs,
182
+ fastestLapIndex: valid.indexOf(fastestLapMs) + 1,
183
+ averageLapMs,
184
+ standardDeviationMs: stdDev,
185
+ consistencyIndex: index,
186
+ consistencyRating: rating,
187
+ totalBatteryUsedMah,
188
+ batteryRemainingMah: Math.max(0, batteryCapacityMah - totalBatteryUsedMah),
189
+ averageSpeedKmh: avgSpeed.kmh,
190
+ averageSpeedMph: avgSpeed.mph,
191
+ };
192
+ }
193
+
194
+ export function formatLapTime(ms: number): string {
195
+ if (!Number.isFinite(ms) || ms < 0) {
196
+ return '00:00.000';
197
+ }
198
+ const totalSeconds = Math.floor(ms / 1000);
199
+ const minutes = Math.floor(totalSeconds / 60);
200
+ const seconds = totalSeconds % 60;
201
+ const milliseconds = Math.floor(ms % 1000);
202
+
203
+ const mm = String(minutes).padStart(2, '0');
204
+ const ss = String(seconds).padStart(2, '0');
205
+ const mmm = String(milliseconds).padStart(3, '0');
206
+
207
+ return `${mm}:${ss}.${mmm}`;
208
+ }
209
+
210
+ export function formatDelta(deltaMs: number, isBest = false): string {
211
+ if (isBest) {
212
+ return 'BEST';
213
+ }
214
+ if (!Number.isFinite(deltaMs) || deltaMs === 0) {
215
+ return '±0.000s';
216
+ }
217
+ const sign = deltaMs > 0 ? '+' : '-';
218
+ const seconds = (Math.abs(deltaMs) / 1000).toFixed(3);
219
+ return `${sign}${seconds}s`;
220
+ }
221
+
222
+ export function isDebounceLocked(lastRecordedTimestampMs: number, currentTimestampMs: number, debounceThresholdMs: number): boolean {
223
+ if (lastRecordedTimestampMs <= 0 || currentTimestampMs <= 0) {
224
+ return false;
225
+ }
226
+ const diff = Math.abs(currentTimestampMs - lastRecordedTimestampMs);
227
+ return diff < debounceThresholdMs;
228
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { fpvDroneLapTimer } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = fpvDroneLapTimer.i18n[locale] || fpvDroneLapTimer.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,52 @@
1
+ import type { SessionConfig } from './logic';
2
+ import { DEFAULT_CONFIG } from './logic';
3
+
4
+ const STORAGE_KEY_CONFIG = 'fpv_drone_lap_timer_config_v1';
5
+ const STORAGE_KEY_LAPS = 'fpv_drone_lap_timer_laps_v1';
6
+
7
+ export function loadSessionConfig(): SessionConfig {
8
+ try {
9
+ const raw = localStorage.getItem(STORAGE_KEY_CONFIG);
10
+ if (!raw) {
11
+ return { ...DEFAULT_CONFIG };
12
+ }
13
+ const parsed = JSON.parse(raw);
14
+ return {
15
+ trackLengthM: Number(parsed.trackLengthM) || DEFAULT_CONFIG.trackLengthM,
16
+ targetLaps: Number(parsed.targetLaps) || DEFAULT_CONFIG.targetLaps,
17
+ batteryCapacityMah: Number(parsed.batteryCapacityMah) || DEFAULT_CONFIG.batteryCapacityMah,
18
+ debounceMs: Number(parsed.debounceMs) || DEFAULT_CONFIG.debounceMs,
19
+ soundEnabled: typeof parsed.soundEnabled === 'boolean' ? parsed.soundEnabled : DEFAULT_CONFIG.soundEnabled,
20
+ };
21
+ } catch {
22
+ return { ...DEFAULT_CONFIG };
23
+ }
24
+ }
25
+
26
+ export function saveSessionConfig(config: SessionConfig): void {
27
+ try {
28
+ localStorage.setItem(STORAGE_KEY_CONFIG, JSON.stringify(config));
29
+ } catch {}
30
+ }
31
+
32
+ export function loadSessionLaps(): number[] {
33
+ try {
34
+ const raw = localStorage.getItem(STORAGE_KEY_LAPS);
35
+ if (!raw) {
36
+ return [];
37
+ }
38
+ const parsed = JSON.parse(raw);
39
+ if (!Array.isArray(parsed)) {
40
+ return [];
41
+ }
42
+ return parsed.filter((n) => typeof n === 'number' && Number.isFinite(n) && n > 0);
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+
48
+ export function saveSessionLaps(laps: number[]): void {
49
+ try {
50
+ localStorage.setItem(STORAGE_KEY_LAPS, JSON.stringify(laps));
51
+ } catch {}
52
+ }
@@ -0,0 +1,58 @@
1
+ export interface FpvDroneLapTimerUI {
2
+ [key: string]: string;
3
+ setupHeading: string;
4
+ trackLengthLabel: string;
5
+ trackLengthUnit: string;
6
+ targetLapsLabel: string;
7
+ targetLapsUnit: string;
8
+ batteryCapacityLabel: string;
9
+ batteryCapacityUnit: string;
10
+ soundEnabledLabel: string;
11
+ debounceThresholdLabel: string;
12
+ debounceThresholdUnit: string;
13
+ presetMultiGpLabel: string;
14
+ presetWhoopLabel: string;
15
+ presetSprintLabel: string;
16
+ startCountdownButton: string;
17
+ pauseTimerButton: string;
18
+ resumeTimerButton: string;
19
+ resetTimerButton: string;
20
+ recordLapButton: string;
21
+ spacebarHint: string;
22
+ statusIdle: string;
23
+ statusCountdown: string;
24
+ statusRunning: string;
25
+ statusPaused: string;
26
+ statusFinished: string;
27
+ currentLapHeading: string;
28
+ lapNumberPrefix: string;
29
+ lastLapHeading: string;
30
+ fastestLapHeading: string;
31
+ averageLapHeading: string;
32
+ deltaBestHeading: string;
33
+ consistencyIndexHeading: string;
34
+ estimatedSpeedHeading: string;
35
+ estimatedBatteryHeading: string;
36
+ speedUnitKmh: string;
37
+ speedUnitMph: string;
38
+ batteryUsedUnit: string;
39
+ batteryRemainingUnit: string;
40
+ lapHistoryHeading: string;
41
+ lapColumnHeader: string;
42
+ timeColumnHeader: string;
43
+ splitColumnHeader: string;
44
+ speedColumnHeader: string;
45
+ batteryColumnHeader: string;
46
+ noLapsRecordedNotice: string;
47
+ consistencyRatingElite: string;
48
+ consistencyRatingPro: string;
49
+ consistencyRatingClub: string;
50
+ consistencyRatingNovice: string;
51
+ fastestLapBadge: string;
52
+ sessionSummaryHeading: string;
53
+ totalTimeLabel: string;
54
+ completedLapsLabel: string;
55
+ exportCsvButton: string;
56
+ copySummaryButton: string;
57
+ copiedNotice: string;
58
+ }
package/src/tools.ts CHANGED
@@ -9,6 +9,7 @@ import { FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL } from './tool/fpv-drone-thrust-t
9
9
  import { DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL } from './tool/drone-motor-propeller-calculator/index';
10
10
  import { FPV_DRONE_SPEED_CALCULATOR_TOOL } from './tool/fpv-drone-speed-calculator/index';
11
11
  import { DRONE_MISSION_BATTERY_RESERVE_PLANNER_TOOL } from './tool/drone-mission-battery-reserve-planner/index';
12
+ import { FPV_DRONE_LAP_TIMER_TOOL } from './tool/fpv-drone-lap-timer/index';
12
13
  import type { ToolDefinition } from './types';
13
14
 
14
15
  export const ALL_TOOLS: ToolDefinition[] = [
@@ -22,6 +23,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
22
23
  DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL,
23
24
  FPV_DRONE_SPEED_CALCULATOR_TOOL,
24
25
  DRONE_MISSION_BATTERY_RESERVE_PLANNER_TOOL,
26
+ FPV_DRONE_LAP_TIMER_TOOL,
25
27
  ];
26
28
 
27
29
  export {
@@ -35,5 +37,6 @@ export {
35
37
  DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL,
36
38
  FPV_DRONE_SPEED_CALCULATOR_TOOL,
37
39
  DRONE_MISSION_BATTERY_RESERVE_PLANNER_TOOL,
40
+ FPV_DRONE_LAP_TIMER_TOOL,
38
41
  };
39
42