onerom-cli 0.1.8

Command line interface to manage One ROM - the most flexible retro ROM replacement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
// Copyright (C) 2026 Piers Finlayson <piers@piers.rocks>
//
// MIT License

//! CLI argument definitions for the One ROM command-line interface.
//!
//! The top-level structure is:
//!   onerom scan                  - Discover connected One ROMs
//!   onerom firmware <subcommand> - Firmware binary management
//!   onerom program               - Build and flash firmware to a One ROM
//!   onerom inspect <subcommand>  - Read-only One ROM state and information
//!   onerom control <subcommand>  - Transient One ROM actions
//!   onerom update <subcommand>   - Persistent One ROM modifications
//!   onerom image <subcommand>    - ROM image file manipulation
//!
//! The --serial option is global and can be specified at any level to select
//! a specific One ROM when multiple are connected.

// rustdoc doesn't like a bunch of the doc comments, which are used for clap
// argument documentation and included in the binary.  Suppress the rustdoc
// warnings.
#![allow(rustdoc::broken_intra_doc_links)]
#![allow(rustdoc::invalid_html_tags)]
#![allow(rustdoc::bare_urls)]

pub mod control;
pub mod firmware;
pub mod image;
pub mod inspect;
pub mod plugin;
pub mod program;
pub mod scan;
pub mod update;

use clap::{Parser, Subcommand};
use enum_dispatch::enum_dispatch;
use log::debug;
use onerom_cli::LogLevel;

use crate::utils::parse_u16_hex_only;
use onerom_cli::{Error, Options};

use control::{
    ControlArgs, ControlCommands, ControlEraseArgs, ControlGpioArgs, ControlLedArgs,
    ControlLedBeaconArgs, ControlLedCommands, ControlLedFlameArgs, ControlLedOffArgs,
    ControlLedOnArgs, ControlPokeArgs, ControlPokeCommands, ControlPokeLiveArgs,
    ControlPokeMemoryArgs, ControlRebootArgs, ControlResetArgs, ControlSelectArgs,
};
use firmware::{
    FirmwareArgs, FirmwareBuildArgs, FirmwareChipsArgs, FirmwareCommands, FirmwareDownloadArgs,
    FirmwareInspectArgs, FirmwareReleasesArgs,
};
use image::{ImageArgs, ImageCommands, ImageSwapBytesArgs};
use inspect::{
    InspectArgs, InspectCommands, InspectGpioArgs, InspectImageArgs, InspectInfoArgs,
    InspectPeekArgs, InspectPeekCommands, InspectPeekLiveArgs, InspectPeekMemoryArgs,
    InspectSlotsArgs, InspectTelemetryArgs,
};
use plugin::PluginArgs;
use program::ProgramArgs;
use scan::ScanArgs;
use update::{UpdateArgs, UpdateCommands, UpdateCommitArgs, UpdateOtpArgs, UpdateSlotArgs};

#[enum_dispatch]
pub trait CommandTrait {
    fn requires_device(&self) -> bool;
}

/// Command line interface for One ROM - the most flexible retrom ROM replacement.
///
/// https://onerom.org/
///
/// Copyright (c) 2026 Piers Finlayson <piers@piers.rocks>
///
/// Manage One ROMs, firmware, and ROM configurations. Run `onerom help
/// <command>` for detailed information on any subcommand.
///
/// Use `onerom scan` to discover connected One ROMs before running One ROM
/// commands.
///
/// Most CLI commands take either --board to identify the One ROM's board
/// type, or --serial to infer it directly from the specified One ROM.  If only
/// a single One ROM is connected and it can be identifed automatically,
/// neither --board or --serial are needed.  --board can be supplied to override
/// the current connected One ROM's board type.
///
/// Unprogrammed and unrecognised (e.g. bricked) One ROMs can be managed by
/// using the --unrecognised flag and supplying --board.
#[derive(Debug, Parser)]
#[command(name = "onerom", version = concat!("v", env!("CARGO_PKG_VERSION")), about, long_about)]
pub struct Cli {
    /// Select a specific One ROM by serial number.
    ///
    /// Required when multiple One ROMs are connected.
    ///
    /// Accepts * and ? wildcards for partial matching.
    ///
    /// If omitted and exactly one One ROM is connected, that One ROM is
    /// used automatically.
    #[arg(global = true, long, short, value_name = "DEVICE")]
    pub serial: Option<String>,

    /// USB vendor/product ID pair (hex, e.g. 1234:abcd).
    ///
    /// Used to detect One ROMs using non-standard USB vendor/product IDs.  If
    /// specified, only those VID/PID pairs specified will be matched.
    ///
    /// Specify multiple pairs by specifying the --vid-pid argument multiple
    /// times.
    ///
    /// Use in conjunction with --unrecognised to manage One ROMs that do not
    /// have a known One ROM firmware signature, such as unprogrammed or
    /// bricked One ROMs.
    #[arg(global = true, long, short='i', visible_alias="id", value_name = "VID:PID", value_parser = parse_vid_pid, action = clap::ArgAction::Append)]
    pub vid_pid: Vec<(u16, u16)>,

    /// Allow management of unrecognised and unprogrammed One ROMs.
    ///
    /// This is a global flag that can be used with any command to allow
    /// this tool to manage RP2350-based One ROMs that do not have a known One
    /// ROM firmware signature, such as unprogrammed or bricked One ROMs.
    ///
    /// Note that even unrecognised One ROMs must expose a valid picoboot USB
    /// interface to be detected and managed by this tool.
    ///
    /// Use with caution as this allows programming of any non-One ROM RP2350
    /// boards that are attached.
    ///
    /// Use in conjunction with --vid-pid to manage One ROMs that have
    /// unexpected USB vendor and/or product IDs.
    #[arg(global = true, visible_alias = "unrecognized", long, short)]
    pub unrecognised: bool,

    /// Auto-confirm all prompts with "yes".
    ///
    /// This is a global flag that can be used with any command to
    /// automatically answer "yes" to all prompts, allowing for non-interactive
    /// use.
    ///
    /// Use with caution, as it may lead to unintended consequences if used
    /// without fully understanding the implications of the command being
    /// run.
    #[arg(global = true, long, short)]
    pub yes: bool,

    /// Enable verbose output.
    #[arg(global = true, long, short)]
    pub verbose: bool,

    /// Set logging level.
    #[arg(global = true, long, value_enum, default_value_t = LogLevel::Warn)]
    pub log_level: LogLevel,

    #[command(subcommand)]
    pub command: Commands,
}

fn parse_vid_pid(s: &str) -> Result<(u16, u16), String> {
    let (vid, pid) = s
        .split_once(':')
        .ok_or_else(|| format!("expected VID:PID, got '{s}'"))?;
    let vid = parse_u16_hex_only(vid).map_err(|e| format!("invalid VID '{vid}': {e}"))?;
    let pid = parse_u16_hex_only(pid).map_err(|e| format!("invalid PID '{pid}': {e}"))?;
    Ok((vid, pid))
}

fn check_vid_pid_unique(vid_pid_list: &[(u16, u16)]) -> Result<(), Error> {
    let mut seen = std::collections::HashSet::new();
    for (vid, pid) in vid_pid_list {
        if !seen.insert((*vid, *pid)) {
            return Err(Error::InvalidArgument(
                "global".to_string(),
                format!("Duplicate VID:PID pair '{:04x}:{:04x}'", vid, pid),
            ));
        }
    }
    Ok(())
}

impl Cli {
    pub async fn try_into_options(&mut self) -> Result<Options, Error> {
        // Build the options struct first.
        let mut options = Options {
            log_level: self.log_level.clone(),
            verbose: self.verbose,
            yes: self.yes,
            unrecognised: self.unrecognised,
            device: None,
            vid_pid: self.vid_pid.clone(),
        };

        // Check for duplicate VID/PID pairs
        check_vid_pid_unique(&options.vid_pid)?;

        let requires_device = self.command.requires_device();

        // Check if command needs a device
        if let Some(device) = self.serial.as_ref()
            && !requires_device
        {
            debug!("Device {device} specified but not required, retrieving it anyway");
        }

        // If a serial was specified, select it and add it to the options,
        // unless handling a scan command (in which case we're scanning for
        // all devices that meet some criteria).
        if let Commands::Scan(scan) = &mut self.command {
            // Save off the serial for special handling in the scan case.
            scan.serial = self.serial.clone();
            return Ok(options);
        } else if let Some(serial) = self.serial.as_ref() {
            if options.verbose {
                println!("Scanning for device with serial '{}' ...", serial);
            }
            match onerom_cli::device::select_device(
                Some(serial),
                options.unrecognised,
                &options.vid_pid,
            )
            .await
            {
                Ok(device) => {
                    if options.verbose {
                        println!("Found device: {device}");
                    }
                    options.device = Some(device);
                }
                Err(e) => {
                    eprintln!("Error selecting device with serial '{}': {e}", serial);
                    std::process::exit(1);
                }
            }
        }

        // If no device was specified, attempt to detect one
        if options.device.is_none() {
            if options.verbose {
                println!("No device specified, scanning for connected devices ...");
            }
            match onerom_cli::device::select_device(None, options.unrecognised, &options.vid_pid)
                .await
            {
                Ok(device) => {
                    if options.verbose {
                        println!("Found device: {device}");
                    }
                    options.device = Some(device);
                }
                Err(Error::NoDevices) => {
                    // No devices found, this may or may not be an error depending on the command.
                    if requires_device {
                        debug!("No devices found.");
                    }
                }
                Err(e) => {
                    return Err(e);
                }
            }
        }

        Ok(options)
    }
}

#[enum_dispatch(CommandTrait)]
#[derive(Debug, Subcommand)]
pub enum Commands {
    /// Discover and list connected One ROM.
    Scan(ScanArgs),

    /// Flash One ROM firmware to a connected One ROM.
    ///
    /// This is the primary workflow for most users. The board is
    /// inferred from the connected One ROM if not specified explicitly.
    ///
    /// This command either flashes a provided firmware binary, or builds on
    /// based on the configuration provided.
    ///
    /// With a single One ROM connected and a config file:
    ///
    ///   onerom program --config c64.json
    ///
    /// With multiple One ROMs connected, using a wildcard to select the target
    /// One ROM:
    ///
    ///   onerom program --serial '5*' --config c64.json
    ///
    /// With explicit ROM arguments instead of a config file:
    ///
    ///   onerom program --board fire-24-e \
    ///       --slot file=kernal.bin,type=2364,cs=active_low \
    ///       --slot file=basic.bin,type=2364,cs=active_low
    ///
    /// Using a local, pre-built firmware binary, containing the ROM metadata and
    /// images:
    ///
    ///   onerom program --firmware firmware.bin
    ///
    /// Using a local, pre-built minimal firmware, with no ROM metadata or images,
    /// and specifying the ROMs via arguments:
    ///
    ///   onerom program --firmware minimal.bin \
    ///       --slot file=kernal.bin,type=2364,cs=active_low \
    ///       --slot file=basic.bin,type=2364,cs=active_low
    ///
    /// To save the firmware to file, **as well** as programming the One ROM, use
    /// --out.
    ///
    ///   onerom program --config c64.json --out firmware.bin
    ///
    /// To generate a firmware binary without programming a One ROM, use the
    /// 'firmware' command.
    Program(ProgramArgs),

    /// Read-only inspection of a connected One ROM.
    #[command(
        subcommand_value_name = "COMMAND",
        subcommand_help_heading = "Commands"
    )]
    Inspect(InspectArgs),

    /// Perform transient actions on a connected One ROM.
    ///
    /// These actions affect the One ROM's current state but do not persist
    /// across power cycles.
    #[command(
        subcommand_value_name = "COMMAND",
        subcommand_help_heading = "Commands"
    )]
    Control(ControlArgs),

    /// Make persistent modifications to a connected One ROM.
    ///
    /// These operations write to the One ROM's flash memory and survive
    /// power cycles.
    #[command(
        subcommand_value_name = "COMMAND",
        subcommand_help_heading = "Commands"
    )]
    Update(UpdateArgs),

    /// Manipulate ROM image files.
    ///
    /// File-based operations for preparing and transforming ROM binary images
    /// before programming. No device connection required.
    ///
    /// Example:
    ///
    ///   onerom image swap-bytes --input kick.bin --output kick-swapped.bin
    #[command(
        subcommand_value_name = "COMMAND",
        subcommand_help_heading = "Commands"
    )]
    Image(ImageArgs),

    /// Read data from One ROM's live ROM image.
    ///
    /// Top-level alias for `inspect peek live`. See `onerom inspect peek live --help`
    /// for full documentation.
    ///
    /// Example:
    ///
    ///   onerom peek live --address 0x100 --length 64
    #[command(
        subcommand_value_name = "COMMAND",
        subcommand_help_heading = "Commands"
    )]
    Peek(InspectPeekLiveArgs),

    /// Write data to One ROM's live ROM image.
    ///
    /// Top-level alias for `control poke live`. See `onerom control poke live --help`
    /// for full documentation.
    ///
    /// Example:
    ///
    ///   onerom poke live --address 0x100 --input patch.bin
    #[command(
        subcommand_value_name = "COMMAND",
        subcommand_help_heading = "Commands"
    )]
    Poke(ControlPokeLiveArgs),

    /// Reboot a One ROM.
    ///
    /// Restarts a selected One ROM. The One ROM re-initialises and
    /// resumes serving ROM images after the reboot.
    ///
    /// By default, this command briefly pauses after a reboot to give the
    /// One ROM time to re-enumerate.
    ///
    /// Example:
    ///
    ///   onerom reboot
    Reboot(ControlRebootArgs),

    /// Build, inspect, and manage One ROM firmware binaries.
    ///
    /// Used to build complete One ROM firmware binaries from configuraton files,
    /// command line configuation, and also inspect firmware binaries.
    ///
    /// Use `program` to flash firmware to a One ROM - `program` can also
    /// build the firmware as part of the programming process.
    #[command(
        subcommand_value_name = "COMMAND",
        subcommand_help_heading = "Commands"
    )]
    Firmware(FirmwareArgs),

    /// List available One ROM plugins.
    ///
    /// Displays plugins from the release manifest with version and minimum
    /// firmware version information.
    Plugin(PluginArgs),

    /// List supported chip types.
    ///
    /// Displays the chip types supported by a specific board, or all chip types
    /// grouped by pin count.
    ///
    /// Examples:
    ///
    ///   onerom firmware chips --board fire-24-e
    ///
    ///   onerom firmware chips --all
    Chips(FirmwareChipsArgs),
}