Core ride logic, FTMS client, FIT encoder and probe CLI
Adds backing state for Resistance and Erg control modes, which had no value to hold and so could never satisfy FR-4.3/FR-4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
//! Profiles shipped with the app, so there is always something to ride and the
|
||||
//! YAML schema (`crates/core/src/profile.rs`) has worked examples.
|
||||
|
||||
use crate::commands::SampleProfile;
|
||||
|
||||
const OVER_UNDERS: &str = r#"name: Over-unders
|
||||
description: Ten minutes up to threshold, then eight over-under cycles, then easy.
|
||||
looping: false
|
||||
blocks:
|
||||
- type: ramp
|
||||
channel: power
|
||||
from: 110
|
||||
to: 210
|
||||
extent: { seconds: 600 }
|
||||
- type: wave
|
||||
channel: power
|
||||
shape: sine
|
||||
midpoint: 245
|
||||
amplitude: 45
|
||||
period: { seconds: 120 }
|
||||
repeats: 8
|
||||
- type: constant
|
||||
channel: power
|
||||
value: 120
|
||||
extent: { seconds: 300 }
|
||||
"#;
|
||||
|
||||
const HILL_REPEATS: &str = r#"name: Hill repeats
|
||||
description: Four kilometres of rolling terrain, looped. Gradient by distance.
|
||||
looping: true
|
||||
blocks:
|
||||
- type: segments
|
||||
segments:
|
||||
- { distance_m: 600, gradient_pct: 1.0 }
|
||||
- { distance_m: 900, gradient_pct: 6.5 }
|
||||
- { distance_m: 300, gradient_pct: 9.0 }
|
||||
- { distance_m: 500, gradient_pct: -3.0 }
|
||||
- { distance_m: 700, gradient_pct: 4.0 }
|
||||
- { distance_m: 1000, gradient_pct: -2.0 }
|
||||
"#;
|
||||
|
||||
const SAWTOOTH_GRADE: &str = r#"name: Sawtooth grade
|
||||
description: A gradient sawtooth for shakedown testing — every 400 m ramps 0 to 8%.
|
||||
looping: true
|
||||
blocks:
|
||||
- type: wave
|
||||
channel: gradient
|
||||
shape: sawtooth
|
||||
midpoint: 4.0
|
||||
amplitude: 4.0
|
||||
period: { metres: 400 }
|
||||
repeats: 20
|
||||
"#;
|
||||
|
||||
const STEADY_ENDURANCE: &str = r#"name: Steady endurance
|
||||
description: Ninety minutes at a fixed grade, with a gentle triangular trim.
|
||||
looping: false
|
||||
blocks:
|
||||
- type: constant
|
||||
channel: gradient
|
||||
value: 2.0
|
||||
extent: { seconds: 900 }
|
||||
- type: wave
|
||||
channel: gradient
|
||||
shape: triangle
|
||||
midpoint: 3.0
|
||||
amplitude: 2.5
|
||||
period: { seconds: 600 }
|
||||
repeats: 7
|
||||
- type: constant
|
||||
channel: gradient
|
||||
value: 0.0
|
||||
extent: { seconds: 600 }
|
||||
"#;
|
||||
|
||||
/// A real GPX, bundled so the route view has something to draw on first run.
|
||||
const SAMPLE_CLIMB_GPX: &str = include_str!("../../testdata/sample-climb.gpx");
|
||||
|
||||
pub fn all() -> Vec<SampleProfile> {
|
||||
let mut out: Vec<SampleProfile> = [OVER_UNDERS, HILL_REPEATS, SAWTOOTH_GRADE, STEADY_ENDURANCE]
|
||||
.iter()
|
||||
.map(|yaml| {
|
||||
let (name, summary) = header(yaml);
|
||||
SampleProfile {
|
||||
name,
|
||||
summary,
|
||||
text: (*yaml).to_string(),
|
||||
is_gpx: false,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
out.insert(
|
||||
0,
|
||||
SampleProfile {
|
||||
name: "Sample climb".into(),
|
||||
summary: "3 km GPX with real GPS elevation noise — smoothed on import.".into(),
|
||||
text: SAMPLE_CLIMB_GPX.to_string(),
|
||||
is_gpx: true,
|
||||
},
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
fn header(yaml: &str) -> (String, String) {
|
||||
let mut name = String::from("Profile");
|
||||
let mut summary = String::new();
|
||||
for line in yaml.lines() {
|
||||
if let Some(rest) = line.strip_prefix("name: ") {
|
||||
name = rest.trim().to_string();
|
||||
} else if let Some(rest) = line.strip_prefix("description: ") {
|
||||
summary = rest.trim().to_string();
|
||||
}
|
||||
}
|
||||
(name, summary)
|
||||
}
|
||||
Reference in New Issue
Block a user