1use std::io::{Read, Write};
2use std::net::TcpStream;
3use std::time::Duration;
4
5use thiserror::Error;
6
7use crate::crypto;
8
9#[derive(Debug, Clone)]
26pub struct DeviceConfig {
27 pub dev_id: String,
29 pub address: String,
31 pub local_key: String,
33 pub version: f32,
35 pub port: u16,
37}
38
39impl DeviceConfig {
40 pub fn from_env() -> Result<Self, String> {
51 Ok(Self {
52 dev_id: std::env::var("DEVICE_ID").map_err(|_| "DEVICE_ID not set")?,
53 address: std::env::var("DEVICE_IP").map_err(|_| "DEVICE_IP not set")?,
54 local_key: std::env::var("LOCAL_KEY").map_err(|_| "LOCAL_KEY not set")?,
55 ..Default::default()
56 })
57 }
58}
59
60impl Default for DeviceConfig {
61 fn default() -> Self {
62 Self {
63 dev_id: String::new(),
64 address: String::new(),
65 local_key: String::new(),
66 version: 3.3,
67 port: 6668,
68 }
69 }
70}
71
72#[derive(Debug, Error)]
74pub enum DeviceError {
75 #[error("TCP connection failed: {0}")]
77 ConnectionFailed(String),
78 #[error("AES decryption failed")]
80 DecryptionFailed,
81 #[error("socket timeout")]
83 Timeout,
84 #[error("invalid response: {0}")]
86 InvalidResponse(String),
87 #[error("connection dropped")]
89 Disconnected,
90}
91
92const MAGIC_PREFIX: u32 = 0x000055AA;
95const MAGIC_SUFFIX: u32 = 0x0000AA55;
96const MAX_PACKET_SIZE: usize = 65_536;
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[repr(u32)]
103pub enum TuyaCommand {
104 Control = 7,
106 Status = 8,
108 Heartbeat = 9,
110 DpQuery = 10,
112 UpdateDps = 18,
114}
115
116#[derive(Debug, Clone, PartialEq)]
118pub struct TuyaPacket {
119 pub seq_num: u32,
121 pub command: u32,
123 pub payload: Vec<u8>,
125}
126
127impl TuyaPacket {
128 pub fn to_bytes(&self, key: &[u8; 16]) -> Vec<u8> {
136 let encrypted = crypto::aes_ecb_encrypt(key, &self.payload);
137
138 let needs_header = !matches!(
140 self.command,
141 9 | 10 | 16 | 18 );
143
144 let header_bytes: &[u8] = if needs_header {
145 b"3.3\0\0\0\0\0\0\0\0\0\0\0\0"
146 } else {
147 b""
148 };
149 let data_len = header_bytes.len() + encrypted.len() + 8; let mut buf = Vec::with_capacity(16 + data_len);
152 buf.extend_from_slice(&MAGIC_PREFIX.to_be_bytes());
153 buf.extend_from_slice(&self.seq_num.to_be_bytes());
154 buf.extend_from_slice(&self.command.to_be_bytes());
155 buf.extend_from_slice(&(data_len as u32).to_be_bytes());
156 buf.extend_from_slice(header_bytes);
157 buf.extend_from_slice(&encrypted);
158
159 let crc = crc32fast::hash(&buf);
161 buf.extend_from_slice(&crc.to_be_bytes());
162 buf.extend_from_slice(&MAGIC_SUFFIX.to_be_bytes());
163
164 buf
165 }
166
167 pub fn from_bytes(data: &[u8], key: &[u8; 16]) -> Result<Self, DeviceError> {
179 if data.len() < 24 {
180 return Err(DeviceError::InvalidResponse("packet too short".into()));
181 }
182
183 let prefix = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
185 if prefix != MAGIC_PREFIX {
186 return Err(DeviceError::InvalidResponse(format!(
187 "bad prefix: 0x{prefix:08X}"
188 )));
189 }
190
191 let seq_num = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
192 let command = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
193 let total_len = u32::from_be_bytes([data[12], data[13], data[14], data[15]]) as usize;
194
195 let suffix_start = 16 + total_len - 4;
197 if data.len() < suffix_start + 4 {
198 return Err(DeviceError::InvalidResponse("packet truncated".into()));
199 }
200 let suffix = u32::from_be_bytes([
201 data[suffix_start],
202 data[suffix_start + 1],
203 data[suffix_start + 2],
204 data[suffix_start + 3],
205 ]);
206 if suffix != MAGIC_SUFFIX {
207 return Err(DeviceError::InvalidResponse(format!(
208 "bad suffix: 0x{suffix:08X}"
209 )));
210 }
211
212 let crc_start = suffix_start - 4;
214 let received_crc = u32::from_be_bytes([
215 data[crc_start],
216 data[crc_start + 1],
217 data[crc_start + 2],
218 data[crc_start + 3],
219 ]);
220 let computed_crc = crc32fast::hash(&data[..crc_start]);
221 if received_crc != computed_crc {
222 return Err(DeviceError::InvalidResponse(format!(
223 "CRC mismatch: received 0x{received_crc:08X}, computed 0x{computed_crc:08X}"
224 )));
225 }
226
227 let offset = 16usize;
228 let data_end = crc_start;
229
230 if data_end <= offset {
231 return Ok(TuyaPacket {
232 seq_num,
233 command,
234 payload: Vec::new(),
235 });
236 }
237
238 let raw = &data[offset..data_end];
239
240 if let Some(payload) = Self::try_decode(raw, key) {
242 return Ok(TuyaPacket {
243 seq_num,
244 command,
245 payload,
246 });
247 }
248
249 let hex: String = raw
251 .iter()
252 .take(64)
253 .map(|b| format!("{b:02x}"))
254 .collect::<Vec<_>>()
255 .join(" ");
256 Err(DeviceError::InvalidResponse(format!(
257 "cannot decode seq={seq_num} cmd={command} data={}b: {hex}{}",
258 raw.len(),
259 if raw.len() > 64 { "..." } else { "" }
260 )))
261 }
262
263 fn try_decode(raw: &[u8], key: &[u8; 16]) -> Option<Vec<u8>> {
274 if raw.is_empty() {
275 return Some(Vec::new());
276 }
277
278 let has_prefix_at = |off: usize| -> bool {
279 raw.len() > off + 2 && raw[off] == b'3' && raw[off + 1] == b'.' && raw[off + 2] == b'3'
280 };
281
282 let try_aes = |slice: &[u8]| -> Option<Vec<u8>> {
283 if !slice.is_empty() && slice.len().is_multiple_of(16) {
284 let decrypted = crypto::aes_ecb_decrypt(key, slice).ok()?;
285 if decrypted.is_empty() {
286 return Some(decrypted);
287 }
288 if decrypted.first() == Some(&b'{') {
291 Some(decrypted)
292 } else {
293 None
294 }
295 } else {
296 None
297 }
298 };
299
300 if has_prefix_at(0) && raw.len() > 15 {
302 let after_prefix = &raw[15..];
303 if let Some(payload) = try_aes(after_prefix) {
304 return Some(payload);
305 }
306
307 if after_prefix.len() >= 4 {
309 let retcode = u32::from_be_bytes(after_prefix[..4].try_into().unwrap());
310 let after_rc = &after_prefix[4..];
311 if retcode == 1 {
312 return Some(after_rc.to_vec()); }
314 if retcode == 0 {
315 if after_rc.is_empty() {
316 return Some(Vec::new());
317 }
318 if let Some(payload) = try_aes(after_rc) {
319 return Some(payload);
320 }
321 }
322 }
323 }
324
325 if raw.len() >= 4 {
327 let retcode = u32::from_be_bytes(raw[..4].try_into().unwrap());
328
329 if retcode <= 1 {
330 let after_rc = &raw[4..];
331
332 if after_rc.is_empty() {
334 return Some(Vec::new());
335 }
336
337 if retcode == 1 {
339 return Some(after_rc.to_vec());
340 }
341
342 if has_prefix_at(4) && after_rc.len() > 15 {
344 let after_both = &after_rc[15..];
345 if let Some(payload) = try_aes(after_both) {
346 return Some(payload);
347 }
348 }
349
350 if let Some(payload) = try_aes(after_rc) {
352 return Some(payload);
353 }
354 }
355 }
356
357 if let Some(payload) = try_aes(raw) {
359 return Some(payload);
360 }
361
362 if raw.iter().all(|&b| b.is_ascii_graphic() || b == b' ') {
364 return Some(raw.to_vec());
365 }
366
367 None
368 }
369}
370
371pub trait Transport {
378 fn dev_id(&self) -> &str;
380 fn send(&mut self, command: TuyaCommand, payload: Vec<u8>) -> Result<TuyaPacket, DeviceError>;
382 fn recv(&mut self) -> Result<TuyaPacket, DeviceError>;
384}
385
386pub struct TuyaConnection {
390 dev_id: String,
391 key: [u8; 16],
392 stream: TcpStream,
393 seq: u32,
394}
395
396impl TuyaConnection {
397 pub fn connect(config: &DeviceConfig) -> Result<Self, DeviceError> {
415 let addr = format!("{}:{}", config.address, config.port);
416 let sock_addr: std::net::SocketAddr = addr
417 .parse()
418 .map_err(|e: std::net::AddrParseError| DeviceError::ConnectionFailed(e.to_string()))?;
419
420 let stream = TcpStream::connect_timeout(&sock_addr, Duration::from_secs(5))
421 .map_err(|e| DeviceError::ConnectionFailed(e.to_string()))?;
422 stream
423 .set_read_timeout(Some(Duration::from_secs(5)))
424 .map_err(|e| DeviceError::ConnectionFailed(e.to_string()))?;
425
426 let key_bytes = config.local_key.as_bytes();
427 if key_bytes.len() != 16 {
428 return Err(DeviceError::ConnectionFailed(format!(
429 "local_key must be exactly 16 bytes, got {}",
430 key_bytes.len()
431 )));
432 }
433 let mut key = [0u8; 16];
434 key.copy_from_slice(key_bytes);
435
436 Ok(Self {
437 dev_id: config.dev_id.clone(),
438 key,
439 stream,
440 seq: 0,
441 })
442 }
443
444 fn recv_packet(&mut self) -> Result<TuyaPacket, DeviceError> {
446 let mut header = [0u8; 16];
447 self.read_exact(&mut header)?;
448
449 let prefix = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
450 if prefix != MAGIC_PREFIX {
451 return Err(DeviceError::InvalidResponse(format!(
452 "bad prefix: 0x{prefix:08X}"
453 )));
454 }
455
456 let data_len =
457 u32::from_be_bytes([header[12], header[13], header[14], header[15]]) as usize;
458
459 if data_len > MAX_PACKET_SIZE {
460 return Err(DeviceError::InvalidResponse(format!(
461 "packet too large: {data_len} bytes (max {MAX_PACKET_SIZE})"
462 )));
463 }
464
465 let mut rest = vec![0u8; data_len];
466 self.read_exact(&mut rest)?;
467
468 let mut full = Vec::with_capacity(16 + data_len);
469 full.extend_from_slice(&header);
470 full.extend_from_slice(&rest);
471
472 TuyaPacket::from_bytes(&full, &self.key)
473 }
474
475 fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), DeviceError> {
476 self.stream.read_exact(buf).map_err(|e| match e.kind() {
477 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => DeviceError::Timeout,
478 _ => DeviceError::Disconnected,
479 })
480 }
481}
482
483impl Transport for TuyaConnection {
484 fn dev_id(&self) -> &str {
485 &self.dev_id
486 }
487
488 fn send(&mut self, command: TuyaCommand, payload: Vec<u8>) -> Result<TuyaPacket, DeviceError> {
489 self.seq += 1;
490 let packet = TuyaPacket {
491 seq_num: self.seq,
492 command: command as u32,
493 payload,
494 };
495 let bytes = packet.to_bytes(&self.key);
496
497 self.stream
498 .write_all(&bytes)
499 .map_err(|_| DeviceError::Disconnected)?;
500
501 self.recv_packet()
502 }
503
504 fn recv(&mut self) -> Result<TuyaPacket, DeviceError> {
505 self.recv_packet()
506 }
507}
508
509pub fn build_dps_json(dev_id: &str, timestamp: u64, dps: &[(&str, serde_json::Value)]) -> String {
525 let mut dps_map = serde_json::Map::new();
526 for (k, v) in dps {
527 dps_map.insert(k.to_string(), v.clone());
528 }
529
530 serde_json::json!({
531 "devId": dev_id,
532 "uid": "",
533 "t": timestamp,
534 "dps": dps_map,
535 })
536 .to_string()
537}
538
539pub fn now() -> u64 {
541 std::time::SystemTime::now()
542 .duration_since(std::time::UNIX_EPOCH)
543 .unwrap()
544 .as_secs()
545}
546
547#[derive(Debug, Clone, PartialEq)]
549pub enum DpValue {
550 Boolean(bool),
552 Integer(i64),
554 String(String),
556 Raw(Vec<u8>),
558}
559
560#[derive(Debug, Clone, PartialEq)]
562pub struct DpsUpdate {
563 pub dps: Vec<(u8, String)>,
565 pub timestamp: Option<u64>,
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use serde_json::json;
573
574 #[test]
575 fn build_dps_json_format() {
576 let json = build_dps_json("devId123", 1770808371, &[("1", json!(true))]);
577 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
578 assert_eq!(parsed["devId"], "devId123");
579 assert_eq!(parsed["uid"], "");
580 assert_eq!(parsed["t"], 1770808371u64);
581 assert_eq!(parsed["dps"]["1"], true);
582 }
583
584 #[test]
587 fn device_config_default() {
588 let cfg = DeviceConfig::default();
589 assert!(cfg.dev_id.is_empty());
590 assert!(cfg.address.is_empty());
591 assert!(cfg.local_key.is_empty());
592 assert_eq!(cfg.version, 3.3);
593 assert_eq!(cfg.port, 6668);
594 }
595
596 #[test]
597 fn device_config_from_env_missing_vars() {
598 assert!(DeviceConfig::from_env().is_err());
600 }
601
602 #[test]
605 fn packet_encode_heartbeat_no_header() {
606 let key = b"0123456789abcdef";
607 let pkt = TuyaPacket {
608 seq_num: 1,
609 command: TuyaCommand::Heartbeat as u32,
610 payload: b"{}".to_vec(),
611 };
612 let bytes = pkt.to_bytes(key);
613 assert_ne!(&bytes[16..19], b"3.3");
616 }
617
618 #[test]
619 fn packet_encode_control_has_header() {
620 let key = b"0123456789abcdef";
621 let pkt = TuyaPacket {
622 seq_num: 1,
623 command: TuyaCommand::Control as u32,
624 payload: b"{}".to_vec(),
625 };
626 let bytes = pkt.to_bytes(key);
627 assert_eq!(&bytes[16..19], b"3.3");
628 }
629
630 fn build_raw_packet(seq: u32, cmd: u32, data: &[u8]) -> Vec<u8> {
634 let data_len = data.len() + 8; let mut buf = Vec::with_capacity(16 + data_len);
636 buf.extend_from_slice(&MAGIC_PREFIX.to_be_bytes());
637 buf.extend_from_slice(&seq.to_be_bytes());
638 buf.extend_from_slice(&cmd.to_be_bytes());
639 buf.extend_from_slice(&(data_len as u32).to_be_bytes());
640 buf.extend_from_slice(data);
641 let crc = crc32fast::hash(&buf);
642 buf.extend_from_slice(&crc.to_be_bytes());
643 buf.extend_from_slice(&MAGIC_SUFFIX.to_be_bytes());
644 buf
645 }
646
647 #[test]
648 fn from_bytes_too_short() {
649 let key = b"0123456789abcdef";
650 let err = TuyaPacket::from_bytes(&[0; 20], key).unwrap_err();
651 assert!(matches!(err, DeviceError::InvalidResponse(_)));
652 }
653
654 #[test]
655 fn from_bytes_bad_prefix() {
656 let key = b"0123456789abcdef";
657 let mut pkt = build_raw_packet(1, 7, b"");
658 pkt[0] = 0xFF; let err = TuyaPacket::from_bytes(&pkt, key).unwrap_err();
660 let msg = format!("{err}");
661 assert!(msg.contains("bad prefix"));
662 }
663
664 #[test]
665 fn from_bytes_bad_suffix() {
666 let key = b"0123456789abcdef";
667 let mut pkt = build_raw_packet(1, 7, b"some data here!!");
668 let last = pkt.len();
669 pkt[last - 1] = 0xFF; let err = TuyaPacket::from_bytes(&pkt, key).unwrap_err();
671 let msg = format!("{err}");
672 assert!(msg.contains("bad suffix"));
673 }
674
675 #[test]
676 fn from_bytes_truncated() {
677 let key = b"0123456789abcdef";
678 let mut buf = Vec::new();
680 buf.extend_from_slice(&MAGIC_PREFIX.to_be_bytes());
681 buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&7u32.to_be_bytes()); buf.extend_from_slice(&255u32.to_be_bytes()); buf.extend_from_slice(&[0u8; 8]); let err = TuyaPacket::from_bytes(&buf, key).unwrap_err();
686 let msg = format!("{err}");
687 assert!(msg.contains("truncated"));
688 }
689
690 #[test]
691 fn from_bytes_crc_mismatch() {
692 let key = b"0123456789abcdef";
693 let mut pkt = build_raw_packet(1, 7, b"some data here!!");
694 pkt[16] ^= 0xFF;
696 let err = TuyaPacket::from_bytes(&pkt, key).unwrap_err();
697 let msg = format!("{err}");
698 assert!(msg.contains("CRC mismatch"));
699 }
700
701 #[test]
702 fn from_bytes_empty_payload() {
703 let key = b"0123456789abcdef";
704 let pkt = build_raw_packet(42, 9, &[]);
705 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
706 assert_eq!(decoded.seq_num, 42);
707 assert_eq!(decoded.command, 9);
708 assert!(decoded.payload.is_empty());
709 }
710
711 #[test]
712 fn from_bytes_undecryptable_data() {
713 let key = b"0123456789abcdef";
714 let garbage = [0x80u8; 16];
716 let pkt = build_raw_packet(1, 7, &garbage);
717 let err = TuyaPacket::from_bytes(&pkt, key).unwrap_err();
718 let msg = format!("{err}");
719 assert!(msg.contains("cannot decode"));
720 }
721
722 #[test]
725 fn decode_format_a_prefix_plus_encrypted() {
726 let key = b"0123456789abcdef";
729 let pkt = TuyaPacket {
730 seq_num: 1,
731 command: TuyaCommand::Status as u32,
732 payload: b"{\"dps\":{\"1\":true}}".to_vec(),
733 };
734 let bytes = pkt.to_bytes(key);
735 let decoded = TuyaPacket::from_bytes(&bytes, key).unwrap();
736 assert_eq!(decoded.payload, pkt.payload);
737 }
738
739 #[test]
740 fn decode_format_b_prefix_retcode0_encrypted() {
741 let key = b"0123456789abcdef";
742 let plaintext = b"{\"result\":\"ok\"}";
743 let encrypted = crypto::aes_ecb_encrypt(key, plaintext);
744
745 let mut data = Vec::new();
746 data.extend_from_slice(b"3.3\0\0\0\0\0\0\0\0\0\0\0\0"); data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&encrypted);
749
750 let pkt = build_raw_packet(1, 8, &data);
751 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
752 assert_eq!(decoded.payload, plaintext);
753 }
754
755 #[test]
756 fn decode_format_b_prefix_retcode0_empty() {
757 let key = b"0123456789abcdef";
758 let mut data = Vec::new();
759 data.extend_from_slice(b"3.3\0\0\0\0\0\0\0\0\0\0\0\0"); data.extend_from_slice(&0u32.to_be_bytes()); let pkt = build_raw_packet(1, 8, &data);
764 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
765 assert!(decoded.payload.is_empty());
766 }
767
768 #[test]
769 fn decode_format_b_prefix_retcode1_plaintext_error() {
770 let key = b"0123456789abcdef";
771 let mut data = Vec::new();
772 data.extend_from_slice(b"3.3\0\0\0\0\0\0\0\0\0\0\0\0"); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(b"parse data error"); let pkt = build_raw_packet(1, 8, &data);
777 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
778 assert_eq!(decoded.payload, b"parse data error");
779 }
780
781 #[test]
782 fn decode_format_c_retcode0_prefix_encrypted() {
783 let key = b"0123456789abcdef";
784 let plaintext = b"{\"status\":\"ok\"}";
785 let encrypted = crypto::aes_ecb_encrypt(key, plaintext);
786
787 let mut data = Vec::new();
788 data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(b"3.3\0\0\0\0\0\0\0\0\0\0\0\0"); data.extend_from_slice(&encrypted);
791
792 let pkt = build_raw_packet(1, 8, &data);
793 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
794 assert_eq!(decoded.payload, plaintext);
795 }
796
797 #[test]
798 fn decode_format_d_retcode0_encrypted() {
799 let key = b"0123456789abcdef";
800 let plaintext = b"{\"dps\":{\"8\":72}}";
801 let encrypted = crypto::aes_ecb_encrypt(key, plaintext);
802
803 let mut data = Vec::new();
804 data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(&encrypted);
806
807 let pkt = build_raw_packet(1, 10, &data);
808 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
809 assert_eq!(decoded.payload, plaintext);
810 }
811
812 #[test]
813 fn decode_format_e_retcode1_plaintext_error() {
814 let key = b"0123456789abcdef";
815 let mut data = Vec::new();
816 data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(b"json parse error");
818
819 let pkt = build_raw_packet(1, 10, &data);
820 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
821 assert_eq!(decoded.payload, b"json parse error");
822 }
823
824 #[test]
825 fn decode_format_f_retcode_only() {
826 let key = b"0123456789abcdef";
827 let data = 0u32.to_be_bytes(); let pkt = build_raw_packet(1, 7, &data);
830 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
831 assert!(decoded.payload.is_empty());
832 }
833
834 #[test]
835 fn decode_format_g_bare_encrypted() {
836 let key = b"0123456789abcdef";
837 let plaintext = b"{\"bare\":true}";
838 let encrypted = crypto::aes_ecb_encrypt(key, plaintext);
839
840 let pkt = build_raw_packet(1, 10, &encrypted);
842 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
843 assert_eq!(decoded.payload, plaintext);
844 }
845
846 #[test]
847 fn decode_plaintext_fallback() {
848 let key = b"0123456789abcdef";
849 let data = b"PLAIN TEXT ERROR MSG";
852
853 let pkt = build_raw_packet(1, 7, data);
854 let decoded = TuyaPacket::from_bytes(&pkt, key).unwrap();
855 assert_eq!(decoded.payload, b"PLAIN TEXT ERROR MSG");
856 }
857
858 #[test]
859 fn decode_hex_dump_truncated_at_64() {
860 let key = b"0123456789abcdef";
861 let garbage: Vec<u8> = (0..80).map(|i| 0x80 | (i & 0x0F)).collect();
863 let pkt = build_raw_packet(1, 7, &garbage);
864 let err = TuyaPacket::from_bytes(&pkt, key).unwrap_err();
865 let msg = format!("{err}");
866 assert!(msg.contains("..."));
867 }
868}