Skip to content
This repository was archived by the owner on Oct 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions examples/sd_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ def open_card():
) from exc


def card_geometry(card):
"""Return ``(blocks, block_size)``, or raise if the card never answered.

``ioctl(4)`` and ``ioctl(5)`` are the block count and block size, and a
card that is absent, unpowered or still timing out answers them with
``-1`` rather than raising. Serving that to a host is serving a drive of
no blocks: the example used to print a negative size and then do nothing
at all, which reads as the example being broken rather than the card
(usbif#4). So refuse it here, while the reason is still known.
"""
blocks = card.ioctl(4, 0)
block_size = card.ioctl(5, 0)
if not isinstance(blocks, int) or not isinstance(block_size, int) \
or blocks <= 0 or block_size <= 0:
raise RuntimeError(
"the SD card did not answer (ioctl reported {} blocks of {} "
"bytes): check that a card is seated in the slot, then reset "
"the board, since a card left half-initialised by an earlier "
"attempt does not recover on its own".format(blocks, block_size))
return blocks, block_size


def main():
dev = usbif.auto.device()

Expand All @@ -78,8 +100,7 @@ def main():
dev.msc_detach()

card = open_card()
blocks = card.ioctl(4, 0)
block_size = card.ioctl(5, 0)
blocks, block_size = card_geometry(card)
print("card: {} blocks x {} bytes = {} MB".format(
blocks, block_size, blocks * block_size // (1024 * 1024)))

Expand Down
99 changes: 99 additions & 0 deletions tests/test_sd_drive_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# SPDX-FileCopyrightText: 2026 Brad Barnett
#
# SPDX-License-Identifier: MIT
"""examples/sd_drive.py must refuse a card that never answered (usbif#4).

A card that is absent or timing out answers ``ioctl(4)``/``ioctl(5)`` with -1
instead of raising. The example used to hand that to the host as a drive of
no blocks and then sit in its service loop, silent. These tests run the
example's ``main()`` against a fake card and a fake device, so the refusal is
checked on the path a user actually takes, with a healthy card as the control
that must still attach.
"""

import importlib.util
import pathlib
import types
import unittest

import _env # noqa: F401

import usbif.auto

ROOT = pathlib.Path(__file__).resolve().parents[1]


def _load_example():
spec = importlib.util.spec_from_file_location(
"sd_drive_example", ROOT / "examples" / "sd_drive.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# CPython's time has no sleep_ms; the service loop only needs it to exist.
module.time = types.SimpleNamespace(sleep_ms=lambda ms: None)
return module


class _Card:
def __init__(self, blocks, block_size):
self._answers = {4: blocks, 5: block_size}

def ioctl(self, op, arg):
return self._answers.get(op)


class _Device:
"""Records what the example asked of the USB device."""

def __init__(self):
self.attached = None

def msc_detach(self):
pass

def msc_attach_blockdev(self, card, writable):
self.attached = card

def functions(self, *names):
pass

def msc_status(self):
# Attached, and already ejected, so the service loop ends at once.
return (self.attached is not None, 0, True)

def msc_bd_stats(self):
return (0, 0)


class SdDriveRefusesDeadCard(unittest.TestCase):
def setUp(self):
self.example = _load_example()
self.dev = _Device()
self._saved_device = usbif.auto.device
usbif.auto.device = lambda: self.dev

def tearDown(self):
usbif.auto.device = self._saved_device

def _run_with(self, card):
self.example.open_card = lambda: card
self.example.main()

def test_card_that_never_answered_is_refused_before_attach(self):
for blocks, size in ((-1, -1), (0, 512), (62685184, -1), (None, 512)):
with self.subTest(blocks=blocks, size=size):
self.dev.attached = None
with self.assertRaises(RuntimeError) as ctx:
self._run_with(_Card(blocks, size))
self.assertIn("did not answer", str(ctx.exception))
self.assertIsNone(
self.dev.attached,
"a card with no geometry was served to the host")

def test_healthy_card_still_attaches(self):
card = _Card(62685184, 512)
self._run_with(card)
self.assertIs(self.dev.attached, card)


if __name__ == "__main__":
unittest.main()
Loading