Skip to content
 
 

Latest commit

 

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sdwire

Go Reference

A Go SDK, and a CLI built on it, for controlling SDWireC and SDWire3 devices — USB-controlled SD card multiplexers that switch a single SD card between a host computer (for flashing images) and a target device under test (for booting them), without physically re-seating the card.

The CLI is a drop-in replacement for the official python package, but includes:

  • hub-power fallback for SDWire3 (which otherwise can cause failures)
  • disk unmounting from the host before switching to target mode
  • support for power managing the target (currently using Meross WiFi power plugs)
  • one-shot command for updaing the target SD card image; the flow:
    1. switch off target
    2. flash SD image
    3. re-mount to target
    4. power on target

This was originally a fork of github.com/fcjr/sdwire.

Install

CLI

go install github.com/jphastings/sdwire/cmd/sdwire@latest

Or download a prebuilt binary from the releases page (built via GoReleaser/CI for darwin, linux, and windows, amd64 and arm64).

SDK

go get github.com/jphastings/sdwire

CLI usage

sdwire is a drop-in replacement for the list, state, and switch commands of the Python sdwire-cli (see Migrating from the Python CLI below), plus new commands for flashing, power control, and scripting.

Every command accepts -s/--serial to select which attached device to operate on: a plain USB serial, the port-suffixed identity form (20120501030900000.1.1.3), a USB location (1-1.1.3), or a device name from your config file. With no -s and no default_device configured, commands that need a single device use the sole attached device, or error out listing every attached device's identity if more than one is found.

sdwire list

List every SDWire this computer knows about: its identity, USB product info, the reader's resolved block device path (or None if it can't currently be found), and which side its card is switched to.

$ sdwire list
Serial                        Product Info		Block Dev		State
20120501030900000.1.1.3       [0bda::0316]		/dev/disk4		Host

Devices remembered in the hub-port cache are listed even when they aren't on the USB bus, since an SDWire3 in target mode is powered off and cannot be enumerated at all. For those, State is read from the hub port without powering anything on:

State Meaning
Host The card is connected to this computer.
Target The port is unpowered: the card is with the target board.
Unknown The port is powered but nothing is enumerated on it — an empty socket, or a reader that has crashed and been dropped from the bus. Recover the latter with sdwire revive.

The first three columns keep the Python CLI's exact widths and separators, so anything parsing that output by position still works; --json carries the same information plus an attached flag.

A remembered port whose status can't be read — the dock it belongs to has moved to a different USB port, say, which changes its bus number — is skipped, and list warns how many it skipped and which file to delete to forget them.

sdwire state

Print which side the selected device's SD card is currently connected to: Host, Target, or Unknown. When the device is attached and powered on, this is an honest live readback via the device — see SDWire3 state semantics below for how a powered-off (target mode) SDWire3 is handled instead.

$ sdwire state
Serial                        	State
20120501030900000.1.1.3       	Host

sdwire switch {dut|target|host|ts|off}

Switch the selected device's SD card. dut/target connect it to the target board; host/ts connect it to this computer. off is rejected with an explanation: SDWireC has no third state, and for SDWire3 "powering the port off" is literally how target mode is implemented — it does not mean disconnected from both sides. Prints nothing and exits 0 on success.

sdwire switch host              # connect the card to this computer
sdwire switch target -s bench   # connect the card to the "bench" device's target

sdwire revive

Power-cycle the selected device's upstream hub port and wait for it to come back: the software equivalent of unplugging the device and plugging it back in. Any volumes mounted from its reader are unmounted first.

sdwire revive                 # the default (or -s named) device
sdwire revive -s 1-1.1.3      # by location, straight from live USB topology

Use it when a reader has stopped answering and been dropped from the bus — sdwire list shows Unknown, or nothing at all. A USB port reset does not clear that state; only removing power does, and the port is held dark long enough for the reader's own supply to drain rather than merely reset.

Because such a device isn't enumerated, it can't be selected by serial alone. -s/--serial accepts a location (1-1.1.3, as shown by sdwire list), which is resolved from the live USB topology — the hub is still there even when the device on it isn't — so a revive works even when the hub-port cache is empty, stale, or names one serial at several ports.

sdwire flash <image>

Write an image to the selected device's SD card and boot the target from it: powers the target off (if a power plugin is configured for the device), switches to host mode, raw-writes the image with progress on stderr, switches back to target mode, then powers the target back on. Raw disk writes need elevated privileges — run with sudo on macOS/Linux, or as Administrator on Windows.

sudo sdwire flash ./ubuntu-24.04-preinstalled.img.xz -s bench
flashed 1234 / 3800 MiB (32%)

If the reader stops responding part-way through the write — see the reader-wedge entry below — the flash does not fail. Its hub port is power-cycled, the reader is waited for at the same USB location, and the write resumes from the last completed chunk, up to three times:

flashed 96 / 272 MiB (35%)
warning: the reader stopped responding 96 MiB into the write; power-cycling its hub port and resuming (attempt 1 of 3)
flashed 272 / 272 MiB (100%)

The warning is deliberately loud: a flash that needed several power cycles should never look like a clean one. Errors that a power cycle cannot fix — a full card, an unreadable image — still fail immediately rather than retrying. WithWriteRetries in the SDK tunes or disables the recovery.

Without a power plugin configured for the device the flash still happens, but nothing power-cycles the target afterwards: it carries on running whatever it was before, which is indistinguishable from a board that won't boot the image you just wrote. flash says so on stderr when it finishes, and prints the config snippet that would fix it. --require-power turns that into an error before any writing starts — worth it in scripts, where a warning nobody reads is no use.

sdwire power {on|off|cycle}

Drive the power plugin configured for the selected device. on/off set target power directly; cycle powers off, waits at least min_off_seconds, then powers back on. This never touches the SDWire's USB connection — it's the normal way to boot a target whose SD card is already switched to it. If the device has no power plugin configured, this prints a ready-to-copy YAML snippet for your config file and exits 1.

sdwire disk

Print just the selected device's resolved block device path — nothing else — for use in scripts:

sudo dd if=image.img of=$(sdwire disk -s bench) bs=4M status=progress

Exits non-zero if the block device can't currently be found.

--json

list, state, and disk accept --json for machine-readable output:

$ sdwire list --json
[
  {
    "serial": "20120501030900000",
    "identity": "20120501030900000.1.1.3",
    "location": "1-1.1.3",
    "product": "USB3.0-CRW",
    "generation": "SDWire3",
    "state": "Host",
    "attached": true,
    "block_dev": "/dev/disk4"
  }
]

$ sdwire state --json
{ "identity": "20120501030900000.1.1.3", "state": "Host" }

$ sdwire disk --json
{ "block_dev": "/dev/disk4" }

--debug and warnings

SDK warnings (for example, an SDWire3 sitting behind a hub that only switches port power in a ganged fashion, affecting sibling ports) always print to stderr, whether or not --debug is set. --debug adds further diagnostics — the resolved config path, which device a selector matched, and so on.

During a flash, --debug also prints how long each chunk's write took and its throughput:

debug: wrote 4096 KiB at offset 84 MiB in 121ms (33.1 MiB/s)

That is the number to watch when a reader is misbehaving: a healthy one holds a steady rate, while one whose internal write buffer is backing up slows chunk by chunk before it stalls outright. WithWriteTiming exposes the same data to SDK callers.

Shell completion

sdwire completion bash|zsh|fish|powershell

See sdwire completion --help (and each shell's subcommand --help) for how to load the generated script.

Exit codes

0 on success, 1 for an operational failure (device not found, config error, flash failure, ...), 2 for a CLI usage error (bad flags, unknown subcommand or argument).

Config file

Path: ~/.config/sdwire/config.yaml on every OS (not os.UserConfigDir()'s platform-specific location — macOS's ~/Library/ Application Support in particular — this project deliberately uses one fixed XDG-style path everywhere). Override with --config <path> or the SDWIRE_CONFIG environment variable. Individual keys are also overridable via SDWIRE_-prefixed environment variables (e.g. SDWIRE_DEFAULT_DEVICE, with . replaced by _ for nested keys).

default_device: bench
devices:
  bench:
    serial: "20120501030900000"        # or the port-suffixed identity form
    location: "1-1.1.3"                # optional; disambiguates identical Realtek serials
    power:
      type: meross
      ip: 192.0.2.10
      key: "<meross account key>"
      # channel: 0                     # optional
min_off_seconds: 8
  • default_device is used whenever -s/--serial isn't given.
  • Each entry under devices names a device you can pass to -s; either key alone is enough. Given both, location is tried first (more specific — needed because every Realtek SDWire3 reader shares the same hardcoded USB serial number) and serial is the fallback: a location names the socket the device is plugged into, so moving it to another port makes the location stale. Commands then find the device by serial and warn that the location: line wants updating, rather than reporting the device missing.
  • power configures a power plugin for that device — see below. A device with no power section works fine for list/state/switch/flash; only sdwire power requires one.
  • min_off_seconds (default 8) is the minimum dark time sdwire power cycle and sdwire flash hold target power off for.

No config file at all is a fully supported setup: with exactly one SDWire attached, list, state, switch, flash, and disk all work without any configuration; power will explain what to add.

Power plugins

The CLI ships one registered power plugin type, meross (for Meross smart plugs — see power/meross/README.md for how to get your Meross account key). The SDK itself (sdwire.PowerFunc) is plugin-agnostic; this registry, and the config wiring around it, lives in the CLI. sdwire power on|off|cycle with an unrecognized power.type lists the registered types in its error.

Migrating from the Python CLI

sdwire state and sdwire switch match the Python sdwire-cli v0.3.1's output byte-for-byte, and sdwire list keeps its first three columns at the same widths and separators — with a State column appended, and rows for devices remembered but not currently on the bus. Scripts reading those three columns by position work unchanged once this binary is what sdwire on your PATH resolves to. flash, power, disk, revive, --json, and --debug are new.

PATH precedence. If you already have the Python CLI installed (e.g. via pip), check which one PATH finds first:

which -a sdwire

go install puts this binary at $(go env GOPATH)/bin/sdwire (typically ~/go/bin/sdwire); make sure that directory precedes wherever the Python version lives (often an Anaconda/Miniconda bin directory, or a pip --user install path) in PATH, or pip uninstall sdwire the Python one.

SDWire3 state semantics

For SDWire3, a device currently in target mode is, physically, powered off — it isn't enumerable on USB at all. sdwire state never powers a device on to answer: when there's nothing live to read, it falls back to the on-disk hub-port cache and a direct hub port-status read, which is enough to report Target honestly without side effects. sdwire switch dut/target does the same check before doing any work, so switching a device that's already in target mode is also a no-op. Only commands that need the SD card to actually move data — switch host/ts and flash — revive a powered-off SDWire3 via the cache, since restoring power is inherent to what those commands do.

That cache is a separate file from your config, at <user cache dir>/sdwire/hubports.json~/Library/Caches/sdwire/hubports.json on macOS, ~/.cache/sdwire/hubports.json on Linux, %LocalAppData%\sdwire\hubports.json on Windows. Any command that finds a device live rewrites its entry, so deleting the file is safe: the worst that happens is a device sitting in target mode can't be found again until it's powered back on some other way.

A device moved between sockets over its life leaves one entry per socket, and nothing removes the old ones — every SDWire3 shares one Realtek serial, so a bare serial can name several remembered ports. Each entry records when the device was last seen there, and a device that isn't on the bus is taken to be at the port it was most recently seen at: state, switch dut's already-in-target check and revive by serial answer for that one, and switch host powers it on first. Entries written before this was recorded have no sighting to compare, so they still report the ambiguity until the device is next seen live.

Permissions

Flashing needs raw block device access: run sdwire flash (or any direct write to the path from sdwire disk) with sudo on macOS/Linux, or as Administrator on Windows.

Linux udev rules. Create /etc/udev/rules.d/99-sdwire.rules:

# SDWireC (FTDI)
SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", ATTR{idProduct}=="6001", MODE="0666"
# SDWire3 (Realtek reader)
SUBSYSTEM=="usb", ATTR{idVendor}=="0bda", ATTR{idProduct}=="0316", MODE="0666"
# SDWire3 switching also needs write access to its upstream hub, since the
# card is handed to the target by cutting that hub port's power (VBUS).
# Either run as root, or grant access to hub devices:
SUBSYSTEM=="usb", ATTR{bDeviceClass}=="09", MODE="0666"

Then reload rules and add your user to plugdev:

sudo udevadm control --reload-rules
sudo udevadm trigger
sudo usermod -a -G plugdev $USER

Windows. SDWire3 switching needs libusb access to the upstream hub, not just the reader — install UsbDk, or bind the hub to WinUSB with Zadig.

SDK quick-start

package main

import (
	"context"
	"log"

	"github.com/jphastings/sdwire"
	"github.com/jphastings/sdwire/power/meross"
)

func main() {
	powerFunc, err := meross.New("192.0.2.10", "your-meross-account-key")
	if err != nil {
		log.Fatal(err)
	}

	dev, err := sdwire.New(
		sdwire.WithTargetPower(powerFunc),
		sdwire.WithWarningHandler(func(msg string) { log.Println("warning:", msg) }),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer dev.Close()

	if err := dev.FlashAndBoot(context.Background(), "./image.img",
		sdwire.WithFlashProgress(func(written, total int64) {
			log.Printf("flashed %d / %d bytes", written, total)
		}),
	); err != nil {
		log.Fatal(err)
	}

	mode, err := dev.Mode()
	if err != nil {
		log.Fatal(err)
	}
	log.Println("now in mode:", mode)
}

Other entry points: ListDevices() enumerates every attached device; NewWithSerial/NewWithIdentity connect to a specific one (by bare serial, or by the port-suffixed identity/location forms respectively — identical to what the CLI's -s flag accepts); SetMode/Mode switch and read back the card's side; PowerCycle and TargetPower drive a configured PowerFunc directly, independent of flashing; WithoutRevive() disables the hub-cache power-on fallback for callers that must not risk switching a target-mode SDWire3 back to host mode; CachedPortState reads an SDWire3's mode from the on-disk hub cache without powering anything on or off at all, for exactly that case; WithWriteRetries and WithWriteTiming tune a flash's recovery from a reader that drops off the bus mid-write, and expose its per-chunk write timings; ListDeviceStates() is the same idea for the whole inventory, returning every attached device plus every remembered-but-absent one with the mode each is in; and Revive power-cycles a device's hub port to recover a reader that has been dropped from the bus, addressable by location when no other selector can reach it.

Supported operating systems

  • Linux — tested on Ubuntu, Debian
  • macOS — tested on macOS 10.15+ (uses ioreg/diskutil for block device discovery)
  • Windows — tested on Windows 10+ (needs libusb access to the SDWire3's hub — see Permissions)

Troubleshooting

Device not found. Confirm it's actually attached and enumerating: SDWireC shows up as USB 04e8:6001, SDWire3 as 0bda:0316. On Linux, check the udev rules above; on Windows, check libusb/UsbDk binding.

SDWire3 stuck in target mode. This is expected, not broken — see SDWire3 state semantics. sdwire switch host/ts and sdwire flash (and, in the SDK, New/NewWithSerial/ NewWithIdentity by default) revive it via the on-disk hub-port cache, powering it back on into host mode; state, list and power never do. sdwire revive forces the same thing explicitly, for a device too broken to be selected the usual way.

The reader vanishes mid-flash and only comes back on a physical replug. The SD reader inside an SDWire3 can stop answering under sustained writes. The OS asks its hub for a port reset, the reset fails to bring the port back up, and the device is torn off the bus — after which nothing re-probes that port, so sdwire list reports it as Unknown (or, if the port was powered down too, Target). On macOS the sequence appears in log show as:

USB3.0-CRW@01113000 endpoint 0x01: status 0xe0005000 (pipe stalled): 65536 bytes transferred
USB3.0-CRW@01113000 endpoint 0x01: status 0xe00002ed (transaction error): 0 bytes transferred
IOUSBMassStorageDriver: USB device 0BDA031601113000 - will be reset!
AppleUSB20HubPort::resetAndCreateDevice: reset did not enable port
AppleUSBHostPort::terminateDevice: destroying 0x0bda/0316/0204 (USB3.0-CRW): reset API call

Endpoint 0x01 is the reader's bulk-OUT pipe, so this is the reader crashing mid-write — not a fault in this tool, and not something a port reset clears. Nothing in software can stop the reader stalling; what software can do is not lose the work.

sdwire flash recovers from this automatically, power-cycling the port and resuming where it stopped. Outside a flash, use sdwire revive instead of reaching for the cable; sdwire revive -s <location> works even when the device can no longer be selected by serial.

Two things make it worse and are worth ruling out: a reader on a USB 2.0 port writes at a tenth of the speed and so spends ten times as long in the window where it can fail, and other software grabbing the card (Spotlight indexing a freshly mounted volume, or anything enumerating USB) adds load at exactly the wrong moment.

"device is attached to a root hub port" / a hubpower.ErrRootPort-shaped error. SDWire3 switching works by cutting power to the device's upstream hub port; a device plugged directly into a computer's built-in (root) USB port has no controllable parent to do that through — see the VBUS story above. Attach it behind an external hub with independent per-port power switching instead.

Contributing

Contributions are welcome — please open an issue first for anything more than a small fix.

License

MIT License — see LICENSE.

Related projects


Forked and extended by @jphastings. Originally made with ❤️ at the Recurse Center.

About

CLI & Go package for controlling SDWireC/SDWire3 devices - USB-controlled SD card multiplexers for automated testing and development

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages