Skip to content
Merged
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
35 changes: 34 additions & 1 deletion firmware/obc-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,29 @@ impl App {
self.metadata.progress_payload(token)
}

/// The ride on past the end of its day of the record that `token` writes: the executor settles
/// the record with [`RodeOn::settle`](crate::trip::RodeOn::settle) before it writes it.
pub fn trip_progress_rode_on(
&self,
token: crate::device_core::OperationToken<crate::device_core::MetadataTag>,
) -> Option<crate::trip::RodeOn> {
self.metadata.progress_rode_on(token)
}

/// A ride on a trip day makes that trip the active one: its start record moves the trip's record
/// to the end of the records, in the store too, so the start card follows it after a power
/// cycle. The store moves its own copy, so records the device has not read yet are safe.
pub(crate) fn note_trip_start(&mut self) {
let Some(day) = self.recorder.ride_stats().trip else { return };
if self.metadata.progress().last().is_some_and(|p| p.key == day.key()) {
return;
}
let Some(trip) = self.trips().iter().find(|t| t.key == day.key()) else { return };
let record = trip.start();
let trips = self.catalogs.trips();
self.metadata.owe_start(record, |key| trips.iter().any(|t| t.key == key));
}

/// A saved ride on a trip day moves that trip's progress to where the ride ended.
pub(crate) fn note_trip_finish(&mut self) {
use crate::trip::TripPosition;
Expand All @@ -1168,6 +1191,8 @@ impl App {
let active_index = end.map_or(self.active_route_index(), |end| Some(end.route));
let progress_m = end.map_or(self.progress_m(), |end| end.progress_m);
let arrived = end.map_or(self.navigator.route_state().arrival.arrived(), |end| end.arrived);
let rode_on =
end.map_or(self.navigator.route_state().arrival == crate::navigator::Arrival::RodeOn, |end| end.rode_on);
let active = active_index.and_then(|i| self.route_ids().get(i).copied());
// After a reset the adopted lead-in is gone, but a built day is still the internal route
// the record and the line facts describe: its rest starts where the record stands.
Expand Down Expand Up @@ -1212,8 +1237,16 @@ impl App {
// can have moved past the day the ride started on.
let finished = if arrived { at.day } else { day };
let record = trip.finish(old, finished, at, today);
// Past the end of its day the ride may be on the next day's line; the executor reads that
// route and projects the fix onto it once.
let rode_on = rode_on.then_some(()).and_then(|()| {
let fix = self.state.user_fix?;
let day = at.day.checked_add(1)?;
let &route = trip.stage_ids.get(usize::from(day))?;
Some(crate::trip::RodeOn { fix: (fix.lon, fix.lat), day, route })
});
let trips = self.catalogs.trips();
self.metadata.owe_progress(record, |key| trips.iter().any(|t| t.key == key));
self.metadata.owe_progress(record, rode_on, |key| trips.iter().any(|t| t.key == key));
}

/// The card after the save of a ride on a trip day: DAY N DONE, or TRIP DONE after the last
Expand Down
2 changes: 2 additions & 0 deletions firmware/obc-app/src/device_core/pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,8 @@ impl App {
}
}
}
// A continuation writes it too: a reset can come before the start's write lands.
self.note_trip_start();
self.recorder.restart_buffers();
self.ui.map_dirty = true;
}
Expand Down
44 changes: 43 additions & 1 deletion firmware/obc-app/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ pub(crate) struct MetadataMachine {
progress: Records,
/// A Finish's record that the store does not hold yet.
progress_owed: Option<TripProgress>,
/// The owed Finish rode on past the end of its day.
rode_on: Option<crate::trip::RodeOn>,
/// The write in flight is the owed record.
writing_progress: bool,
/// Where the active trip's next day meets the day before, from the last catalog read.
Expand All @@ -80,6 +82,7 @@ impl MetadataMachine {
blocked: false,
progress: Records::new(),
progress_owed: None,
rode_on: None,
writing_progress: false,
day_join: None,
replaced: heapless::Vec::new(),
Expand All @@ -105,6 +108,10 @@ impl MetadataMachine {
pub(crate) fn progress_payload(&self, token: OperationToken<MetadataTag>) -> Option<&TripProgress> {
self.progress_owed.as_ref().filter(|_| self.writing_progress && self.ops.is_current(token))
}
/// The owed record's ride on past the end of its day, while `token` is the write in flight.
pub(crate) fn progress_rode_on(&self, token: OperationToken<MetadataTag>) -> Option<crate::trip::RodeOn> {
self.progress_payload(token).and(self.rode_on)
}
pub(crate) fn apply_outcome(&mut self, outcome: MetadataOutcome) -> bool {
if !self.inflight || !self.ops.is_current(outcome.token()) {
return false;
Expand All @@ -116,16 +123,31 @@ impl MetadataMachine {
);
if core::mem::take(&mut self.writing_progress) && !retry {
self.progress_owed = None;
self.rode_on = None;
}
self.ops.invalidate();
self.inflight = false;
self.blocked = matches!(outcome, MetadataOutcome::Failed { error: MetadataError::RemountRequired, .. });
true
}
/// A Finish's record. The resident records take it at once; the store takes it when it can.
pub(crate) fn owe_progress(&mut self, record: TripProgress, stored: impl Fn(u64) -> bool) {
pub(crate) fn owe_progress(
&mut self,
record: TripProgress,
rode_on: Option<crate::trip::RodeOn>,
stored: impl Fn(u64) -> bool,
) {
obc_formats::trip_progress::record(&mut self.progress, record.clone(), stored);
self.progress_owed = Some(record);
self.rode_on = rode_on;
// A write in flight carries the record before this one, so its answer must not clear this.
self.writing_progress = false;
}
/// A start's record. It never displaces a Finish's record that the store does not hold yet.
pub(crate) fn owe_start(&mut self, record: TripProgress, stored: impl Fn(u64) -> bool) {
if self.progress_owed.is_none() {
self.owe_progress(record, None, stored);
}
}
pub(crate) fn progress(&self) -> &[TripProgress] {
&self.progress
Expand Down Expand Up @@ -171,6 +193,7 @@ impl MetadataMachine {
&& !self.blocked
&& self.progress.is_empty()
&& self.progress_owed.is_none()
&& self.rode_on.is_none()
&& !self.writing_progress
&& self.day_join.is_none()
&& self.replaced.is_empty()
Expand Down Expand Up @@ -200,4 +223,23 @@ mod tests {
assert!(machine.apply_outcome(MetadataOutcome::Cancelled { token: retry }));
assert!(machine.next_checkpoint_effect().is_some());
}

#[test]
fn a_finish_owed_while_a_start_write_is_in_flight_is_written_next() {
let record = |key, last_finished| TripProgress {
key,
day: 0,
day_route: obc_formats::trip_progress::RouteVersion { id: 7, revision: 0 },
metres: 0,
last_finished,
dates: [0; obc_formats::trip_progress::MAX_DAYS],
};
let mut machine = MetadataMachine::new();
machine.owe_start(record(1, None), |_| true);
let start = machine.next_progress_effect().unwrap().token();
machine.owe_progress(record(1, Some(0)), None, |_| true);
assert!(machine.apply_outcome(MetadataOutcome::ProgressWritten { token: start }));
let finish = machine.next_progress_effect().expect("the Finish is still owed").token();
assert_eq!(machine.progress_payload(finish), Some(&record(1, Some(0))));
}
}
26 changes: 13 additions & 13 deletions firmware/obc-app/src/navigator/arrival.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ mod tests {
use super::Arrival;
use crate::activity::Mode;
use crate::device_core::{DerivedInputs, DerivedTargets, ExternalFacts, OutcomeSlots, PassClock, PassInputs};
use crate::harness::support::{mount_store, quiet_pass, VecSink, EVERY_CAPABILITY};
use crate::harness::support::{VecSink, EVERY_CAPABILITY};
use crate::screen::{self, ArrivalView, MapScreen, RouteSwapScreen, Screen, Transition, WarningFlags};
use crate::trip::TripInput;
use crate::{App, AppState, Gesture, RecorderIntent};
Expand Down Expand Up @@ -124,13 +124,15 @@ mod tests {
std::thread_local! {
/// The executor's answers to the last pass, delivered on the next one.
static OUTCOMES: core::cell::RefCell<OutcomeSlots> = const { core::cell::RefCell::new(OutcomeSlots::new()) };
/// The store's revision. Each write moves it, and the app reads the catalog again.
static REVISION: core::cell::Cell<u64> = const { core::cell::Cell::new(1) };
}

/// One pass at `ms`, with a fresh fix at `(lon, lat)` µdeg when given. It answers the store work
/// the pass asks for, as an executor would, and returns a trip progress record it writes.
fn pass(
app: &mut App,
route: &RouteReader,
route: Option<&RouteReader>,
ms: u32,
fix: Option<(i32, i32)>,
gestures: &[Gesture],
Expand All @@ -140,7 +142,7 @@ mod tests {
use crate::metadata::{MetadataEffect, MetadataOutcome};
use crate::recorder::{CheckpointStatus, RecorderEffect, RecorderOutcome};

let scope = StoreRevision { store: StoreIdentity::new(1), revision: Revision::new(1) };
let scope = StoreRevision { store: StoreIdentity::new(1), revision: Revision::new(REVISION.get()) };
let mut loc = Once(fix.map(|(lon, lat)| Fix::at(lat, lon)));
let mut facts = ExternalFacts::NONE;
facts.note_store_revision(scope);
Expand All @@ -149,7 +151,7 @@ mod tests {
now: PassClock { ride: RideClock(ms), ui: InputClock(ms) },
gestures,
sensors: Sensors::new(&mut loc),
route: Some(route),
route,
support: EVERY_CAPABILITY,
outcomes,
facts: &mut facts,
Expand All @@ -172,6 +174,7 @@ mod tests {
}
let Some(MetadataEffect::WriteProgress { token, .. }) = plan.effects.metadata.take() else { return None };
let _ = outcomes.metadata.try_put(MetadataOutcome::ProgressWritten { token });
REVISION.set(REVISION.get() + 1);
app.trip_progress_payload(token).cloned()
})
}
Expand All @@ -180,15 +183,15 @@ mod tests {
fn ride(app: &mut App, route: &RouteReader, fixes: &[(i32, i32)]) {
for &fix in fixes {
let ms = app.ui.now_ms + 1_000;
pass(app, route, ms, Some(fix), &[]);
pass(app, Some(route), ms, Some(fix), &[]);
}
let ms = app.ui.now_ms + 1_000;
pass(app, route, ms, None, &[]);
pass(app, Some(route), ms, None, &[]);
}

fn press(app: &mut App, route: &RouteReader, gestures: &[Gesture]) {
let ms = app.ui.now_ms + 1_000;
pass(app, route, ms, None, gestures);
pass(app, Some(route), ms, None, gestures);
}

/// Recording on catalog route 0 of `routes`, on the Map.
Expand All @@ -207,14 +210,11 @@ mod tests {
let (summaries, ids): (Vec<_>, Vec<_>) = routes.iter().cloned().unzip();
app.set_routes_with_ids(&summaries, &ids);
app.set_trips(trips);
mount_store(&mut app);
let store = crate::device_core::StoreIdentity::new(1);
let scope = crate::device_core::StoreRevision { store, revision: crate::device_core::Revision::new(1) };
app.catalogs.loaded_scope = Some(scope);
pass(&mut app, None, 0, None, &[]); // mounts the store, which answers the catalog read
app.activate_route(first);
prepare(&mut app);
app.recorder.request(RecorderIntent::Start);
quiet_pass(&mut app, 1);
pass(&mut app, None, 1, None, &[]);
assert!(app.recorder.recording());
app.activity.mode = Mode::Riding;
screen::apply(&mut app.ui.stack, Transition::Root(Screen::Map(MapScreen::new())));
Expand Down Expand Up @@ -393,7 +393,7 @@ mod tests {
(0..40)
.find_map(|_| {
let ms = app.ui.now_ms + 1_000;
pass(app, route, ms, None, &[])
pass(app, Some(route), ms, None, &[])
})
.expect("the Finish writes the trip progress")
}
Expand Down
3 changes: 3 additions & 0 deletions firmware/obc-app/src/navigator/following.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub(crate) struct RideEnd {
pub(crate) progress_m: u32,
/// The rider had arrived at the route's end.
pub(crate) arrived: bool,
/// The rider rode on past the route's end after arriving.
pub(crate) rode_on: bool,
}

/// A seam re-anchor waiting for the next tick with matching route geometry.
Expand Down Expand Up @@ -243,6 +245,7 @@ impl NavigatorMachine {
route,
progress_m: self.following.progress_m,
arrived: self.following.arrival.arrived(),
rode_on: self.following.arrival == Arrival::RodeOn,
});
}

Expand Down
81 changes: 80 additions & 1 deletion firmware/obc-app/src/trip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,42 @@ pub fn gap_m(end: (i32, i32), start: (i32, i32)) -> u32 {
m + u32::from((m as f32) < d)
}

/// A fix at most this far from the next day's route puts a ride that rode on past the end of its
/// day onto the next day (spec §7.7).
pub const RODE_ON_MATCH_M: f32 = 50.0;

/// A Finish after the rider rode on past the end of the loaded day: the last fix, `(lon, lat)`
/// µdeg, and the next day with its route. The executor projects the fix onto that route once,
/// when it writes the Finish's record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RodeOn {
pub fix: (i32, i32),
pub day: u16,
pub route: CatalogObjectId,
}

impl RodeOn {
/// The Finish's record once the fix is projected. `nearest` is `(metres along, metres away)` on
/// the next day's route. Within [`RODE_ON_MATCH_M`] the position moves into the next day, and
/// the finished day stays.
pub fn apply(self, record: TripProgress, nearest: Option<(u32, f32)>) -> TripProgress {
match nearest {
Some((metres, away)) if away <= RODE_ON_MATCH_M => TripProgress {
day: self.day,
day_route: RouteVersion { id: self.route, revision: 0 },
metres,
..record
},
_ => record,
}
}

/// [`apply`](Self::apply) with the projection read from `next`, the next day's route bytes.
pub fn settle(self, record: TripProgress, next: &dyn obc_formats::io::ByteSource) -> TripProgress {
self.apply(record, obc_route::nearest_along(next, self.fix).ok().flatten())
}
}

/// A position on a trip: a day, that day's route, and metres into it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TripPosition {
Expand Down Expand Up @@ -293,14 +329,28 @@ impl TripSummary {
TripProgress {
key: self.key,
day: at.day,
// The store stamps the revision it holds when it writes the record.
// Revision 0: the store stamps the revision it holds when it writes the record.
day_route: RouteVersion { id: at.route, revision: 0 },
metres: at.metres,
last_finished: Some(ridden),
dates,
}
}

/// The start record a ride on this trip writes. It has no finished day, so it never replaces
/// the trip's record: the write moves that record to the end, where the latest record names the
/// active trip. A trip without a record gets this one.
pub fn start(&self) -> TripProgress {
TripProgress {
key: self.key,
day: 0,
day_route: RouteVersion { id: self.stage_ids.first().copied().unwrap_or(0), revision: 0 },
metres: 0,
last_finished: None,
dates: [0; MAX_TRIP_DAYS],
}
}

/// How day `day` loads. When the position is on the day before, more than [`REST_MIN_M`] before
/// it leaves the line, and no transfer lies between the two days, the day is the rest of that
/// day and then this day. Otherwise it is this day's route as it is.
Expand Down Expand Up @@ -406,6 +456,20 @@ mod tests {
assert_eq!(next(&[]), None);
}

#[test]
fn a_start_moves_the_record_and_gives_a_trip_without_one_no_progress() {
let t = trip(0);
let early = progress(1, Some(0), &[MON]);
let mut records = obc_formats::trip_progress::Records::new();
obc_formats::trip_progress::record(&mut records, early.clone(), |_| true);
obc_formats::trip_progress::record(&mut records, t.start(), |_| true);
assert_eq!(records.as_slice(), [early]);
let fresh = t.start();
assert_eq!((fresh.day, fresh.day_route.id, fresh.metres, fresh.last_finished), (0, 10, 0, None));
assert_eq!(t.next_day(Some(&fresh)), Some(0));
assert!(!t.is_ticked(0, Some(&fresh)));
}

#[test]
fn later_days_count_after_the_day_and_skip_a_dangling_one() {
let route = |distance_km| RouteSummary {
Expand Down Expand Up @@ -546,6 +610,21 @@ mod tests {
assert_eq!(t.next_day(Some(&short)), Some(2));
}

#[test]
fn a_ride_that_rode_on_moves_into_the_next_day_when_the_fix_is_on_its_route() {
let t = trip(0);
// Day 2 ridden to its end, then 20 km on along Day 3 before the Finish.
let end = t.finish(None, 1, TripPosition { day: 1, route: 20, metres: 74_000 }, MON);
let rode_on = RodeOn { fix: (8_000_000, 46_000_000), day: 2, route: 30 };
let on = rode_on.apply(end.clone(), Some((20_000, RODE_ON_MATCH_M)));
assert_eq!((on.day, on.day_route.id, on.metres, on.last_finished), (2, 30, 20_000, Some(1)));
assert_eq!((t.next_day(Some(&on)), t.position_m(Some(&on))), (Some(2), 20_000));
assert_eq!(on.dates, end.dates);
// Off Day 3's line, or no line to read: the position stays at the end of Day 2.
assert_eq!(rode_on.apply(end.clone(), Some((20_000, RODE_ON_MATCH_M + 1.0))), end);
assert_eq!(rode_on.apply(end.clone(), None), end);
}

#[test]
fn progress_past_a_shorter_reupload_is_dropped_not_done() {
// Recorded on a five-day version: in Day 5, Day 4 finished. The trip now has three days.
Expand Down
Loading
Loading