sdwire 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sdwire
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: CLI application to interact with Badgerd SDWire devices
5
5
  License: GPL-3
6
6
  Author: Talha Can Havadar
@@ -111,3 +111,19 @@ bdgrd_sdwirec_522 [sd-wire::SRPOL]
111
111
  ❯ sdwire switch host
112
112
  ```
113
113
 
114
+ ### SDWire3 and Linux USB power management
115
+
116
+ On Linux, the SDWire3 only hands the SD card to the target while the USB device
117
+ is runtime-suspended. `sdwire switch target` enables this automatically
118
+ (`power/autosuspend_delay_ms=0`, `power/control=auto`) and verifies that
119
+ `power/runtime_status` becomes `suspended`; otherwise it exits with an error.
120
+ Writing to sysfs requires root, or install `udev/90-sdwire.rules`, which applies
121
+ the setting when the device is plugged in.
122
+
123
+ To do it by hand (replace `2-8` with your device's sysfs name, e.g. `5-1`):
124
+
125
+ ```
126
+ echo 0 | sudo tee /sys/bus/usb/devices/2-8/power/autosuspend_delay_ms
127
+ echo auto | sudo tee /sys/bus/usb/devices/2-8/power/control
128
+ ```
129
+
@@ -94,3 +94,19 @@ bdgrd_sdwirec_522 [sd-wire::SRPOL]
94
94
 
95
95
  ❯ sdwire switch host
96
96
  ```
97
+
98
+ ### SDWire3 and Linux USB power management
99
+
100
+ On Linux, the SDWire3 only hands the SD card to the target while the USB device
101
+ is runtime-suspended. `sdwire switch target` enables this automatically
102
+ (`power/autosuspend_delay_ms=0`, `power/control=auto`) and verifies that
103
+ `power/runtime_status` becomes `suspended`; otherwise it exits with an error.
104
+ Writing to sysfs requires root, or install `udev/90-sdwire.rules`, which applies
105
+ the setting when the device is plugged in.
106
+
107
+ To do it by hand (replace `2-8` with your device's sysfs name, e.g. `5-1`):
108
+
109
+ ```
110
+ echo 0 | sudo tee /sys/bus/usb/devices/2-8/power/autosuspend_delay_ms
111
+ echo auto | sudo tee /sys/bus/usb/devices/2-8/power/control
112
+ ```
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sdwire"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "CLI application to interact with Badgerd SDWire devices"
5
5
  authors = ["Talha Can Havadar <havadartalha@gmail.com>"]
6
6
  license = "GPL-3"
@@ -17,6 +17,8 @@ import usb.util
17
17
 
18
18
  log = logging.getLogger(__name__)
19
19
 
20
+ USB_HUB_CLASS = 9
21
+
20
22
 
21
23
  def get_sdwirec_devices() -> List[SDWireC]:
22
24
  """Detect and return all connected SDWireC devices.
@@ -25,7 +27,11 @@ def get_sdwirec_devices() -> List[SDWireC]:
25
27
  List of SDWireC device instances found on the system
26
28
  """
27
29
  try:
28
- found_devices = usb.core.find(find_all=True)
30
+ # skip hubs: opening one blocks while the kernel enumerates a child port
31
+ found_devices = usb.core.find(
32
+ find_all=True,
33
+ custom_match=lambda d: d.bDeviceClass != USB_HUB_CLASS,
34
+ )
29
35
  devices = list(found_devices or [])
30
36
  except Exception as e:
31
37
  log.debug("Error finding USB devices: %s", e)
@@ -0,0 +1,86 @@
1
+ """Linux USB runtime power-management helpers for SDWire3.
2
+
3
+ The SDWire3 RTS5306E controller hands the SD card to the target side only when
4
+ the USB device is runtime-suspended, which requires ``power/control == auto``.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ import time
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+ SYSFS_USB_DEVICES = "/sys/bus/usb/devices"
14
+
15
+
16
+ class PowerManagementError(RuntimeError):
17
+ """Raised when the device power state cannot be configured or verified."""
18
+
19
+
20
+ class UsbPower:
21
+ def __init__(
22
+ self,
23
+ bus: int | None,
24
+ port_numbers,
25
+ root: str = SYSFS_USB_DEVICES,
26
+ ):
27
+ self.path: str | None = None
28
+ if bus is not None and port_numbers:
29
+ name = f"{bus}-{'.'.join(map(str, port_numbers))}"
30
+ self.path = os.path.join(root, name)
31
+
32
+ @property
33
+ def available(self) -> bool:
34
+ return self.path is not None and os.path.isdir(os.path.join(self.path, "power"))
35
+
36
+ def _file(self, name: str) -> str:
37
+ return os.path.join(self.path or "", "power", name)
38
+
39
+ def _read(self, name: str) -> str | None:
40
+ try:
41
+ with open(self._file(name)) as f:
42
+ return f.read().strip()
43
+ except OSError:
44
+ return None
45
+
46
+ def _write(self, name: str, value: str) -> None:
47
+ try:
48
+ with open(self._file(name), "w") as f:
49
+ f.write(value)
50
+ except PermissionError as e:
51
+ raise PowerManagementError(
52
+ f"permission denied writing '{value}' to {self._file(name)}; "
53
+ "run as root or install the udev rule from the udev/ directory"
54
+ ) from e
55
+ except OSError as e:
56
+ raise PowerManagementError(
57
+ f"failed to write '{value}' to {self._file(name)}: {e}"
58
+ ) from e
59
+
60
+ @property
61
+ def control(self) -> str | None:
62
+ return self._read("control")
63
+
64
+ @property
65
+ def runtime_status(self) -> str | None:
66
+ return self._read("runtime_status")
67
+
68
+ def enable_autosuspend(self) -> None:
69
+ """Set autosuspend delay to 0 and power/control to auto."""
70
+ if not self.available:
71
+ raise PowerManagementError(
72
+ "cannot locate the sysfs power directory of the SDWire3 device"
73
+ )
74
+ if self._read("autosuspend_delay_ms") != "0":
75
+ self._write("autosuspend_delay_ms", "0")
76
+ if self.control != "auto":
77
+ self._write("control", "auto")
78
+
79
+ def wait_for_status(self, expected: str, timeout: float = 5.0) -> bool:
80
+ deadline = time.monotonic() + timeout
81
+ while True:
82
+ if self.runtime_status == expected:
83
+ return True
84
+ if time.monotonic() >= deadline:
85
+ return False
86
+ time.sleep(0.1)
@@ -0,0 +1,139 @@
1
+ import logging
2
+
3
+ import usb.core
4
+ import usb.util
5
+
6
+ from sdwire.backend.block_device_utils import map_usb_device_to_block_device
7
+ from sdwire.backend.device.power import PowerManagementError, UsbPower
8
+ from sdwire.backend.device.usb_device import PortInfo, USBDevice
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ SDWIRE_GENERATION_SDWIRE3 = 2
13
+
14
+
15
+ class SDWire(USBDevice):
16
+ __block_dev = None
17
+
18
+ def __init__(self, port_info: PortInfo, generation: int):
19
+ super().__init__(port_info)
20
+ self.generation = generation
21
+ self._update_block_device()
22
+
23
+ @property
24
+ def power(self) -> UsbPower:
25
+ return UsbPower(
26
+ getattr(self.usb_device, "bus", None),
27
+ getattr(self.usb_device, "port_numbers", None),
28
+ )
29
+
30
+ def query_device_state(self):
31
+ """Return True when in host mode, False when in target mode."""
32
+ if not self.usb_device:
33
+ raise RuntimeError("USB device not available")
34
+
35
+ try:
36
+ host_attached = bool(self.usb_device.is_kernel_driver_active(0))
37
+ except NotImplementedError as e:
38
+ raise RuntimeError("state query is not supported on this platform") from e
39
+ power = self.power
40
+ if (
41
+ not host_attached
42
+ and power.available
43
+ and power.runtime_status != "suspended"
44
+ ):
45
+ log.warning(
46
+ "driver is detached but the device is not runtime-suspended "
47
+ "(power/control=%s, runtime_status=%s); the card is not switched to target",
48
+ power.control,
49
+ power.runtime_status,
50
+ )
51
+ return host_attached
52
+
53
+ def _release_usb_handle(self) -> None:
54
+ # an open usb handle keeps the device awake and prevents runtime suspend
55
+ usb.util.dispose_resources(self.usb_device)
56
+
57
+ def switch_ts(self) -> None:
58
+ if not self.usb_device:
59
+ raise RuntimeError("USB device not available")
60
+
61
+ try:
62
+ if self.usb_device.is_kernel_driver_active(0):
63
+ log.debug("already in ts mode")
64
+ return
65
+ self.usb_device.attach_kernel_driver(0)
66
+ self.usb_device.reset()
67
+ except (usb.core.USBError, NotImplementedError) as e:
68
+ if self.power.available:
69
+ raise
70
+ # platforms without sysfs (e.g. macOS) can't verify the switch,
71
+ # keep the previous non-fatal behaviour
72
+ log.warning("not able to switch to ts mode: %s", e)
73
+ self._release_usb_handle()
74
+
75
+ def switch_dut(self) -> None:
76
+ if not self.usb_device:
77
+ raise RuntimeError("USB device not available")
78
+
79
+ power = self.power
80
+ if power.available:
81
+ power.enable_autosuspend()
82
+ else:
83
+ log.debug("sysfs power control not found, cannot verify the switch")
84
+
85
+ try:
86
+ if self.usb_device.is_kernel_driver_active(0):
87
+ self.usb_device.detach_kernel_driver(0)
88
+ self.usb_device.reset()
89
+ except (usb.core.USBError, NotImplementedError) as e:
90
+ if power.available:
91
+ raise
92
+ log.warning("not able to switch to dut mode: %s", e)
93
+ self._release_usb_handle()
94
+
95
+ if power.available and not power.wait_for_status("suspended"):
96
+ raise PowerManagementError(
97
+ f"device did not runtime-suspend (power/control={power.control}, "
98
+ f"runtime_status={power.runtime_status}) at {power.path}; "
99
+ "the SD card is not connected to the target"
100
+ )
101
+
102
+ def _update_block_device(self) -> None:
103
+ """Update block device detection based on current device state."""
104
+ if not self.usb_device:
105
+ self.__block_dev = None
106
+ return
107
+
108
+ try:
109
+ storage_device = self.storage_device
110
+ if storage_device is not None:
111
+ self.__block_dev = map_usb_device_to_block_device(storage_device)
112
+ log.debug(f"SDWire3: Found block device: {self.__block_dev}")
113
+ else:
114
+ self.__block_dev = None
115
+ except Exception as e:
116
+ log.debug(f"SDWire3: Block device detection failed: {e}")
117
+ self.__block_dev = None
118
+
119
+ @property
120
+ def block_dev(self) -> str | None:
121
+ return self.__block_dev
122
+
123
+ @property
124
+ def storage_device(self) -> usb.core.Device | None:
125
+ """Return the USB device that corresponds to the storage interface.
126
+
127
+ For SDWire3, this is the same device we control (direct media controller).
128
+
129
+ Returns:
130
+ usb.core.Device: The USB device for storage, or None if not available
131
+ """
132
+ return self.usb_device
133
+
134
+ def __str__(self) -> str:
135
+ block_dev_str = self.block_dev if self.block_dev is not None else "None"
136
+ return f"{self.serial_string:<30}[{int(self.manufacturer_string):04x}::{int(self.product_string):04x}]\t\t{block_dev_str}"
137
+
138
+ def __repr__(self) -> str:
139
+ return self.__str__()
@@ -82,3 +82,50 @@ def handle_switch_command(ctx, serial):
82
82
  raise click.UsageError(
83
83
  f"There is no such sdwire device connected with serial={serial}"
84
84
  )
85
+
86
+
87
+ def handle_state_command(ctx, serial):
88
+ """Handle querying device state.
89
+
90
+ Args:
91
+ ctx: Click context containing device information
92
+ serial: Serial number of the device to query
93
+ """
94
+
95
+ devices = detect.get_sdwire_devices()
96
+
97
+ if serial is None:
98
+ # check the devices
99
+ if len(devices) == 0:
100
+ raise click.UsageError("There is no sdwire device connected!")
101
+ if len(devices) > 1:
102
+ raise click.UsageError(
103
+ "There is more then 1 sdwire device connected, please use --serial|-s to specify!"
104
+ )
105
+ log.info("1 sdwire/sdwirec device detected")
106
+ ctx.obj["device"] = devices[0]
107
+ else:
108
+ for device in devices:
109
+ if device.serial_string == serial:
110
+ ctx.obj["device"] = device
111
+ break
112
+ else:
113
+ raise click.UsageError(
114
+ f"There is no such sdwire device connected with serial={serial}"
115
+ )
116
+
117
+ device = ctx.obj["device"]
118
+ if isinstance(device, (SDWireC)):
119
+ log.info(
120
+ "SDWireC or legacy sdwire devices don't have state functionality"
121
+ )
122
+ print("SDWireC don't have state functionality implemented")
123
+ sys.exit(1)
124
+ try:
125
+ state = device.query_device_state()
126
+ print(f"{'Serial':<30}\tState")
127
+ print(f"{device.serial_string:<30}\t{'Host' if state else 'Target'}")
128
+ except Exception as e:
129
+ log.error(f"Failed to get device state: {e}")
130
+ print(f"Error: Failed to get device state: {e}")
131
+ sys.exit(1)
@@ -29,6 +29,20 @@ def list() -> None:
29
29
  print(sdwire)
30
30
 
31
31
 
32
+ @main.command()
33
+ @click.pass_context
34
+ @click.option(
35
+ "-s",
36
+ "--serial",
37
+ required=False,
38
+ help="Serial number of the sdwire device, if there is only one sdwire connected then it will be used by default",
39
+ )
40
+ def state(ctx: click.Context, serial=None):
41
+ """Query and display the state of all connected SDWire devices."""
42
+ ctx.ensure_object(dict)
43
+ utils.handle_state_command(ctx, serial)
44
+
45
+
32
46
  @main.group()
33
47
  @click.pass_context
34
48
  @click.option(
@@ -1,85 +0,0 @@
1
- import logging
2
- from typing import Optional
3
- import usb.core
4
- from sdwire.backend.device.usb_device import USBDevice, PortInfo
5
- from sdwire.backend.block_device_utils import map_usb_device_to_block_device
6
-
7
- log = logging.getLogger(__name__)
8
-
9
- SDWIRE_GENERATION_SDWIRE3 = 2
10
-
11
-
12
- class SDWire(USBDevice):
13
- __block_dev = None
14
-
15
- def __init__(self, port_info: PortInfo, generation: int):
16
- super().__init__(port_info)
17
- self.generation = generation
18
- self._update_block_device()
19
-
20
- def switch_ts(self) -> None:
21
- if not self.usb_device:
22
- log.error("USB device not available")
23
- return
24
-
25
- try:
26
- self.usb_device.attach_kernel_driver(0)
27
- self.usb_device.reset()
28
- except Exception as e:
29
- log.debug(
30
- "not able to switch to ts mode. Device might be already in ts mode, err: %s",
31
- e,
32
- )
33
-
34
- def switch_dut(self) -> None:
35
- if not self.usb_device:
36
- log.error("USB device not available")
37
- return
38
-
39
- try:
40
- self.usb_device.detach_kernel_driver(0)
41
- self.usb_device.reset()
42
- except Exception as e:
43
- log.debug(
44
- "not able to switch to dut mode. Device might be already in dut mode, err: %s",
45
- e,
46
- )
47
-
48
- def _update_block_device(self) -> None:
49
- """Update block device detection based on current device state."""
50
- if not self.usb_device:
51
- self.__block_dev = None
52
- return
53
-
54
- try:
55
- storage_device = self.storage_device
56
- if storage_device is not None:
57
- self.__block_dev = map_usb_device_to_block_device(storage_device)
58
- log.debug(f"SDWire3: Found block device: {self.__block_dev}")
59
- else:
60
- self.__block_dev = None
61
- except Exception as e:
62
- log.debug(f"SDWire3: Block device detection failed: {e}")
63
- self.__block_dev = None
64
-
65
- @property
66
- def block_dev(self) -> Optional[str]:
67
- return self.__block_dev
68
-
69
- @property
70
- def storage_device(self) -> Optional[usb.core.Device]:
71
- """Return the USB device that corresponds to the storage interface.
72
-
73
- For SDWire3, this is the same device we control (direct media controller).
74
-
75
- Returns:
76
- usb.core.Device: The USB device for storage, or None if not available
77
- """
78
- return self.usb_device
79
-
80
- def __str__(self) -> str:
81
- block_dev_str = self.block_dev if self.block_dev is not None else "None"
82
- return f"{self.serial_string:<30}[{int(self.manufacturer_string):04x}::{int(self.product_string):04x}]\t\t{block_dev_str}"
83
-
84
- def __repr__(self) -> str:
85
- return self.__str__()
File without changes
File without changes
File without changes