usbc_cable_caps/lib.rs
1//! What a USB-C cable can carry, derived from its class rather than its packaging.
2//!
3//! Every USB-C plug is the same shape, and almost nothing else about USB-C cables
4//! is the same. A cable may carry 240 W and 480 Mbps, or 60 W and 40 Gbps, or any
5//! other combination — the connector tells you nothing, and the printed marketing
6//! is frequently wrong or silent.
7//!
8//! The two facts that actually decide capability:
9//!
10//! 1. **Current above 3 A requires an e-marker chip** in the plug that declares the
11//! rating. No chip means the source is obliged to stay at 3 A regardless of what
12//! either end could do.
13//! 2. **Data rate and power are independent.** The cheapest charging cable and a
14//! Thunderbolt cable both charge a laptop; only one carries a display.
15
16/// Data capability of a cable, in the order the standards actually arrived.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DataClass {
19 /// USB 2.0 only — 480 Mbps. The majority of bundled charging cables.
20 Usb2,
21 /// 5 Gbps (USB 3.2 Gen 1, once called USB 3.0).
22 Gen1,
23 /// 10 Gbps (USB 3.2 Gen 2).
24 Gen2,
25 /// 20 Gbps (USB 3.2 Gen 2x2).
26 Gen2x2,
27 /// 40 Gbps (USB4 / Thunderbolt 3 and 4).
28 Usb4,
29}
30
31impl DataClass {
32 /// Advertised throughput in megabits per second.
33 pub fn mbps(self) -> u32 {
34 match self {
35 DataClass::Usb2 => 480,
36 DataClass::Gen1 => 5_000,
37 DataClass::Gen2 => 10_000,
38 DataClass::Gen2x2 => 20_000,
39 DataClass::Usb4 => 40_000,
40 }
41 }
42
43 /// Whether the cable can carry a DisplayPort signal to an external monitor.
44 ///
45 /// This is the single most common disappointment: a cable that charges a
46 /// laptop perfectly well and shows nothing on the monitor. USB 2.0 cables
47 /// lack the high-speed pairs the alternate mode needs.
48 pub fn carries_video(self) -> bool {
49 !matches!(self, DataClass::Usb2)
50 }
51}
52
53/// A cable as it physically exists, not as it is marketed.
54#[derive(Debug, Clone, Copy)]
55pub struct Cable {
56 pub data: DataClass,
57 /// Whether an e-marker chip is present in the plug.
58 pub emarker: bool,
59 /// Current the e-marker declares, in amps. Ignored when `emarker` is false.
60 pub declared_amps: f64,
61 pub length_m: f64,
62}
63
64impl Cable {
65 /// Current the cable is permitted to carry, in amps.
66 ///
67 /// Without an e-marker the ceiling is 3 A no matter how thick the copper is.
68 /// This is a protocol limit, not a thermal one — the source has no way to
69 /// learn the cable is capable, so it must assume it is not.
70 pub fn allowed_amps(&self) -> f64 {
71 if self.emarker { self.declared_amps.min(5.0) } else { 3.0 }
72 }
73
74 /// Maximum power the cable can pass at a given negotiated voltage, in watts.
75 pub fn max_watts_at(&self, volts: f64) -> f64 {
76 volts * self.allowed_amps()
77 }
78
79 /// Whether the cable can reach a target wattage at a given voltage.
80 pub fn can_deliver(&self, target_watts: f64, volts: f64) -> bool {
81 self.max_watts_at(volts) >= target_watts
82 }
83
84 /// Whether passive operation is plausible at this length for the data class.
85 ///
86 /// Signal loss scales with length and with frequency, so the faster the class
87 /// the shorter the passive cable can be. A 3 m 40 Gbps cable exists, but it
88 /// contains active signal conditioning and costs accordingly — which is why
89 /// long "Thunderbolt" cables are expensive and short ones are not.
90 pub fn passive_length_is_plausible(&self) -> bool {
91 let limit_m = match self.data {
92 DataClass::Usb2 => 4.0,
93 DataClass::Gen1 => 3.0,
94 DataClass::Gen2 | DataClass::Gen2x2 => 1.0,
95 DataClass::Usb4 => 0.8,
96 };
97 self.length_m <= limit_m
98 }
99}
100
101/// The cable most people already own: bundled, unmarked, USB 2.0, no e-marker.
102pub fn typical_bundled() -> Cable {
103 Cable { data: DataClass::Usb2, emarker: false, declared_amps: 0.0, length_m: 1.0 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn the_bundled_cable_caps_a_laptop_charger() {
112 let c = typical_bundled();
113 // A 100 W laptop charger negotiates 20 V; 3 A caps it at 60 W.
114 assert!(!c.can_deliver(100.0, 20.0));
115 assert_eq!(c.max_watts_at(20.0), 60.0);
116 }
117
118 #[test]
119 fn the_emarker_not_the_copper_lifts_the_ceiling() {
120 let thick_but_dumb = Cable { emarker: false, declared_amps: 5.0, ..typical_bundled() };
121 let marked = Cable { emarker: true, declared_amps: 5.0, ..typical_bundled() };
122 assert_eq!(thick_but_dumb.allowed_amps(), 3.0, "declared amps are ignored without a chip");
123 assert_eq!(marked.allowed_amps(), 5.0);
124 }
125
126 #[test]
127 fn charging_well_and_carrying_video_are_unrelated() {
128 let charger = Cable { data: DataClass::Usb2, emarker: true, declared_amps: 5.0, length_m: 2.0 };
129 assert!(charger.can_deliver(100.0, 20.0), "it charges a laptop fine");
130 assert!(!charger.data.carries_video(), "and still shows nothing on a monitor");
131 }
132
133 #[test]
134 fn speed_shortens_the_plausible_passive_cable() {
135 let long_slow = Cable { data: DataClass::Usb2, length_m: 3.0, ..typical_bundled() };
136 let long_fast = Cable { data: DataClass::Usb4, length_m: 3.0, ..typical_bundled() };
137 assert!(long_slow.passive_length_is_plausible());
138 assert!(!long_fast.passive_length_is_plausible(),
139 "a passive 3 m 40 Gbps cable should be treated as implausible");
140 }
141}