Skip to main content

bambu_rs/
camera.rs

1//! Camera snapshot — the A1/P1 chamber-image stream.
2//!
3//! Proprietary JPEG stream over **TLS on TCP 6000** (see `docs/protocol.md`):
4//! send an 80-byte auth packet (`bblp` + access code), then read framed JPEGs
5//! (16-byte header whose first u32 is the JPEG length). Self-signed v1 cert, so it
6//! shares the LAN rustls config ([`crate::tls`]) that accepts any certificate.
7//!
8//! Note: the A1 camera is intermittently unavailable (a firmware quirk); when it
9//! isn't streaming, the connection is accepted but no frame arrives, surfaced
10//! here as a read timeout.
11
12use std::io::{Read, Write};
13use std::net::TcpStream;
14use std::time::Duration;
15
16use rustls::pki_types::ServerName;
17use rustls::{ClientConnection, StreamOwned};
18
19use crate::config::ResolvedTarget;
20
21const CAMERA_PORT: u16 = 6000;
22const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
23
24/// Errors from a camera snapshot. Messages never include the access code.
25#[derive(Debug, thiserror::Error)]
26pub enum CameraError {
27    #[error("TLS setup failed: {0}")]
28    Tls(String),
29    #[error("camera connection failed: {0}")]
30    Connect(String),
31    #[error("camera i/o error (no frame? the A1 camera is often off): {0}")]
32    Io(#[from] std::io::Error),
33    #[error("unexpected frame header (jpeg length {0}); framing differs or camera off")]
34    BadFrame(u32),
35}
36
37/// The 80-byte auth packet (header + 32-byte user + 32-byte access code).
38fn auth_packet(access_code: &str) -> Vec<u8> {
39    let mut p = Vec::with_capacity(80);
40    p.extend_from_slice(&0x40u32.to_le_bytes());
41    p.extend_from_slice(&0x3000u32.to_le_bytes());
42    p.extend_from_slice(&0u32.to_le_bytes());
43    p.extend_from_slice(&0u32.to_le_bytes());
44    let mut field = [0u8; 32];
45    let user = b"bblp";
46    field[..user.len()].copy_from_slice(user);
47    p.extend_from_slice(&field);
48    let mut field = [0u8; 32];
49    let code = access_code.as_bytes();
50    let n = code.len().min(32);
51    field[..n].copy_from_slice(&code[..n]);
52    p.extend_from_slice(&field);
53    p
54}
55
56/// A one-shot camera client.
57pub struct CameraClient {
58    target: ResolvedTarget,
59    timeout: Duration,
60}
61
62impl CameraClient {
63    pub fn new(target: ResolvedTarget) -> Self {
64        Self {
65            target,
66            timeout: DEFAULT_TIMEOUT,
67        }
68    }
69
70    pub fn with_timeout(mut self, timeout: Duration) -> Self {
71        self.timeout = timeout;
72        self
73    }
74
75    /// Grab a single JPEG frame from the chamber camera.
76    pub fn snapshot(&self) -> Result<Vec<u8>, CameraError> {
77        let config =
78            crate::tls::lan_client_config().map_err(|e| CameraError::Tls(e.to_string()))?;
79        let tcp = TcpStream::connect((self.target.ip.as_str(), CAMERA_PORT))?;
80        tcp.set_read_timeout(Some(self.timeout))?;
81        tcp.set_write_timeout(Some(self.timeout))?;
82        let server_name = ServerName::try_from(self.target.ip.clone())
83            .map_err(|e| CameraError::Tls(e.to_string()))?;
84        let conn = ClientConnection::new(config, server_name)
85            .map_err(|e| CameraError::Tls(e.to_string()))?;
86        let mut tls = StreamOwned::new(conn, tcp);
87
88        tls.write_all(&auth_packet(&self.target.access_code))?;
89
90        let mut header = [0u8; 16];
91        tls.read_exact(&mut header)?;
92        let jpeg_len = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
93        if !(1000..=8_000_000).contains(&jpeg_len) {
94            return Err(CameraError::BadFrame(jpeg_len));
95        }
96        let mut jpeg = vec![0u8; jpeg_len as usize];
97        tls.read_exact(&mut jpeg)?;
98        Ok(jpeg)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn auth_packet_layout() {
108        let p = auth_packet("12345678");
109        assert_eq!(p.len(), 80);
110        assert_eq!(&p[0..4], &0x40u32.to_le_bytes()); // payload size
111        assert_eq!(&p[4..8], &0x3000u32.to_le_bytes()); // type
112        assert_eq!(&p[16..20], b"bblp"); // username, null-padded
113        assert_eq!(p[20], 0);
114        assert_eq!(&p[48..56], b"12345678"); // access code at offset 48
115    }
116}