struct2ui
Render C structs / JSON schemas as an editable PySide6 UI, and convert freely between C / JSON / bin. Built for algorithm parameter tuning: describe a C interface in JSON, auto-generate a Qt form, edit it, then export back to C source, JSON, or binary.
Features
- JSON → UI: describe C structs / enums / arrays with minimal JSON and auto-render Qt widgets (int→QSpinBox, float→QDoubleSpinBox, enum→QComboBox, etc.).
- Embeddable:
StructEditoris a plainQWidgetthat drops into any PySide6 / PyQt UI. - Multi-format export: edited results export to C source, JSON, or binary; C source can also be parsed back into a schema.
- Read bin back:
import_binloads a raw.bin(e.g. read back from the chip) into the UI — the byte-for-byte inverse of the binary export. - Validation: keyword spell-checking (with “Did you mean X?” hints), semantic audits (
min<=max,step>0), and pipeline cross-validation. - ELF layout check: optionally verify each module — and the whole pipeline against its SDK container struct — against a compiled ELF's DWARF layout, catching offset/size drift and wrong stage order.
- Qt-binding agnostic: built on Qt.py — works with PySide6 / PyQt6 / PySide2 / PyQt5.
Installation
pip install struct2ui
# Pick a Qt binding (choose one)
pip install "struct2ui[pyside6]"
pip install "struct2ui[pyqt6]"
# Enable ELF layout verification (optional)
pip install "struct2ui[elf]"
The library itself only depends on
Qt.py; you must install a Qt binding (PySide6 / PyQt6 / etc.) yourself. ELF verification depends onpyelftools, installed via the[elf]extra.
Quick Start
from Qt import QtWidgets
from struct2ui import StructEditor
app = QtWidgets.QApplication([])
editor = StructEditor()
editor.set_sources(
cfg_dir="cfg_t", # modules dir: *.json holding struct/enum/typedef
flow_file="abc.json", # pipeline definition file
)
editor.resize(480, 600)
editor.show()
app.exec_()
JSON Schema Reference
This is the core of the library: you describe your C types in JSON and the UI is
generated from it. Files live in the modules directory (cfg_dir), one
*.json per logical group. Each file is a flat object whose keys are either
type names (your structs / enums) or the reserved key typedefs.
File layout
{
// optional free-form metadata (ignored by the loader, allowed anywhere)
"version": "1.0",
"description": "audio EQ parameters",
// type aliases: map a custom C type to a primitive
"typedefs": { "gain_t": "int32_t", "freq_t": "float" },
// a struct definition
"eq_cfg_t": {
"type": "struct",
"items": [
{ "name": "enabled", "type": "uint8_t", "value": 1 },
{ "name": "gain", "type": "gain_t", "value": 0,
"min": -12, "max": 12, "step": 1, "unit": "dB",
"tip": "output gain", "when": { "enabled": 1 } }
]
},
// an enum definition
"mode_t": {
"type": "enum",
"items": { "OFF": 0, "LOW": 1, "HIGH": 2 }
}
}
Top-level blocks
| Block type | Keys | Notes |
|---|---|---|
struct |
type, items (list of field specs) |
items is required |
enum |
type, items (object {NAME: value}) |
reads back the enumerator name |
union |
type, items (list of branch structs), branch (optional) |
tagged union; each branch is an inline struct selected by its when (see below) |
typedefs |
object {alias: real_type} |
resolved before field building |
Free-form metadata keys are allowed and ignored at any level: version,
description, author, comment, note.
Field properties (inside a struct's items)
Each entry in items is a field spec. Only the following keys are
recognized — any other key triggers an "unknown keyword" error (with a
"Did you mean …?" hint):
| Property | Applies to | Description |
|---|---|---|
name |
all | Required. C field name. |
type |
all | Required. C type or a typedefs alias (e.g. int32_t, float, char, an enum/struct name). |
value |
all | Default value. For arrays, a list (per element) or a scalar (applied to every element). |
count |
arrays | Makes the field an array. Integer, a #define name, an expression (N - 1), or a list for multi-dim ([3, 4], stored flat as 12). |
min / max |
int, float | Numeric bounds. Validated min <= max. Drive spin-box / slider / dial ranges. Like count, each may be an integer, a #define name, or an integer expression of those (e.g. MAX_GAIN - 1); it is resolved to a number at load time. |
step |
int, float | Increment, must be > 0. |
decimals |
float | Number of fractional digits shown/stored. Non-negative integer (capped at 18). If omitted, it is derived from step (e.g. step: 0.5 → 1 digit); if neither is set, defaults to 3. For double fields needing more precision, set decimals explicitly (e.g. 15-16). |
unit |
all | Unit suffix shown in the label, e.g. gain (dB). |
tip |
all | Tooltip text. |
choices |
int, float, char[N] |
Discrete value set rendered as a combo (see below). |
when |
all | Conditional enable/disable (see below). |
widget |
all | Override the auto-picked editor (see table below). |
widget values
When omitted, the widget is inferred from type (and from min/max,
choices). Override it explicitly with widget:
widget |
Valid for | Renders as |
|---|---|---|
checkbox |
int | Check box (also auto-picked when min:0, max:1). |
toggle |
bool/int | Toggle push-button. |
combo |
int, float, char[N] |
Combo box; pair with choices. |
slider |
int | Slider with min/max labels (requires min & max). |
dial |
int | Rotary dial popup (requires min & max). |
file |
array | Path label + Browse button; loads array values from a text file. |
multiline |
array | Single-line shell that pops up a multi-line editor. |
table |
struct array | Force grid layout (one column per struct member; a union member pinned via branch expands its branch members into columns). |
Numeric fields without an explicit widget become a QSpinBox /
QDoubleSpinBox; scalar arrays become a comma-separated line edit.
choices (discrete values)
A non-enum integer / float / char[N] field can be constrained to a fixed set.
Two forms are accepted:
"choices": [0, 1, 2, 3] // label = str(value)
"choices": [{ "Off": 0 }, { "Low": 1 }, { "High": 2 }] // explicit labels
The combo reads back the underlying value (not the label), so C / JSON / bin
export still emits the number (or string for char[N]). Audits enforce: each
value within [min, max] when declared, and value (the default) must be one
of the choices.
when (conditional enable/disable)
A field can be greyed out unless other fields hold specific values. when is an
object mapping a sibling field name to its required value; all entries must
match (logical AND):
{ "name": "cutoff", "type": "float", "when": { "enabled": 1, "mode": "HIGH" } }
The dependency name is matched against the dotted leaf name in scope. When a dependency is not found, the field stays enabled.
union (tagged union) & branch
A union block declares overlapping branches, each an inline struct in
items. At runtime the branch whose when matches is the one shown / exported
(first match wins); the union's .bin / .c size is always the largest branch
and unmatched exports are zero-padded.
{ "name": "FilterCoeffs", "type": "union", "items": [
{ "name": "raw", "type": "struct", "when": { "type": "IIR_BIQUARD_RAW" },
"items": [ /* 5 floats */ ] },
{ "name": "design", "type": "struct", "when": { "type": { "!=": "IIR_BIQUARD_RAW" } },
"items": [ { "name": "f0", "type": "float" }, /* ... */ ] }
] }
Set branch on the union to pin it to a single branch: only that branch is
a candidate (the others are never shown and their when is ignored); the pinned
branch is still gated by its own when. Pinning to a flat (all-leaf) branch lets
a struct array carrying the union render as a table — the branch members
become columns, and a row whose discriminator makes the branch when false has
those cells disabled (value kept, still exported zero-padded).
{ "name": "coeffs", "type": "FilterCoeffs", "branch": "design" }
count expressions & multi-dimensional arrays
count may be an integer, a #define constant name, or an integer expression
of those using + - * / % () (e.g. MAX_BAND_NUM - 1). A list makes a
multi-dimensional array that is stored flat to match the C ABI:
{ "name": "matrix", "type": "float", "count": [3, 4] } // 12 contiguous floats, shown as [3][4]
char[N] is treated as a C string by default (use widget to override).
Validation
Loading runs three layers of checks, surfaced in the load report panel:
- Keyword spelling — unknown keys report "Did you mean
tip?" style hints. - Semantic audits —
min <= max,step > 0,count > 0, choices within bounds, default is one of the choices. - Pipeline cross-validation — values in the pipeline file are checked against the schema (range, choices, enum membership).
Pipeline File
While the modules directory (cfg_dir) defines your types, the
pipeline file (flow_file) describes one concrete instance: which module
stages are present, in what order, and their edited values. It is a single JSON
object with one top-level group key (any name you like, e.g. the product /
tuning name) whose value is the group body:
{
"Speech Tuning": {
"flow": ["RX NS", "RX DRC", "RX EQ"], // stage order, required
"container": "SpeechConfig", // optional, enables layout check
"RX NS": { "type": "SpeechNs2FloatConfig", "items": { /* values */ } },
"RX DRC": { "type": "MultiCompexpConfig" }, // 'items' may be omitted
"RX EQ": { "type": "EqConfig", "items": { /* values */ } }
}
}
Group-level keys
| Key | Required | Notes |
|---|---|---|
flow |
yes | Ordered list of stage names. The flow buttons and the export order follow it. Every listed name must have a stage block; defined stages missing from flow are a warning. |
container |
no | Name of the SDK struct that holds all modules (see Container layout check). Omit to disable that check. |
| stage name | — | Any other key is a stage block; its name is the label shown on the flow button. |
All keys are lowercase and case-sensitive — write flow, not Flow. A
mistyped key (Flow, flwo, …) is reported with a "Did you mean flow?" hint.
Free-form metadata keys (version, description, author, comment, note)
are allowed and ignored.
Stage block
| Key | Required | Notes |
|---|---|---|
type |
yes | A struct / union type name defined in the modules directory. |
items |
no | The edited values, keyed by field name. Omitting items means "all defaults" — the stage renders from the type's declared defaults. |
member |
no | For the container layout check: the member name inside container this stage maps to (defaults to the stage label). |
Container layout check
When container names an SDK struct and an ELF is loaded, the whole pipeline is
laid out in flow order — as if each stage were a member of that container —
and compared member-by-member against the struct's DWARF layout (offset, size,
total size). This catches stages in the wrong order: if the modules are all
correct but reordered, you get a single "module order is wrong" error naming the
expected order, instead of a pile of misleading offset mismatches.
Embedding into an Existing UI
StructEditor is a regular QWidget; just put it into a layout:
from Qt import QtWidgets
from struct2ui import StructEditor
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
editor = StructEditor()
editor.set_sources(cfg_dir="cfg_t", flow_file="abc.json")
self.setCentralWidget(editor)
StructEditor takes no source paths at construction time and keeps no
persistent state of its own. Point it at its sources after creating it with
set_sources() (or let the user pick them at runtime via Settings). If you
want to remember the last-used paths across runs, store them in the host app
and call set_sources() again at startup.
Constructor Parameters
| Parameter | Description |
|---|---|
parent |
Qt parent object, defaults to None |
appearance |
Button appearance overrides, {key: {'mode': ..., 'icon': ..., 'text': ...}} |
custom_button_position |
Where add_custom_button() drops buttons relative to the built-ins: 'end' (after, default) or 'start' (before / far left) |
Setting Sources
Source paths are configured after construction, not passed to the constructor. Omitting an argument leaves that slot untouched.
editor.set_sources(
cfg_dir="cfg_t", # modules dir: *.json with struct/enum/typedef defs
flow_file="abc.json", # pipeline definition file
elf_file="fw.elf", # optional ELF for .bin layout verification
)
set_sources argument |
Description |
|---|---|
cfg_dir |
Modules directory holding *.json (struct / enum / typedef definitions) |
flow_file |
Path to the pipeline JSON file (pipeline definition) |
elf_file |
Path to an ELF for .bin layout verification; setting it enables verification (toggle later via the Settings checkbox, which keeps the path) |
elf_enabled |
Override whether ELF verification runs, independent of whether a path is set |
reload |
Rebuild schema / flow / UI from the new sources, defaults to True |
async_load |
Run that reload on a background thread (defaults to False). See Async Loading |
Toolbar orientation is set with set_flow_orientation('horizontal' | 'vertical').
Async Loading
Parsing the modules, validating the pipeline and (optionally) reading the ELF
all happen during the reload. For large module sets or ELFs this can take a
while, and by default it runs synchronously — set_sources() blocks until the
editor is fully built, which is what programmatic callers and tests want.
A GUI host can avoid the startup freeze by passing async_load=True after
show(): the window appears immediately and shows a Loading... placeholder
while the heavy parsing runs on a worker thread, then swaps in the real content
when it is ready. Reloads triggered while one is already running supersede the
older one.
editor.show()
editor.set_sources(
cfg_dir="cfg_t",
flow_file="abc.json",
elf_file="fw.elf",
async_load=True, # parse off the GUI thread; window stays responsive
)
Custom Toolbar Buttons
Host applications can add their own buttons to the top path bar via
add_custom_button(). All custom buttons form a single group, separated from
the built-in buttons by a divider that is added automatically — you only choose
which side they sit on (once, via the custom_button_position constructor arg).
editor = StructEditor(custom_button_position="end")
editor.set_sources(cfg_dir="cfg_t", flow_file="abc.json")
def on_send():
print(editor.current_values())
btn = editor.add_custom_button(
"Send", # tooltip / accessible label (shown when no icon)
on_send, # callable invoked on click (no arguments)
icon="icons/send.png", # optional; falls back to text when missing
checkable=False, # optional; make the button toggleable
)
# `btn` is the created QPushButton, returned so you can tweak it further.
| Parameter | Description |
|---|---|
text |
Tooltip / accessible label, also shown when no icon is given. Required. |
on_click |
Callable invoked on click, takes no arguments. Required. |
icon |
Path to an icon file; falls back to text when the path is missing/empty. |
checkable |
Make the button toggleable, defaults to False. |
Returns the created QPushButton.
Programmatic API
Beyond the UI, StructEditor exposes methods to read state and drive exports
from code (e.g. wired to a custom button):
| Method | Description |
|---|---|
set_sources(cfg_dir=None, flow_file=None, elf_file=None, elf_enabled=None, reload=True, async_load=False) |
Point the editor at its source files after construction. See Setting Sources. |
set_flow_orientation(orientation) |
Switch the flow-button strip between 'horizontal' (top, default) and 'vertical' (left). Rebuilds the strip and keeps the active stage selected. |
show_section(label) -> None |
Open the stage/section named label in the content area (the same as clicking its flow button). Paints the error panel instead when that stage has pipeline errors; clears the content when the label is unknown. |
current_values() -> dict |
Live editor values of the active section, keyed by field name (empty dict when no parameter section is shown). |
current_paths() -> dict |
Current {'modules', 'pipeline', 'elf'} file paths; an empty string means that slot is unset. |
add_custom_button(text, on_click, icon=None, checkable=False) -> QPushButton |
Add a host button to the top path bar. See Custom Toolbar Buttons. |
export_bin(path, show_dialogs=False) -> bool |
Write the current pipeline to a .bin at path — the programmatic equivalent of clicking Export → Binary. Returns True when there are no ELF layout errors (or ELF verification is off), False otherwise. |
import_bin(source, show_dialogs=False) -> bool |
Load a raw .bin image (e.g. read back from the chip) into the UI — the inverse of export_bin. source is a path or bytes. The image length must match the pipeline layout exactly; on any mismatch the image is rejected and the UI is left unchanged. On success it overwrites every stage's values and re-renders. |
export_bin verifies the ELF layout first when an ELF is selected and enabled (the file is
still written, matching the UI flow). With show_dialogs=True it pops the same
success / ELF-mismatch / failure dialogs the UI shows; with the default
show_dialogs=False no dialogs appear and render/write failures are raised as
exceptions for the caller to handle.
paths = editor.current_paths() # {'modules': ..., 'pipeline': ..., 'elf': ...}
name = paths['pipeline'] # derive an output name from the pipeline
ok = editor.export_bin("out/speech.bin") # True when ELF layout is clean
import_bin is the reverse trip: feed it a .bin the chip sent back and the UI
is repopulated from it. Because the image is a byte-for-byte C-ABI layout, its
length must match the pipeline layout exactly — a short or long image is
rejected wholesale so a stale/mismatched dump can never partially corrupt the
form.
ok = editor.import_bin("out/speech.bin") # True when the image was applied
Export API
The low-level export functions can be used standalone, without any UI:
from struct2ui.schema import SchemaRegistry
from struct2ui.exporters import (
emit_c, # sections + registry -> C source string
dumps_json, # -> JSON string
emit_bin, # -> binary bytes
read_bin, # binary bytes -> values dict (inverse of emit_bin)
merge_abi, # merge ABI info
verify_sections, # verify .bin layout against an ELF
parse_c_source, # C source -> parse result
build_schema_dict,
)
Architecture
| Layer | Module | Responsibility |
|---|---|---|
| Schema (pure data, no Qt) | struct2ui.schema |
Parse *.json into a Field tree; spell-checking, semantic audits, pipeline cross-validation |
| UI rendering | struct2ui.ui |
WidgetFactory, FormRenderer/TreeRenderer, array tables, when conditional binding |
| Export | struct2ui.exporters |
C / JSON / bin export, C source reverse parsing, ELF verification |
| Top-level widget | struct2ui.StructEditor |
Action toolbar / content area; Settings panel for source paths; load report panel |
Development
pip install -e ".[dev]" --no-build-isolation
python -m pytest
Tests live under tests/ and drive real Qt widgets headlessly on the offscreen platform.
License
MIT © Jay
Metadata
Release files for struct2ui 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| struct2ui-0.5.0.tar.gz | 128.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| struct2ui-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 233.5 kB
Release files / struct2ui-0.5.0.tar.gz
| Download URL | struct2ui-0.5.0.tar.gz |
|---|---|
| Size | 128.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ccfe09b8cb042fba6c6fc7efda798c7e6450ad6c8d97ec9c1749da46ab0e4de6
|
|
BLAKE2b-256 checksum How to use checksums |
4f3c2bc2a8b7fed93efbbb3686c3524a5b467d91a0ba0556d041dde7bc5df20c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / struct2ui-0.5.0-py3-none-any.whl
| Download URL | struct2ui-0.5.0-py3-none-any.whl |
|---|---|
| Size | 105.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bb6eeda6ad9a8b08508463f37458c37807e9300e521c153725f2eaf1918c0ebf
|
|
BLAKE2b-256 checksum How to use checksums |
7e59b5bbc70c577cfe5b601f9cb8b651df81f2ff14ad051d687c0210862d7ca7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|