onerom_protocol/
lab.rs

1//! One ROM Protocol - One ROM Lab support
2//!
3//! Used by both the One ROM Lab firmware and host tools to communicate.
4//!
5//! Uses `airfrog-rpc` for the underlying RPC transport.
6//!
7//! The host can retrieve the RAM metada using `sdrr-fw-parser` which provides
8//! the RAM channel addresses required for RPC communication.
9//!
10//! See `airfrog::firmware::onerom_lab` for example host usage.
11
12// Copyright (c) 2025 Piers Finlayson <piers@piers.rocks>
13//
14// MIT licence
15
16use alloc::string::{String, ToString};
17use alloc::vec;
18use alloc::vec::Vec;
19#[allow(unused_imports)]
20use log::{debug, error, info, trace, warn};
21
22use onerom_database::{RomEntry, RomType};
23
24use crate::Error;
25
26/// Commands supported by One ROM Lab
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[repr(u32)]
29pub enum Command {
30    /// Response with a Pong.  No data follows.
31    Ping = 0x0000_0000,
32
33    /// Trigger a read of the connected ROM.  No data follows.
34    ReadRom = 0x0000_0001,
35
36    /// Trigger a read of a set of raw data associated with the last ROM read.
37    /// Data follows - see [`GetRawData`].
38    GetRawData = 0x0000_0002,
39
40    /// Unknown command, do not use.  No data follows.
41    Unknown = 0xFFFF_FFFF,
42}
43
44impl From<u32> for Command {
45    fn from(value: u32) -> Self {
46        match value {
47            0x0000_0000 => Command::Ping,
48            0x0000_0001 => Command::ReadRom,
49
50            _ => Command::Unknown,
51        }
52    }
53}
54
55impl From<Command> for u32 {
56    fn from(cmd: Command) -> Self {
57        cmd as u32
58    }
59}
60
61impl Command {
62    pub fn size() -> usize {
63        core::mem::size_of::<Self>()
64    }
65
66    pub fn as_bytes(&self) -> [u8; 4] {
67        (*self as u32).to_le_bytes()
68    }
69}
70
71/// Responses from One ROM Lab to Commands
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[repr(u32)]
74pub enum Response {
75    /// Ping response
76    Pong = 0x0000_0000,
77
78    /// ReadRom successful response.  Following this word, is the ROM metadata
79    /// as a sequence of bytes.  See [`LabRomEntry`].
80    /// - Name of the ROM, followed by 0
81    /// - Part number of the ROM, followed by 0
82    /// - 32-bit wrapping checksum of the ROM, little endian encoded
83    /// - 20 byte SHA1 digest of the ROM
84    RomEntry = 0x0000_0001,
85
86    /// ROM (probably) connected but not recognised.  No data follows
87    RomNotRecognised = 0x0000_0002,
88
89    /// One ROM Lab hit an error
90    Error = 0x8000_0000,
91
92    /// One ROM Lab did not detect a ROM connected, but it may have been
93    /// unrecognised
94    NoRom = 0x8000_0001,
95
96    Unknown = 0xFFFF_FFFF,
97}
98
99impl Response {
100    pub const fn size() -> usize {
101        core::mem::size_of::<Self>()
102    }
103
104    pub fn to_bytes(&self, buf: &mut [u8]) {
105        let value = *self as u32;
106        buf[..4].copy_from_slice(&value.to_le_bytes());
107    }
108}
109
110impl From<u32> for Response {
111    fn from(value: u32) -> Self {
112        match value {
113            0x0000_0000 => Response::Pong,
114            0x0000_0001 => Response::RomEntry,
115            0x0000_0002 => Response::RomNotRecognised,
116            0x8000_0000 => Response::Error,
117            0x8000_0001 => Response::NoRom,
118            _ => Response::Unknown,
119        }
120    }
121}
122
123#[derive(Debug, serde::Serialize, serde::Deserialize)]
124pub struct GetRawData {
125    /// The type of ROM to get information for
126    rom_type: RomType,
127}
128
129impl GetRawData {
130    const fn binary_size() -> usize {
131        RomType::binary_size()
132    }
133
134    pub fn from_buffer(buf: &[u8]) -> Result<Self, Error> {
135        // Get RomType
136        let rom_type_size = Self::binary_size();
137        if buf.len() < rom_type_size {
138            return Err(Error::BufferTooSmall);
139        }
140        let rom_type = RomType::from_bytes(&buf[0..rom_type_size])?;
141
142        Ok(Self { rom_type })
143    }
144
145    pub fn to_buffer(&self) -> Result<Vec<u8>, Error> {
146        let mut pos = 0;
147
148        let size = Self::binary_size() + Command::size();
149        let mut buf = vec![0; size];
150
151        // Write Response code
152        if size < Command::size() {
153            return Err(Error::BufferTooSmall);
154        }
155        let rsp_u32 = Command::GetRawData as u32;
156        buf[pos..pos + 4].copy_from_slice(&rsp_u32.to_le_bytes());
157        pos += 4;
158
159        // Write RomType
160        let rom_type_size = RomType::binary_size();
161        if size < pos + rom_type_size {
162            return Err(Error::BufferTooSmall);
163        }
164        self.rom_type.to_bytes(&mut buf[pos..pos + rom_type_size])?;
165
166        Ok(buf)
167    }
168}
169
170#[derive(Debug, serde::Serialize, serde::Deserialize)]
171pub struct LabRomType {
172    #[serde(rename = "ROM Type")]
173    rom_type: RomType,
174}
175
176#[derive(Debug, serde::Serialize, serde::Deserialize)]
177pub struct RomRawData {
178    #[serde(rename = "ROM Type")]
179    rom_type: RomType,
180    #[serde(rename = "Checksum")]
181    checksum: u32,
182    #[serde(rename = "SHA1 Digest")]
183    sha1: [u8; 20],
184}
185
186impl RomRawData {
187    const fn binary_size() -> usize {
188        RomType::binary_size() + 4 + 20
189    }
190
191    pub fn from_buffer(buf: &[u8]) -> Result<Self, Error> {
192        let mut pos = 0;
193
194        // Read RomType
195        let rom_type_size = RomType::binary_size();
196        if buf.len() < pos + rom_type_size {
197            return Err(Error::BufferTooSmall);
198        }
199        let rom_type = RomType::from_bytes(&buf[pos..pos + rom_type_size])?;
200        pos += rom_type_size;
201
202        // Read 32-bit checksum (little endian)
203        if buf.len() < pos + 4 {
204            return Err(Error::BufferTooSmall);
205        }
206        let checksum = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
207        pos += 4;
208
209        // Read 20-byte SHA1
210        if buf.len() < pos + 20 {
211            return Err(Error::BufferTooSmall);
212        }
213        let mut sha1 = [0u8; 20];
214        sha1.copy_from_slice(&buf[pos..pos + 20]);
215
216        Ok(Self {
217            rom_type,
218            checksum,
219            sha1,
220        })
221    }
222
223    pub fn to_buffer(&self) -> Result<Vec<u8>, Error> {
224        let mut pos = 0;
225
226        let size = Self::binary_size();
227        let mut buf = vec![0; size];
228
229        // Write RomType
230        let rom_type_size = RomType::binary_size();
231        if size < pos + rom_type_size {
232            return Err(Error::BufferTooSmall);
233        }
234        self.rom_type.to_bytes(&mut buf[pos..pos + rom_type_size])?;
235        pos += rom_type_size;
236
237        // Write 32-bit checksum (little endian)
238        if size < pos + 4 {
239            return Err(Error::BufferTooSmall);
240        }
241        buf[pos..pos + 4].copy_from_slice(&self.checksum.to_le_bytes());
242        pos += 4;
243
244        // Write 20-byte SHA1
245        if size < pos + 20 {
246            return Err(Error::BufferTooSmall);
247        }
248        buf[pos..pos + 20].copy_from_slice(&self.sha1);
249
250        Ok(buf)
251    }
252}
253
254/// Response data for RomMetadata
255#[derive(Debug, serde::Serialize, serde::Deserialize)]
256pub struct LabRomEntry {
257    #[serde(rename = "Name")]
258    name: String,
259    #[serde(rename = "Part Number")]
260    part_number: String,
261    #[serde(rename = "Checksum")]
262    checksum: u32,
263    #[serde(rename = "SHA1 Digest")]
264    sha1: [u8; 20],
265}
266
267impl LabRomEntry {
268    /// Get RomMetadata from the appropriate response
269    pub fn from_buffer(buf: &[u8]) -> Result<Self, Error> {
270        let mut pos = 0;
271
272        // Get Response code
273        if buf.len() < 4 {
274            return Err(Error::BufferTooSmall);
275        }
276        let rsp_u32 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
277        let response: Response = rsp_u32.into();
278        pos += 4;
279        match response {
280            Response::RomEntry => (), // Continue
281            Response::NoRom => Err(Error::NoRom)?,
282            Response::RomNotRecognised => Err(Error::RomNotRecognised)?,
283            _ => {
284                warn!("Unexpected response code for RomMetadata: {rsp_u32:#010X} {response:?}");
285                Err(Error::InvalidResponse)?
286            }
287        }
288
289        // Parse name (null-terminated string)
290        let name_end = buf[pos..].iter().position(|&b| b == 0).ok_or_else(|| {
291            warn!("Name string not null-terminated");
292            Error::InvalidData
293        })?;
294        let name = String::from_utf8(buf[pos..pos + name_end].to_vec()).map_err(|_| {
295            warn!("Invalid UTF-8 in name");
296            Error::InvalidData
297        })?;
298        pos += name_end + 1; // Skip null terminator
299
300        // Parse part number (null-terminated string)
301        let part_end = buf[pos..].iter().position(|&b| b == 0).ok_or_else(|| {
302            warn!("Part number string not null-terminated");
303            Error::InvalidData
304        })?;
305        let part_number = String::from_utf8(buf[pos..pos + part_end].to_vec()).map_err(|_| {
306            warn!("Invalid UTF-8 in part number");
307            Error::InvalidData
308        })?;
309        pos += part_end + 1; // Skip null terminator
310
311        // Parse 32-bit checksum (little endian)
312        if buf.len() < pos + 4 {
313            warn!("Buffer too short for checksum");
314            return Err(Error::BufferTooSmall);
315        }
316        let checksum = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
317        pos += 4;
318
319        // Parse 20-byte SHA1
320        if buf.len() < pos + 20 {
321            warn!("Buffer too short for SHA1 Digest");
322            return Err(Error::BufferTooSmall);
323        }
324        let mut sha1 = [0u8; 20];
325        sha1.copy_from_slice(&buf[pos..pos + 20]);
326
327        Ok(LabRomEntry {
328            name,
329            part_number,
330            checksum,
331            sha1,
332        })
333    }
334
335    fn buf_size(&self) -> usize {
336        Response::size() + self.name.len() + 1 + self.part_number.len() + 1 + 4 + 20
337    }
338
339    pub fn to_buffer(&self) -> Result<Vec<u8>, Error> {
340        let mut pos = 0;
341
342        let size = self.buf_size();
343        let mut buf = vec![0; size];
344
345        // Write Response code
346        if size < 4 {
347            return Err(Error::BufferTooSmall);
348        }
349        let rsp_u32 = Response::RomEntry as u32;
350        buf[pos..pos + 4].copy_from_slice(&rsp_u32.to_le_bytes());
351        pos += 4;
352
353        // Write name (null-terminated string)
354        let name_bytes = self.name.as_bytes();
355        if size < pos + name_bytes.len() + 1 {
356            return Err(Error::BufferTooSmall);
357        }
358        buf[pos..pos + name_bytes.len()].copy_from_slice(name_bytes);
359        pos += name_bytes.len();
360        buf[pos] = 0; // Null terminator
361        pos += 1;
362
363        // Write part number (null-terminated string)
364        let part_bytes = self.part_number.as_bytes();
365        if size < pos + part_bytes.len() + 1 {
366            return Err(Error::BufferTooSmall);
367        }
368        buf[pos..pos + part_bytes.len()].copy_from_slice(part_bytes);
369        pos += part_bytes.len();
370        buf[pos] = 0; // Null terminator
371        pos += 1;
372
373        // Write 32-bit checksum (little endian)
374        if size < pos + 4 {
375            return Err(Error::BufferTooSmall);
376        }
377        buf[pos..pos + 4].copy_from_slice(&self.checksum.to_le_bytes());
378        pos += 4;
379
380        // Write 20-byte SHA1
381        if size < pos + 20 {
382            return Err(Error::BufferTooSmall);
383        }
384        buf[pos..pos + 20].copy_from_slice(&self.sha1);
385
386        Ok(buf)
387    }
388}
389
390impl From<RomEntry> for LabRomEntry {
391    fn from(entry: RomEntry) -> Self {
392        LabRomEntry {
393            name: entry.name().to_string(),
394            part_number: entry.part().to_string(),
395            checksum: entry.sum(),
396            sha1: *entry.sha1(),
397        }
398    }
399}