1use std::time::{Duration, Instant};
31
32use nusb::transfer::{ControlIn, ControlOut, ControlType, Recipient};
33use nusb::{Interface, MaybeFuture};
34use restorekit_dongle_proto as proto;
37
38use crate::device::{self, Device, APPLE_VID};
39use crate::error::{Error, Result};
40
41pub const DONGLE_VID: u16 = proto::VID;
43pub const DONGLE_PID: u16 = proto::PID;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
58#[serde(rename_all = "kebab-case")]
59pub enum DongleModel {
60 Lite,
62}
63
64impl DongleModel {
65 pub fn from_product(product: &str) -> Option<Self> {
68 match product {
69 proto::PRODUCT_LITE => Some(Self::Lite),
70 _ => None,
71 }
72 }
73
74 fn release_tag_prefix(self) -> &'static str {
76 match self {
77 Self::Lite => "dongle-lite-fw-v",
78 }
79 }
80
81 fn release_asset(self) -> &'static str {
83 match self {
84 Self::Lite => "dongle-lite-fw.bin",
85 }
86 }
87}
88
89const FW_RELEASE_REPO: &str = "fcjr/restorekit";
92
93#[derive(Debug, Clone, serde::Serialize)]
95pub struct FirmwareRelease {
96 pub version: String,
98 pub tag: String,
100 #[serde(skip)]
102 url: String,
103}
104
105impl FirmwareRelease {
106 pub fn newer_than(&self, fw_version: &str) -> bool {
108 match (parse_version(&self.version), parse_version(fw_version)) {
109 (Some(a), Some(b)) => a > b,
110 (Some(_), None) => true,
112 _ => false,
113 }
114 }
115
116 pub fn download(&self) -> Result<Vec<u8>> {
118 let resp = crate::firmware::http_client()?
119 .get(&self.url)
120 .send()
121 .map_err(Error::Http)?
122 .error_for_status()
123 .map_err(Error::Http)?;
124 Ok(resp.bytes().map_err(Error::Http)?.to_vec())
125 }
126}
127
128fn parse_version(s: &str) -> Option<(u32, u32, u32)> {
129 let mut it = s.split('.').map(|p| p.parse::<u32>().ok());
130 match (it.next(), it.next(), it.next(), it.next()) {
131 (Some(Some(a)), Some(Some(b)), Some(Some(c)), None) => Some((a, b, c)),
132 _ => None,
133 }
134}
135
136pub fn latest_firmware(model: DongleModel) -> Result<Option<FirmwareRelease>> {
139 let releases: serde_json::Value = crate::firmware::http_client()?
140 .get(format!(
141 "https://api.github.com/repos/{FW_RELEASE_REPO}/releases?per_page=100"
142 ))
143 .send()
144 .map_err(Error::Http)?
145 .error_for_status()
146 .map_err(Error::Http)?
147 .json()
148 .map_err(Error::Http)?;
149
150 let mut best: Option<((u32, u32, u32), FirmwareRelease)> = None;
151 for rel in releases.as_array().map(Vec::as_slice).unwrap_or_default() {
152 let tag = rel["tag_name"].as_str().unwrap_or_default();
153 let Some(version) = tag.strip_prefix(model.release_tag_prefix()) else {
154 continue;
155 };
156 let Some(parsed) = parse_version(version) else {
157 continue;
158 };
159 let Some(url) = rel["assets"]
160 .as_array()
161 .map(Vec::as_slice)
162 .unwrap_or_default()
163 .iter()
164 .find(|a| a["name"].as_str() == Some(model.release_asset()))
165 .and_then(|a| a["browser_download_url"].as_str())
166 else {
167 continue;
168 };
169 if best.as_ref().is_none_or(|(v, _)| parsed > *v) {
170 best = Some((
171 parsed,
172 FirmwareRelease {
173 version: version.to_string(),
174 tag: tag.to_string(),
175 url: url.to_string(),
176 },
177 ));
178 }
179 }
180 Ok(best.map(|(_, r)| r))
181}
182
183const CTRL_TIMEOUT: Duration = Duration::from_millis(500);
184const CMD_TIMEOUT: Duration = Duration::from_secs(8);
186const FW_CHUNK_TIMEOUT: Duration = Duration::from_secs(3);
189const FW_DONE_TIMEOUT: Duration = Duration::from_secs(10);
190
191#[derive(Debug, Clone, serde::Serialize)]
194pub struct Dongle {
195 pub serial: String,
197 pub product: String,
199 pub model: DongleModel,
201 #[serde(skip)]
203 bus_id: String,
204 #[serde(skip)]
206 port_chain: Vec<u8>,
207 #[serde(skip)]
209 vendor_iface: u8,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
214#[serde(rename_all = "kebab-case")]
215pub enum PdState {
216 Disconnected,
217 VbusOn,
218 Connected,
219 Accept,
220 Idle,
221 Unknown,
222}
223
224#[derive(Debug, Clone, serde::Serialize)]
226pub struct DongleStatus {
227 pub pd_state: PdState,
229 pub target_attached: bool,
231 pub polarity_cc2: bool,
233 #[serde(skip)]
235 result: u8,
236}
237
238impl DongleStatus {
239 fn parse(buf: &[u8]) -> Result<Self> {
240 if buf.len() < 4 {
241 return Err(Error::Dongle("short status response from dongle".into()));
242 }
243 let pd_state = match buf[1] {
244 proto::PD_DISCONNECTED => PdState::Disconnected,
245 proto::PD_VBUS_ON => PdState::VbusOn,
246 proto::PD_CONNECTED => PdState::Connected,
247 proto::PD_ACCEPT => PdState::Accept,
248 proto::PD_IDLE => PdState::Idle,
249 _ => PdState::Unknown,
250 };
251 Ok(DongleStatus {
252 pd_state,
253 target_attached: buf[2] & proto::FLAG_TARGET_ATTACHED != 0,
254 polarity_cc2: buf[2] & proto::FLAG_POLARITY_CC2 != 0,
255 result: buf[3],
256 })
257 }
258
259 pub fn target_ready(&self) -> bool {
262 self.target_attached
263 }
264}
265
266#[derive(Debug, Clone)]
268pub enum DongleTarget {
269 Auto,
271 Id(String),
273 Ecid(u64),
275}
276
277pub fn list() -> Result<Vec<Dongle>> {
279 let infos = nusb::list_devices()
280 .wait()
281 .map_err(|e| Error::Usb(e.to_string()))?;
282 let mut out = Vec::new();
283 for info in infos {
284 if info.vendor_id() != DONGLE_VID || info.product_id() != DONGLE_PID {
285 continue;
286 }
287 let product = info.product_string().unwrap_or("");
290 let Some(model) = DongleModel::from_product(product) else {
291 continue;
292 };
293 let Some(vendor_iface) = info
295 .interfaces()
296 .find(|i| i.class() == proto::VENDOR_CLASS)
297 .map(|i| i.interface_number())
298 else {
299 continue;
300 };
301 out.push(Dongle {
302 serial: info.serial_number().unwrap_or("").to_string(),
303 product: product.to_string(),
304 model,
305 bus_id: info.bus_id().to_string(),
306 port_chain: info.port_chain().to_vec(),
307 vendor_iface,
308 });
309 }
310 Ok(out)
311}
312
313pub fn find(target: DongleTarget) -> Result<Dongle> {
316 match target {
317 DongleTarget::Id(id) => select_by_id(list()?, &id),
318 DongleTarget::Ecid(ecid) => find_for_ecid(ecid),
319 DongleTarget::Auto => {
320 let mut ds = list()?;
321 match ds.len() {
322 0 => Err(Error::NoDongle),
323 1 => Ok(ds.remove(0)),
324 _ => Err(Error::MultipleDongles(serials(&ds))),
325 }
326 }
327 }
328}
329
330fn select_by_id(ds: Vec<Dongle>, id: &str) -> Result<Dongle> {
333 if let Some(i) = ds.iter().position(|d| d.serial.eq_ignore_ascii_case(id)) {
334 let mut ds = ds;
335 return Ok(ds.swap_remove(i));
336 }
337 let needle = id.to_ascii_lowercase();
338 let mut matches: Vec<Dongle> = ds
339 .into_iter()
340 .filter(|d| d.serial.to_ascii_lowercase().contains(&needle))
341 .collect();
342 match matches.len() {
343 0 => Err(Error::Dongle(format!(
344 "no dongle matching '{id}' (see `restorekit dongle list`)"
345 ))),
346 1 => Ok(matches.remove(0)),
347 _ => Err(Error::MultipleDongles(serials(&matches))),
348 }
349}
350
351fn serials(ds: &[Dongle]) -> String {
352 ds.iter()
353 .map(|d| d.serial.as_str())
354 .collect::<Vec<_>>()
355 .join(", ")
356}
357
358pub fn wait(target: DongleTarget, timeout: std::time::Duration) -> Result<Dongle> {
361 let deadline = std::time::Instant::now() + timeout;
362 loop {
363 match find(target.clone()) {
364 Ok(d) => return Ok(d),
365 Err(Error::NoDongle) if std::time::Instant::now() < deadline => {
366 std::thread::sleep(std::time::Duration::from_millis(500));
367 }
368 Err(Error::NoDongle) => return Err(Error::NoDongle),
369 Err(e) => return Err(e),
370 }
371 }
372}
373
374pub fn find_for_ecid(ecid: u64) -> Result<Dongle> {
380 let infos: Vec<_> = nusb::list_devices()
381 .wait()
382 .map_err(|e| Error::Usb(e.to_string()))?
383 .collect();
384
385 let mac = infos
386 .iter()
387 .find(|&i| i.vendor_id() == APPLE_VID && device::from_usb(i).ecid == Some(ecid))
388 .ok_or(Error::EcidNotConnected(ecid))?;
389
390 list()?
391 .into_iter()
392 .find(|d| shares_parent_hub(d, mac))
393 .ok_or_else(|| {
394 Error::Dongle(format!(
395 "the Mac with ECID {ecid:#x} is not behind a known dongle"
396 ))
397 })
398}
399
400fn shares_parent_hub(dongle: &Dongle, mac: &nusb::DeviceInfo) -> bool {
403 let mac_chain = mac.port_chain();
404 mac.bus_id() == dongle.bus_id
405 && !dongle.port_chain.is_empty()
406 && mac_chain.len() == dongle.port_chain.len()
407 && mac_chain[..mac_chain.len() - 1] == dongle.port_chain[..dongle.port_chain.len() - 1]
408}
409
410const USB_CLASS_HUB: u8 = 0x09;
412
413#[derive(Debug, Clone, PartialEq, Eq)]
415pub enum Connection {
416 Direct,
418 Dongle(String),
421 Hub,
423}
424
425impl Connection {
426 pub fn kind(&self) -> &'static str {
428 match self {
429 Connection::Direct => "direct",
430 Connection::Dongle(_) => "dongle",
431 Connection::Hub => "hub",
432 }
433 }
434
435 pub fn dongle(&self) -> Option<&str> {
437 match self {
438 Connection::Dongle(s) => Some(s.as_str()),
439 _ => None,
440 }
441 }
442
443 pub fn host_reachable(&self) -> bool {
446 matches!(self, Connection::Direct)
447 }
448}
449
450pub fn connection_for(dev: &Device) -> Connection {
455 let infos: Vec<_> = match nusb::list_devices().wait() {
456 Ok(it) => it.collect(),
457 Err(_) => return Connection::Direct,
458 };
459 let Some(me) = infos
460 .iter()
461 .find(|i| i.vendor_id() == APPLE_VID && i.serial_number() == Some(dev.serial.as_str()))
462 else {
463 return Connection::Direct;
464 };
465
466 if let Ok(dongles) = list() {
468 for d in dongles {
469 if shares_parent_hub(&d, me) {
470 return Connection::Dongle(d.serial);
471 }
472 }
473 }
474
475 let chain = me.port_chain();
478 let behind_hub = infos.iter().any(|h| {
479 h.class() == USB_CLASS_HUB
480 && h.bus_id() == me.bus_id()
481 && !h.port_chain().is_empty()
482 && h.port_chain().len() < chain.len()
483 && chain.starts_with(h.port_chain())
484 });
485 if behind_hub {
486 Connection::Hub
487 } else {
488 Connection::Direct
489 }
490}
491
492impl Dongle {
493 pub fn open(&self) -> Result<DongleHandle> {
495 let info = nusb::list_devices()
497 .wait()
498 .map_err(|e| Error::Usb(e.to_string()))?
499 .find(|i| {
500 i.vendor_id() == DONGLE_VID
501 && i.product_id() == DONGLE_PID
502 && i.serial_number() == Some(self.serial.as_str())
503 })
504 .ok_or(Error::NoDongle)?;
505 let dev = info.open().wait().map_err(|e| Error::Usb(e.to_string()))?;
506 let iface = dev
507 .claim_interface(self.vendor_iface)
508 .wait()
509 .map_err(|e| Error::Usb(e.to_string()))?;
510 Ok(DongleHandle {
511 iface,
512 iface_num: self.vendor_iface,
513 })
514 }
515
516 pub fn dfu(&self) -> Result<()> {
518 self.open()?.dfu()
519 }
520
521 pub fn reboot(&self) -> Result<()> {
523 self.open()?.reboot()
524 }
525
526 pub fn serial(&self) -> Result<()> {
529 self.open()?.serial()
530 }
531
532 pub fn status(&self) -> Result<DongleStatus> {
534 self.open()?.status()
535 }
536
537 pub fn bootsel(&self) -> Result<()> {
539 self.open()?.bootsel()
540 }
541
542 pub fn fw_version(&self) -> Result<String> {
544 self.open()?.fw_version()
545 }
546
547 pub fn attached_device(&self) -> Result<Option<Device>> {
552 let infos = nusb::list_devices()
553 .wait()
554 .map_err(|e| Error::Usb(e.to_string()))?;
555 Ok(infos
556 .filter(|i| i.vendor_id() == APPLE_VID)
557 .find(|i| shares_parent_hub(self, i))
558 .map(|i| device::from_usb(&i)))
559 }
560}
561
562pub struct DongleHandle {
564 iface: Interface,
565 iface_num: u8,
566}
567
568impl DongleHandle {
569 pub fn dfu(&self) -> Result<()> {
571 self.command(proto::VCMD_DFU, "dfu")
572 }
573
574 pub fn reboot(&self) -> Result<()> {
576 self.command(proto::VCMD_REBOOT, "reboot")
577 }
578
579 pub fn serial(&self) -> Result<()> {
581 self.command(proto::VCMD_SERIAL, "serial")
582 }
583
584 pub fn debugusb(&self) -> Result<()> {
586 self.command(proto::VCMD_DEBUGUSB, "debugusb")
587 }
588
589 pub fn nop(&self) -> Result<()> {
591 self.command(proto::VCMD_NOP, "nop")
592 }
593
594 pub fn bootsel(&self) -> Result<()> {
598 self.vendor_out_raw(proto::VREQ_CMD, proto::VCMD_BOOTSEL, &[], CTRL_TIMEOUT)
599 .map_err(|e| match e {
600 nusb::transfer::TransferError::Stall => Error::Dongle(
603 "this dongle's firmware predates USB bootsel; type `bootsel` on its \
604 serial console (CDC0) or replug it with the BOOTSEL button held"
605 .into(),
606 ),
607 e => Error::Dongle(e.to_string()),
608 })
609 }
610
611 pub fn update(&self, image: &[u8], mut progress: impl FnMut(usize, usize)) -> Result<()> {
621 if image.is_empty() {
622 return Err(Error::Dongle("empty firmware image".into()));
623 }
624 let total = image.len();
625 self.vendor_out_raw(
626 proto::VREQ_FW_BEGIN,
627 0,
628 &(total as u32).to_le_bytes(),
629 CTRL_TIMEOUT,
630 )
631 .map_err(|e| match e {
632 nusb::transfer::TransferError::Stall => Error::Dongle(
633 "the dongle rejected the update: its firmware predates USB updates \
634 (flash it once over the bootrom with `just fw-flash-full`), or the \
635 image is too big for its spare slot"
636 .into(),
637 ),
638 e => Error::Dongle(format!("starting the update: {e}")),
639 })?;
640 let mut chunk_buf = vec![0xFFu8; proto::FW_CHUNK];
641 for (i, chunk) in image.chunks(proto::FW_CHUNK).enumerate() {
642 chunk_buf.fill(0xFF);
643 chunk_buf[..chunk.len()].copy_from_slice(chunk);
644 self.vendor_out(proto::VREQ_FW_DATA, i as u16, &chunk_buf, FW_CHUNK_TIMEOUT)
645 .map_err(|e| {
646 Error::Dongle(format!(
647 "update failed at {}/{} bytes: {e}",
648 i * proto::FW_CHUNK,
649 total
650 ))
651 })?;
652 progress((i * proto::FW_CHUNK + chunk.len()).min(total), total);
653 }
654 self.vendor_out(
655 proto::VREQ_FW_DONE,
656 0,
657 &proto::crc32(image).to_le_bytes(),
658 FW_DONE_TIMEOUT,
659 )
660 .map_err(|e| Error::Dongle(format!("update verification failed: {e}")))?;
661 Ok(())
662 }
663
664 fn vendor_out(&self, request: u8, value: u16, data: &[u8], timeout: Duration) -> Result<()> {
666 self.vendor_out_raw(request, value, data, timeout)
667 .map_err(|e| Error::Dongle(e.to_string()))
668 }
669
670 fn vendor_out_raw(
673 &self,
674 request: u8,
675 value: u16,
676 data: &[u8],
677 timeout: Duration,
678 ) -> std::result::Result<(), nusb::transfer::TransferError> {
679 self.iface
680 .control_out(
681 ControlOut {
682 control_type: ControlType::Vendor,
683 recipient: Recipient::Interface,
684 request,
685 value,
686 index: self.iface_num as u16,
687 data,
688 },
689 timeout,
690 )
691 .wait()?;
692 Ok(())
693 }
694
695 pub fn status(&self) -> Result<DongleStatus> {
697 let buf = self.vendor_in(proto::VREQ_STATUS, proto::STATUS_LEN as u16)?;
698 DongleStatus::parse(&buf)
699 }
700
701 pub fn fw_version(&self) -> Result<String> {
703 let buf = self.vendor_in(proto::VREQ_VERSION, proto::FW_VERSION_MAX_LEN as u16)?;
704 String::from_utf8(buf).map_err(|_| Error::Dongle("firmware version is not UTF-8".into()))
705 }
706
707 fn vendor_in(&self, request: u8, length: u16) -> Result<Vec<u8>> {
709 self.iface
710 .control_in(
711 ControlIn {
712 control_type: ControlType::Vendor,
713 recipient: Recipient::Interface,
714 request,
715 value: 0,
716 index: self.iface_num as u16,
717 length,
718 },
719 CTRL_TIMEOUT,
720 )
721 .wait()
722 .map_err(|e| Error::Dongle(e.to_string()))
723 }
724
725 fn command(&self, code: u16, name: &str) -> Result<()> {
727 self.vendor_out(proto::VREQ_CMD, code, &[], CTRL_TIMEOUT)?;
728
729 let deadline = Instant::now() + CMD_TIMEOUT;
732 loop {
733 match self.status()?.result {
734 proto::RES_PENDING => {}
735 proto::RES_OK => return Ok(()),
736 proto::RES_NOTARGET => return Err(Error::DongleNoTarget),
737 _ => {}
738 }
739 if Instant::now() >= deadline {
740 return Err(Error::Dongle(format!(
741 "{name}: timed out waiting for the dongle"
742 )));
743 }
744 std::thread::sleep(Duration::from_millis(10));
745 }
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 fn dongle(serial: &str) -> Dongle {
754 Dongle {
755 serial: serial.into(),
756 product: proto::PRODUCT_LITE.into(),
757 model: DongleModel::Lite,
758 bus_id: String::new(),
759 port_chain: Vec::new(),
760 vendor_iface: 0,
761 }
762 }
763
764 #[test]
765 fn select_by_id_matches_fragments() {
766 let ds = || vec![dongle("DL-5F417536"), dongle("DL-AA00BB11")];
767 assert_eq!(
769 select_by_id(ds(), "DL-5F417536").unwrap().serial,
770 "DL-5F417536"
771 );
772 assert_eq!(
773 select_by_id(ds(), "dl-aa00bb11").unwrap().serial,
774 "DL-AA00BB11"
775 );
776 assert_eq!(select_by_id(ds(), "5f41").unwrap().serial, "DL-5F417536");
777 match select_by_id(ds(), "DL-") {
779 Err(Error::MultipleDongles(s)) => {
780 assert!(s.contains("DL-5F417536") && s.contains("DL-AA00BB11"))
781 }
782 other => panic!("expected MultipleDongles, got {other:?}"),
783 }
784 assert!(select_by_id(ds(), "zzz").is_err());
785 let mut ds2 = ds();
787 ds2.push(dongle("DL-5F41"));
788 assert_eq!(select_by_id(ds2, "DL-5F41").unwrap().serial, "DL-5F41");
789 }
790}