roland_core/
lib.rs

1//! Core library for Roland VR-6HD remote control protocol
2//!
3//! This library provides the core functionality for communicating with
4//! Roland VR-6HD devices via LAN/RS-232 interface.
5//!
6//! # Features
7//!
8//! - `no_std` compatible (requires `alloc` for string operations)
9//! - Zero external dependencies
10//! - Pure protocol implementation
11
12#![no_std]
13
14extern crate alloc;
15
16use alloc::format;
17use alloc::string::{String, ToString};
18use alloc::vec::Vec;
19use core::fmt;
20
21/// Error types for Roland VR-6HD communication
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum RolandError {
24    /// Syntax error in received command
25    SyntaxError,
26    /// Invalid command due to other settings
27    Invalid,
28    /// Parameter out of range
29    OutOfRange,
30    /// Missing STX at command start (RS-232 only)
31    NoStx,
32    /// Unknown error code
33    UnknownError(u8),
34    /// Invalid address format
35    InvalidAddress,
36    /// Invalid value format
37    InvalidValue,
38    /// Invalid response format
39    InvalidResponse,
40}
41
42impl fmt::Display for RolandError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            RolandError::SyntaxError => write!(f, "Syntax error in received command"),
46            RolandError::Invalid => write!(f, "Invalid command due to other settings"),
47            RolandError::OutOfRange => write!(f, "Parameter out of range"),
48            RolandError::NoStx => write!(f, "Missing STX at command start"),
49            RolandError::UnknownError(code) => write!(f, "Unknown error code: {}", code),
50            RolandError::InvalidAddress => write!(f, "Invalid address format"),
51            RolandError::InvalidValue => write!(f, "Invalid value format"),
52            RolandError::InvalidResponse => write!(f, "Invalid response format"),
53        }
54    }
55}
56
57/// SysEx address (3 bytes)
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct Address {
60    /// High byte
61    pub high: u8,
62    /// Mid byte
63    pub mid: u8,
64    /// Low byte
65    pub low: u8,
66}
67
68impl Address {
69    /// Create a new address from three bytes
70    pub fn new(high: u8, mid: u8, low: u8) -> Self {
71        Self { high, mid, low }
72    }
73
74    /// Create an address from a hex string (6 hex digits)
75    ///
76    /// # Example
77    /// ```
78    /// use roland_core::Address;
79    /// let addr = Address::from_hex("123456").unwrap();
80    /// assert_eq!(addr.high, 0x12);
81    /// assert_eq!(addr.mid, 0x34);
82    /// assert_eq!(addr.low, 0x56);
83    /// ```
84    pub fn from_hex(hex: &str) -> Result<Self, RolandError> {
85        if hex.len() != 6 {
86            return Err(RolandError::InvalidAddress);
87        }
88
89        // Manual hex parsing to avoid std::str dependencies
90        let high = parse_hex_byte(&hex[0..2])?;
91        let mid = parse_hex_byte(&hex[2..4])?;
92        let low = parse_hex_byte(&hex[4..6])?;
93
94        Ok(Self { high, mid, low })
95    }
96
97    /// Convert address to hex string (6 hex digits, uppercase)
98    ///
99    /// Requires `alloc` for String allocation.
100    pub fn to_hex(&self) -> String {
101        format!("{:02X}{:02X}{:02X}", self.high, self.mid, self.low)
102    }
103
104    /// Write address as hex to a formatter
105    ///
106    /// This method doesn't require `alloc` and can be used in `no_std` environments
107    /// without heap allocation.
108    pub fn write_hex<W: fmt::Write>(&self, w: &mut W) -> fmt::Result {
109        write_hex_byte(w, self.high)?;
110        write_hex_byte(w, self.mid)?;
111        write_hex_byte(w, self.low)
112    }
113}
114
115/// Parse a single hex byte (2 hex digits)
116fn parse_hex_byte(s: &str) -> Result<u8, RolandError> {
117    if s.len() != 2 {
118        return Err(RolandError::InvalidAddress);
119    }
120
121    let mut result = 0u8;
122    for ch in s.chars() {
123        let digit = match ch {
124            '0'..='9' => ch as u8 - b'0',
125            'A'..='F' => ch as u8 - b'A' + 10,
126            'a'..='f' => ch as u8 - b'a' + 10,
127            _ => return Err(RolandError::InvalidAddress),
128        };
129        result = result * 16 + digit;
130    }
131    Ok(result)
132}
133
134/// Write a byte as hex (2 hex digits, uppercase)
135fn write_hex_byte<W: fmt::Write>(w: &mut W, byte: u8) -> fmt::Result {
136    let high = (byte >> 4) & 0x0F;
137    let low = byte & 0x0F;
138
139    let high_char = if high < 10 {
140        (b'0' + high) as char
141    } else {
142        (b'A' + high - 10) as char
143    };
144
145    let low_char = if low < 10 {
146        (b'0' + low) as char
147    } else {
148        (b'A' + low - 10) as char
149    };
150
151    w.write_char(high_char)?;
152    w.write_char(low_char)
153}
154
155/// Command types for VR-6HD
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum Command {
158    /// Write parameter (DTH)
159    WriteParameter {
160        /// SysEx address
161        address: Address,
162        /// Value to write (0-255)
163        value: u8,
164    },
165    /// Read parameter (RQH)
166    ReadParameter {
167        /// SysEx address
168        address: Address,
169        /// Size to read (typically 1 for single byte)
170        size: u32,
171    },
172    /// Get version information (VER)
173    GetVersion,
174}
175
176impl Command {
177    /// Encode command to string format
178    ///
179    /// For Telnet, STX (0x02) is optional and omitted here.
180    /// For RS-232, STX should be prepended by the transport layer.
181    ///
182    /// Requires `alloc` for String allocation.
183    pub fn encode(&self) -> String {
184        match self {
185            Command::WriteParameter { address, value } => {
186                format!("DTH:{},{:02X};", address.to_hex(), value)
187            }
188            Command::ReadParameter { address, size } => {
189                // Size is 3 bytes in hex (6 hex digits)
190                let size_hex = format!("{:06X}", size);
191                format!("RQH:{},{};", address.to_hex(), size_hex)
192            }
193            Command::GetVersion => "VER;".to_string(),
194        }
195    }
196
197    /// Encode command with STX prefix (for RS-232)
198    ///
199    /// Requires `alloc` for String allocation.
200    pub fn encode_with_stx(&self) -> String {
201        format!("\x02{}", self.encode())
202    }
203
204    /// Write command to a formatter
205    ///
206    /// This method doesn't require `alloc` and can be used in `no_std` environments
207    /// without heap allocation.
208    pub fn write<W: fmt::Write>(&self, w: &mut W) -> fmt::Result {
209        match self {
210            Command::WriteParameter { address, value } => {
211                w.write_str("DTH:")?;
212                address.write_hex(w)?;
213                w.write_str(",")?;
214                write_hex_byte(w, *value)?;
215                w.write_str(";")
216            }
217            Command::ReadParameter { address, size } => {
218                w.write_str("RQH:")?;
219                address.write_hex(w)?;
220                w.write_str(",")?;
221                // Size is 3 bytes in hex (6 hex digits)
222                write_hex_u24(w, *size)?;
223                w.write_str(";")
224            }
225            Command::GetVersion => w.write_str("VER;"),
226        }
227    }
228
229    /// Write command with STX prefix to a formatter
230    pub fn write_with_stx<W: fmt::Write>(&self, w: &mut W) -> fmt::Result {
231        w.write_char('\x02')?;
232        self.write(w)
233    }
234}
235
236/// Write a 24-bit value as hex (6 hex digits, uppercase)
237fn write_hex_u24<W: fmt::Write>(w: &mut W, value: u32) -> fmt::Result {
238    write_hex_byte(w, ((value >> 16) & 0xFF) as u8)?;
239    write_hex_byte(w, ((value >> 8) & 0xFF) as u8)?;
240    write_hex_byte(w, (value & 0xFF) as u8)
241}
242
243/// Response types from VR-6HD
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub enum Response {
246    /// Acknowledge (ack)
247    Acknowledge,
248    /// Data response (DTH)
249    Data {
250        /// SysEx address
251        address: Address,
252        /// Parameter value
253        value: u8,
254    },
255    /// Version information (VER)
256    Version {
257        /// Product name
258        product: String,
259        /// Version string
260        version: String,
261    },
262    /// Error response (ERR)
263    Error(RolandError),
264}
265
266impl Response {
267    /// Parse response from string slice
268    ///
269    /// Handles both Telnet (no STX) and RS-232 (with STX) formats.
270    ///
271    /// Requires `alloc` for String allocation in Version response.
272    pub fn parse(response: &str) -> Result<Self, RolandError> {
273        let response = response.trim();
274
275        // Remove STX if present (0x02)
276        let response = response.strip_prefix('\x02').unwrap_or(response);
277
278        // Handle ACK (0x06)
279        if response == "\x06" || response == "ack" {
280            return Ok(Response::Acknowledge);
281        }
282
283        // Handle XON/XOFF (flow control)
284        // These are handled by the transport layer, but we can detect them
285        if response == "\x11" || response == "xon" {
286            // XON - can continue sending (not an error, but transport layer should handle)
287            return Err(RolandError::InvalidResponse);
288        }
289        if response == "\x13" || response == "xoff" {
290            // XOFF - pause sending (not an error, but transport layer should handle)
291            return Err(RolandError::InvalidResponse);
292        }
293
294        // Parse DTH response: DTH:address,value;
295        if let Some(content) = response.strip_prefix("DTH:") {
296            if !content.ends_with(';') {
297                return Err(RolandError::InvalidResponse);
298            }
299            let content = &content[..content.len() - 1];
300            let parts: Vec<&str> = content.split(',').collect();
301            if parts.len() != 2 {
302                return Err(RolandError::InvalidResponse);
303            }
304            let address = Address::from_hex(parts[0])?;
305            let value = parse_hex_byte(parts[1])?;
306            return Ok(Response::Data { address, value });
307        }
308
309        // Parse VER response: VER:product,version;
310        if let Some(content) = response.strip_prefix("VER:") {
311            if !content.ends_with(';') {
312                return Err(RolandError::InvalidResponse);
313            }
314            let content = &content[..content.len() - 1];
315            let parts: Vec<&str> = content.split(',').collect();
316            if parts.len() != 2 {
317                return Err(RolandError::InvalidResponse);
318            }
319            return Ok(Response::Version {
320                product: parts[0].to_string(),
321                version: parts[1].to_string(),
322            });
323        }
324
325        // Parse ERR response: ERR:code;
326        if let Some(content) = response.strip_prefix("ERR:") {
327            if !content.ends_with(';') {
328                return Err(RolandError::InvalidResponse);
329            }
330            let content = &content[..content.len() - 1];
331            let code = parse_decimal_u8(content)?;
332            let error = match code {
333                0 => RolandError::SyntaxError,
334                4 => RolandError::Invalid,
335                5 => RolandError::OutOfRange,
336                6 => RolandError::NoStx,
337                _ => RolandError::UnknownError(code),
338            };
339            return Ok(Response::Error(error));
340        }
341
342        Err(RolandError::InvalidResponse)
343    }
344}
345
346/// Parse a decimal u8
347fn parse_decimal_u8(s: &str) -> Result<u8, RolandError> {
348    let mut result = 0u8;
349    for ch in s.chars() {
350        let digit = match ch {
351            '0'..='9' => ch as u8 - b'0',
352            _ => return Err(RolandError::InvalidResponse),
353        };
354        result = result
355            .checked_mul(10)
356            .and_then(|r| r.checked_add(digit))
357            .ok_or(RolandError::InvalidResponse)?;
358    }
359    Ok(result)
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_address_from_hex() {
368        let addr = Address::from_hex("123456").unwrap();
369        assert_eq!(addr.high, 0x12);
370        assert_eq!(addr.mid, 0x34);
371        assert_eq!(addr.low, 0x56);
372    }
373
374    #[test]
375    fn test_address_to_hex() {
376        let addr = Address::new(0x12, 0x34, 0x56);
377        assert_eq!(addr.to_hex(), "123456");
378    }
379
380    #[test]
381    fn test_address_write_hex() {
382        let addr = Address::new(0x12, 0x34, 0x56);
383        let mut s = String::new();
384        addr.write_hex(&mut s).unwrap();
385        assert_eq!(s, "123456");
386    }
387
388    #[test]
389    fn test_write_command() {
390        let cmd = Command::WriteParameter {
391            address: Address::from_hex("123456").unwrap(),
392            value: 0x01,
393        };
394        assert_eq!(cmd.encode(), "DTH:123456,01;");
395    }
396
397    #[test]
398    fn test_write_command_write() {
399        let cmd = Command::WriteParameter {
400            address: Address::from_hex("123456").unwrap(),
401            value: 0x01,
402        };
403        let mut s = String::new();
404        cmd.write(&mut s).unwrap();
405        assert_eq!(s, "DTH:123456,01;");
406    }
407
408    #[test]
409    fn test_read_command() {
410        let cmd = Command::ReadParameter {
411            address: Address::from_hex("123456").unwrap(),
412            size: 1,
413        };
414        assert_eq!(cmd.encode(), "RQH:123456,000001;");
415    }
416
417    #[test]
418    fn test_version_command() {
419        let cmd = Command::GetVersion;
420        assert_eq!(cmd.encode(), "VER;");
421    }
422
423    #[test]
424    fn test_parse_ack() {
425        let resp = Response::parse("\x06").unwrap();
426        assert_eq!(resp, Response::Acknowledge);
427    }
428
429    #[test]
430    fn test_parse_data() {
431        let resp = Response::parse("DTH:123456,01;").unwrap();
432        match resp {
433            Response::Data { address, value } => {
434                assert_eq!(address.to_hex(), "123456");
435                assert_eq!(value, 0x01);
436            }
437            _ => panic!("Expected Data response"),
438        }
439    }
440
441    #[test]
442    fn test_parse_version() {
443        let resp = Response::parse("VER:VR-6HD,1.00;").unwrap();
444        match resp {
445            Response::Version { product, version } => {
446                assert_eq!(product, "VR-6HD");
447                assert_eq!(version, "1.00");
448            }
449            _ => panic!("Expected Version response"),
450        }
451    }
452
453    #[test]
454    fn test_parse_error() {
455        let resp = Response::parse("ERR:0;").unwrap();
456        match resp {
457            Response::Error(RolandError::SyntaxError) => {}
458            _ => panic!("Expected SyntaxError"),
459        }
460    }
461}