This release is a pre-release and may not be stable for production use.
ultimattewire
ultimattewire is a small, dependency-free Python library that archives and restores the configuration of a Blackmagic Ultimatte 12 or Ultimatte 12 4K keyer over its native TCP protocol. It produces and consumes the same zip archives that Blackmagic's Ultimatte Smart Remote 4 (also shipped as "Ultimatte Software Control") writes with Archive All and reads with Restore, so an archive taken with this library restores from the vendor app and vice versa. Blackmagic does not document the protocol; everything here was reverse-engineered from packet captures of the vendor app talking to real hardware, which is why the wire details in the code are marked as not to be changed without re-validating against a unit.
Features
archive_unit_to_bytes(host): pull every saved preset slot plus theGPISettingsandSavedSettingsresources into an in-memory zip in Smart Remote's exact layout, with a human-readable annotated state dump riding along.restore_unit_from_bytes(host, zip_bytes): push such a zip back, in the order the vendor app uses, aborting at the first rejected write.- Reads the unit's text prelude on TCP 9998 (label, firmware release, live control values, the preset FILE LIST) and does binary slot/resource reads and writes on TCP 9996.
- Failed reads are omitted from the archive and reported as warnings instead of being written as placeholder bytes that a later restore would push into live hardware.
- Short reads raise; truncated blobs are never archived.
- One-retry connects to survive cold-ARP and first-packet hiccups.
- Per-parameter display ranges for about 150 control values, used to annotate the state dump (raw
0..10000to percent, frames, pixels). - Pure standard library. No logging; results and warnings are returned to the caller.
Install
pip install "git+https://github.com/lucas-romanenko/ultimattewire.git@v0.1.0.dev0"
Python 3.10 or newer. To run the tests from a checkout:
pip install ".[test]"
python -m pytest
Usage
from ultimattewire import archive_unit_to_bytes, restore_unit_from_bytes
# Archive one unit. The label comes from the unit's own Label field.
zip_bytes, info = archive_unit_to_bytes("192.0.2.21")
with open(f"{info['label']}.zip", "wb") as f:
f.write(zip_bytes)
print(info["slots"], info["warnings"])
# Restore it (or a Smart Remote "Archive All" zip) onto another unit.
with open("Keyer_A.zip", "rb") as f:
ok, report = restore_unit_from_bytes("192.0.2.22", f.read())
if not ok:
print("stopped at", report["stopped_at"])
A zip produced by this library can be opened with Smart Remote 4's Restore
unchanged, and a Smart Remote "Archive All" zip can be passed straight to
restore_unit_from_bytes.
Protocol notes
This is the reverse-engineered part and the most useful thing to read before changing anything.
How it was determined
The wire format was recovered from packet captures of Blackmagic's Ultimatte Smart Remote 4 / Ultimatte Software Control performing Archive All and Restore against a real Ultimatte 12 4K. The library was then validated by round-tripping archives through the vendor app: a zip produced here restores from Smart Remote, and a Smart Remote zip restores through this code. The display ranges come from the Ultimatte 12 Operations Manual (February 2026 revision) and from Smart Remote 4 panel screenshots, not from the wire.
Two TCP channels
| Port | Role | Framing |
|---|---|---|
| 9998 | Text control channel | The unit streams a text prelude on connect |
| 9996 | Binary settings channel | One request per TCP connection, big-endian length-prefixed frames |
The 9998 prelude
On connect the unit immediately sends a text prelude and, if the client does
not continue with the interactive greeting the vendor app uses, may close the
connection partway through. The library reads until the literal
END PRELUDE: marker, then drains for a further 0.3 s, and returns whatever
arrived (decoded as UTF-8 with replacement). The prelude is a sequence of
sections introduced by an upper-case header ending in a colon, each holding
key: value lines. The parts the library uses:
Label: <name>: the unit's user-assigned name. Sanitised to[A-Za-z0-9._-]for use as the archive filename root.Software Release: <x.y>: the firmware version. Not used by the library itself, but this is the field to read if you want it.CONTROL:andCONTROL DEFAULT:: the live and default control values asName: rawlines, raw integers mostly in0..10000. Only used for the annotated text dump.FILE LIST:: one saved preset slot name per line. This is the list of slots the archive will read.
Section extraction is a regular expression anchored on the header and
terminated by the next all-caps header or END PRELUDE:; two pitfalls
already hit and fixed are recorded in _preamble.py (a $ anchor that
truncated multi-line FILE LISTs, and a \s* that let an empty section
swallow the next one).
The 9996 binary channel
Every request opens its own TCP connection; the unit closes it after replying.
Read (binary_request):
send [opcode: u16 BE][len: u32 BE][payload: len bytes]
recv [len: u32 BE][body: len bytes]
Write (upload_one):
send [opcode: u16 BE][name_len: u32 BE][name][data_len: u32 BE][data][0x00]
recv 6 bytes of 0x00 (success ACK)
Known opcodes:
| Opcode | Direction | Payload / name | Meaning |
|---|---|---|---|
0x0000 |
read | preset slot name from FILE LIST | read a saved preset blob |
0x0003 |
read | GPISettings or SavedSettings |
read a named resource |
0x0100 |
write | .../presets/<slot> |
write a preset slot (mirror of 0x0000) |
0x0103 |
write | .../GPISettings or .../SavedSettings |
write a resource (mirror of 0x0003) |
Details that matter and were established by experiment:
- The trailing
0x00on a write frame is mandatory. Without it the unit silently declines to ACK. - The vendor app puts the full filesystem path of the extracted zip member
into the name field (a macOS temporary directory under
Ultimatte Software Control-PYREST/...). The unit appears to inspect only the last path segment, and for slot writes the presence of apresets/segment. The library sends the same path shape for parity. - The ACK is six zero bytes, but the unit closes the socket so quickly after
sending it that a client can see EOF after fewer than six. The library
accepts any all-zero prefix as success; any non-zero byte is treated as a
rejection and raised as
IOError. - A short read of the 4-byte length header, including an immediate clean
EOF, and a short body read both raise
IOError. An empty resource whose header says length 0 returnsb""cleanly. - Blob contents are opaque. The library never parses preset or resource bytes; it moves them verbatim.
Archive layout
The zip must match the vendor app byte for byte in structure, or its Restore
rejects it. Members in this order, with explicit directory entries carrying
0o40755 external attributes:
images/ (empty directory entry)
GPISettings (raw bytes, no length prefix)
SavedSettings (raw bytes, no length prefix)
quickfiles/ (empty directory entry)
presets/ (explicit directory entry; required)
presets/<slot> (one per FILE LIST entry)
<label>_config_readable.txt (this library's addition; ignored by the vendor app)
Restore order
- Every
presets/<slot>member, sorted by name, with opcode0x0100. GPISettingswith opcode0x0103, if present.SavedSettingswith opcode0x0103, if present. It overwrites the live state, so it goes last.
The first failed write aborts the run; restore_unit_from_bytes returns
(False, report) with report["stopped_at"] naming the member. Members
that are not presets or the two resources are ignored, which is how the
annotated text dump rides along harmlessly.
Timeouts and retries
Connect timeout 5 s, read timeout 30 s on writes, 2 s quiet timeout while reading the prelude. Every connect is retried once after 250 ms because a cold unit regularly refuses or times out the very first packet and answers the second.
Verified against
- Ultimatte 12 4K, the unit the captures were taken from and the round-trip tests were run on. The docstrings also name the Ultimatte 12 (HD) as a target of the same protocol.
- The firmware version of the bench unit was not recorded at capture time.
Read it from the prelude's
Software Releaseline on your own unit and treat any difference from the behaviour above as something to re-verify.
Unverified or guessed
- Whether the unit reads anything in the write name field beyond the last
segment and the
presets/anchor. The full vendor path is sent to be safe. - Whether non-zero ACK bytes carry an error code. They are treated as a generic rejection.
images/andquickfiles/are always written as empty directories. A unit that actually holds images or quick files would not have them archived by this library, and no opcode for them is known.- Other opcodes almost certainly exist (the gaps between
0x0000and0x0003, and between0x0100and0x0103, are suggestive). None have been probed. - The interactive greeting the vendor app sends on 9998 after the prelude is not implemented; the library only ever reads the prelude.
read_preambleis bounded perrecvbut has no overall deadline or size cap; a unit that never stops sending would keep it reading.- Ultimatte 12 HD Mini and other models were not tested.
- The display-range table is an interpretation of the manual and the panel UI, not wire data. It affects only the readable text dump.
License
MIT. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ultimattewire-0.1.0.dev0.tar.gz.
File metadata
- Download URL: ultimattewire-0.1.0.dev0.tar.gz
- Upload date:
- Size: 18.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b841dcceeed1a697c4a3e735ddecfb73e7a1813bd0b8d4d7fd41f0bf170c1efc
|
|
| MD5 |
5e580ac39ceddb1c6035fd9f05f78f0c
|
|
| BLAKE2b-256 |
3c108d040f951fa42d13417a91e05f63895da7c07c7ac40f4602c47356d7ac3c
|
Provenance
The following attestation bundles were made for ultimattewire-0.1.0.dev0.tar.gz:
Publisher:
publish.yml on lucas-romanenko/ultimattewire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ultimattewire-0.1.0.dev0.tar.gz -
Subject digest:
b841dcceeed1a697c4a3e735ddecfb73e7a1813bd0b8d4d7fd41f0bf170c1efc - Sigstore transparency entry: 2796280978
- Sigstore integration time:
-
Permalink:
lucas-romanenko/ultimattewire@67d4ca535ae7cc8d0245cadd2e2bd14bdc98f4ca -
Branch / Tag:
refs/heads/main - Owner: https://github.com/lucas-romanenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@67d4ca535ae7cc8d0245cadd2e2bd14bdc98f4ca -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file ultimattewire-0.1.0.dev0-py3-none-any.whl.
File metadata
- Download URL: ultimattewire-0.1.0.dev0-py3-none-any.whl
- Upload date:
- Size: 17.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e8657a264f663136c62cd5bee426cce9c203f294d9fd6dea61b55fbc0260877
|
|
| MD5 |
8ad0c295bd25f7b50461e410d7ddfea0
|
|
| BLAKE2b-256 |
aa650c15d93b1495c6f3cfe70e446bbb4289a72b6a06c68faf9a577277e79f38
|
Provenance
The following attestation bundles were made for ultimattewire-0.1.0.dev0-py3-none-any.whl:
Publisher:
publish.yml on lucas-romanenko/ultimattewire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ultimattewire-0.1.0.dev0-py3-none-any.whl -
Subject digest:
3e8657a264f663136c62cd5bee426cce9c203f294d9fd6dea61b55fbc0260877 - Sigstore transparency entry: 2796281112
- Sigstore integration time:
-
Permalink:
lucas-romanenko/ultimattewire@67d4ca535ae7cc8d0245cadd2e2bd14bdc98f4ca -
Branch / Tag:
refs/heads/main - Owner: https://github.com/lucas-romanenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@67d4ca535ae7cc8d0245cadd2e2bd14bdc98f4ca -
Trigger Event:
workflow_dispatch
-
Statement type: