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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "sync"
eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow", "wayland", "x11"] }
egui = "0.35"
egui_plot = "0.36"
# image PNG encoding, for the window photographing itself (xtce-gs-gui::capture).
# Already in the tree behind eframe; only the encoder is wanted, so the
# decoders and formats that come with the default features stay out.
image = { version = "0.25", default-features = false, features = ["png"] }
clap = { version = "4", features = ["derive"] }
thiserror = "2"
serde = { version = "1", features = ["derive"] }
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,21 @@ $ xtce-gs export testdata/jpss/jpss1_geolocation_xtce_v1.xml \
testdata/jpss/J01_G011_LZ_2021-04-09T00-00-00Z_V01.DAT1 -o jpss.csv
```

![The station replaying a JPSS pass: a parameter tree, a table of current values, and plots of
GPS position and velocity over one orbit](docs/station.png)

That picture is not a screen grab. The window photographs itself, so there is a command that
makes it again rather than a file nobody can reproduce:

```console
$ xtce-gs replay testdata/jpss/jpss1_geolocation_xtce_v1.xml \
testdata/jpss/J01_G011_LZ_2021-04-09T00-00-00Z_V01.DAT1 \
--rate 100000 --repeat --screenshot docs/station.png --screenshot-after 4
```

The replay opens with three plots already drawn — GPS position, GPS velocity and the attitude
quaternion, 7 200 points each — because a layout ships beside the definition as
quaternion, 7 200 points each, the third below the fold — because a layout ships beside the
definition as
`jpss1_geolocation_xtce_v1.xml.gs-layout.json`. That is not a special case for the demo: a
layout is a plain JSON file of qualified parameter names that the interface writes next to
whatever definition you open, so the arrangement belongs to the mission rather than to your
Expand Down
35 changes: 35 additions & 0 deletions crates/xtce-gs-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ pub const DEFAULT_INTERLEAVE: usize = 1;
/// be 600 MB of rings for an export that never looks in one.
pub const EXPORT_HISTORY_DEPTH: usize = 1;

/// Seconds a `--screenshot` waits by default.
///
/// The bundled JPSS recording is 5.1 seconds of pass at the rate the README's example uses,
/// so ten seconds is a whole pass and some of a second one — long enough that every plot in
/// the shipped layout is drawn from a full orbit rather than from the start of one.
pub const DEFAULT_SCREENSHOT_DELAY: u64 = 10;

/// A ground station for CCSDS telemetry described by XTCE.
#[derive(Parser, Debug)]
#[command(name = "xtce-gs", version, about, long_about = None)]
Expand Down Expand Up @@ -126,6 +133,20 @@ pub struct RunArgs {
/// Run without a window, printing counters once a second.
#[arg(long)]
pub headless: bool,

/// Write a PNG of the window once it has filled, then exit.
///
/// The window photographs itself, so this needs no screen-recording permission and
/// produces the same image on any machine that can open it.
#[arg(long, value_name = "PATH")]
pub screenshot: Option<PathBuf>,

/// Seconds to let the station fill before the screenshot is taken.
///
/// Every plot is empty until packets arrive, so a picture taken immediately is a true and
/// useless one. How long is a property of the source, which only you know.
#[arg(long, value_name = "SECS", default_value_t = DEFAULT_SCREENSHOT_DELAY)]
pub screenshot_after: u64,
}

impl RunArgs {
Expand Down Expand Up @@ -202,6 +223,20 @@ pub struct ReplayArgs {
/// Run without a window, printing counters once a second.
#[arg(long)]
pub headless: bool,

/// Write a PNG of the window once it has filled, then exit.
///
/// The window photographs itself, so this needs no screen-recording permission and
/// produces the same image on any machine that can open it.
#[arg(long, value_name = "PATH")]
pub screenshot: Option<PathBuf>,

/// Seconds to let the station fill before the screenshot is taken.
///
/// Every plot is empty until packets arrive, so a picture taken immediately is a true and
/// useless one. How long is a property of the source, which only you know.
#[arg(long, value_name = "SECS", default_value_t = DEFAULT_SCREENSHOT_DELAY)]
pub screenshot_after: u64,
}

impl ReplayArgs {
Expand Down
20 changes: 18 additions & 2 deletions crates/xtce-gs-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]

use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;

use clap::Parser;

Expand Down Expand Up @@ -83,6 +85,18 @@ fn report(error: &dyn std::error::Error) {
}
}

/// Pairs `--screenshot` with `--screenshot-after`.
///
/// The delay has a default and the path does not, so the path is what decides whether a
/// picture is wanted at all; `--screenshot-after` on its own is a no-op rather than an error,
/// the same way `--rate` is when nothing is replayed.
fn screenshot(path: Option<PathBuf>, after: u64) -> Option<xtce_gs_gui::Screenshot> {
path.map(|path| xtce_gs_gui::Screenshot {
path,
after: Duration::from_secs(after),
})
}

/// Turns one parsed command line into one thing done.
///
/// The conversions happen *here* rather than inside [`run`], so that a configuration nothing
Expand All @@ -96,7 +110,8 @@ fn dispatch(cli: Cli) -> Result<(), CliError> {
if args.headless {
run::headless(config, verbose)
} else {
run::gui(config)
let shot = screenshot(args.screenshot, args.screenshot_after);
run::gui(config, shot)
}
}

Expand All @@ -105,7 +120,8 @@ fn dispatch(cli: Cli) -> Result<(), CliError> {
if args.headless {
run::headless(config, verbose)
} else {
run::gui(config)
let shot = screenshot(args.screenshot, args.screenshot_after);
run::gui(config, shot)
}
}

Expand Down
7 changes: 5 additions & 2 deletions crates/xtce-gs-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@ const CCSDS_INTERLEAVE: [u64; 6] = [1, 2, 3, 4, 5, 8];
///
/// [`CliError::Gui`] for a session that will not start, a layout that will not parse, or a
/// platform that will not give the process a window.
pub fn gui(config: SessionConfig) -> Result<(), CliError> {
xtce_gs_gui::run(config).map_err(CliError::from)
pub fn gui(
config: SessionConfig,
screenshot: Option<xtce_gs_gui::Screenshot>,
) -> Result<(), CliError> {
xtce_gs_gui::run_capturing(config, screenshot).map_err(CliError::from)
}

/// Runs with no window, printing counters once a second.
Expand Down
1 change: 1 addition & 0 deletions crates/xtce-gs-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ tokio.workspace = true
eframe.workspace = true
egui.workspace = true
egui_plot.workspace = true
image.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
Expand Down
25 changes: 22 additions & 3 deletions crates/xtce-gs-gui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use xtce_gs_engine::Session;
use xtce_gs_engine::session::log;
use xtce_model::{ParamId, XtceDb};

use crate::capture::{Capture, Next, Screenshot};
use crate::layout::Layout;
use crate::panels::events::Events;
use crate::panels::plots::{Plots, View};
Expand Down Expand Up @@ -97,6 +98,8 @@ pub struct App {
view_drawn: View,
/// Passes still owed a refill after input arrived — see [`INPUT_REFRESH_PASSES`].
input_passes: u8,
/// A picture of the window this run owes somebody, and has not taken yet.
capture: Option<Capture>,
}

/// Whether two axis ranges are the same number.
Expand Down Expand Up @@ -158,12 +161,17 @@ impl App {
/// layout comes from the file [`crate::Layout::load`] read before the window opened. The
/// parameter is taken anyway because it is where the theme and the fonts belong.
#[must_use]
pub fn new(cc: &eframe::CreationContext<'_>, session: Session, mut layout: Layout) -> Self {
pub fn new(
cc: &eframe::CreationContext<'_>,
session: Session,
mut layout: Layout,
screenshot: Option<Screenshot>,
) -> Self {
let db = Arc::clone(session.db());
cc.egui_ctx.set_theme(layout.theme.preference());

watch_what_is_plotted(&mut layout);
let named = layout.watched.len();
let named = layout.named();
let resolved = {
let mut store = session
.store()
Expand Down Expand Up @@ -195,6 +203,7 @@ impl App {
db,
layout,
status: Status::new(),
capture: screenshot.map(Capture::new),
// `Tree::new` starts dirty, which is what forces the first frame's copy: the
// store's generation can legitimately still be 0 at this point.
tree: Tree::new(),
Expand Down Expand Up @@ -528,7 +537,7 @@ fn restore_report(resolved: usize, named: usize) -> Event {
format!(
"layout: {resolved} of {named} saved parameters are in this definition; the \
other {} are kept in the layout in case it comes back",
named - resolved
named.saturating_sub(resolved)
),
)
}
Expand All @@ -553,6 +562,16 @@ impl eframe::App for App {
}
// The clock, the ages and the live indicator move on their own.
ctx.request_repaint_after_secs(IDLE_REPAINT_SECONDS);

// Last, so the frame handed back is one `refresh` has already filled.
let taken = self
.capture
.as_mut()
.is_some_and(|capture| capture.poll(ctx) == Next::Done);
if taken {
self.capture = None;
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}

/// Lays the panels out over the buffers [`App::refresh`] filled. Computes nothing.
Expand Down
Loading
Loading