Skip to content

Architecture and Packages

Espen Hovland edited this page Jun 5, 2026 · 2 revisions

Architecture & Packages

PyCommander is a small monorepo of cooperating packages plus a thin runtime that locates and runs the bundled Simplicity Commander executable. This page explains how the pieces fit together — useful if you are contributing, debugging an installation, or just curious about what happens under the hood.

The package family

silabs-pycommander            (meta-package: clean install UX)
├── silabs-pycommander-cli     (CLI flavor;  bundles commander-cli executable)
│   └── silabs-pycommander-core
└── silabs-pycommander-gui     (GUI flavor;  bundles commander executable)   [extra: gui]
    └── silabs-pycommander-core

silabs-pycommander-core        (the actual Python framework — no executable)
  • pycommander-core contains all the real logic: the CommanderBase class, the command modules, the high-level Adapter/Target/AemStreamBase classes, the subprocess Runner, the typed dataclasses, and the error types. It contains no Commander executable and is not usable on its own.
  • pycommander-cli and pycommander-gui are flavor packages. Each one:
    • bundles a platform-specific Simplicity Commander archive under an _archive/ resource directory,
    • exposes flavor-specific Commander, Adapter, Target and AemStream classes that subclass the *Base classes from core and hard-wire cli=True/cli=False,
    • provides a console entry point (pycommander-cli / pycommander-gui).
  • pycommander is a meta-package: it depends on pycommander-cli (and on pycommander-gui via the gui extra) and provides the user-facing pycommander command and import pycommander.

Flavor selection

When you import pycommander or run pycommander, the GUI flavor is preferred when present:

try:
    from pycommander_gui import Commander   # GUI if installed
    CLI = False
except ImportError:
    from pycommander_cli import Commander   # otherwise CLI
    CLI = True

If neither flavor is installed, importing fails with a helpful message telling you to pip install silabs-pycommander-cli or silabs-pycommander-gui.

How the bundled executable is found

Pure-Python though it is, each flavor wheel carries a platform-specific Commander archive. On first use, pycommander_core._ensure_commander.ensure_commander() makes sure the executable exists:

  1. It locates the bundled archive (*.zip on macOS/Windows, *.tar.bz on Linux) inside the flavor package's _archive/ resources.
  2. It computes a short SHA-256 hash of that archive.
  3. It extracts the archive (once) into a per-user cache directory: user_cache_dir("pycommander", "silabs") / <archive-hash>. The hash in the path means a new Commander version lands in a fresh directory and is re-extracted automatically.
  4. It returns the path to the platform-specific executable inside that cache.

Extraction is platform-aware: ditto on macOS, tar on Linux, and PowerShell Expand-Archive on Windows. The executable's expected relative path per platform lives in pycommander_core.paths:

Platform CLI executable GUI executable
Windows Simplicity Commander CLI/commander-cli.exe Simplicity Commander/commander.exe
macOS Commander-cli.app/Contents/MacOS/commander-cli Commander.app/Contents/MacOS/commander
Linux commander-cli/commander-cli commander/commander

You can bypass all of this by passing an explicit executable_path= to Commander (or PyCommanderCLI), which is handy for testing against a specific Commander build.

How commands run

All hardware/CLI interaction funnels through pycommander_core.runner.Runner:

  • run(*args, json_format=True) runs Commander synchronously via subprocess.run(...). When json_format is true (the default), --json is appended so output can be parsed as JSON. The call uses a timeout (default 300 seconds, CommanderBase.default_timeout_s).
    • A TimeoutExpired becomes a Python TimeoutError.
    • A non-zero exit becomes a typed exception: return code -1/255PyCommanderInputError, -2/254PyCommanderRuntimeError, anything else → PyCommanderError. See Error-Handling.
  • open(*args) starts Commander asynchronously (a subprocess.Popen) for streaming use cases such as AemStream. The runner can then sendCtrlC, terminate, kill, wait, and gracefully close the process.
  • Logging. If a log_file_path was supplied, every invocation is appended to the log with a timestamp. See Logging.
  • Windows niceties. On Windows the runner sets CREATE_NO_WINDOW, suppresses the GPF crash dialog, and uses CREATE_NEW_PROCESS_GROUP for async processes (so Ctrl-Break can be delivered).

The command layer

CommanderBase.__init__ instantiates one command object per CLI suite and attaches it as an attribute, deriving the attribute name from the class name:

AdapterCommand  -> commander.adapter
FlashCommand    -> commander.flash
DeviceCommand   -> commander.device
...

Each command class subclasses BaseCommand, which provides helpers for assembling CLI arguments — connection options (--serialno/--ip/--identifybyserialport), debug options (--speed/--tif/--irpre/--drpre), the --device option, and value formatters for ranges, patches, regions, tokens and ELF sections. Each public method builds an argument list and calls self._run(...), which returns the parsed-JSON dict.

The full surface is documented in the Command-Reference.

The high-level layer

On top of the command layer sit the task-oriented classes, which parse Commander's JSON into the typed dataclasses in Data-Types:

  • AdapterBase → flavor Adapter — adapter info, reset, firmware upgrade, supply voltage, VCOM, energy analysis.
  • Target — target info, flashing, erasing, CTUNE, protection, security, keys, region config, SE firmware.
  • AemStreamBase → flavor AemStream — an iterable, unbounded AEM measurement stream.

Repository layout

pycommander/
├── README.md
├── CODE_OF_CONDUCT.md
├── archives/                       # platform Commander archives used at release time
├── scripts/
│   └── run_tests.py                # discovers & runs all package test suites
├── .github/workflows/
│   ├── unittest-and-coverage.yml
│   └── build-wheels-and-publish.yml
└── packages/
    ├── pycommander/                # meta-package
    ├── pycommander-cli/            # CLI flavor (bundles commander-cli)
    ├── pycommander-gui/            # GUI flavor (bundles commander)
    └── pycommander-core/           # the framework
        └── src/pycommander_core/
            ├── commander_base.py   # CommanderBase
            ├── adapter_base.py     # AdapterBase
            ├── target.py           # Target
            ├── aemstream_base.py   # AemStreamBase
            ├── runner.py           # subprocess Runner
            ├── types.py            # dataclasses / enums
            ├── errors.py           # exception hierarchy
            ├── paths.py            # per-platform executable paths
            ├── _ensure_commander.py# extract bundled executable
            └── commands/           # one module per CLI suite

See Contributing-and-Development for how these are tested.

Clone this wiki locally