ultimattewire 0.1.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lucas Romanenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: ultimattewire
3
+ Version: 0.1.0.dev0
4
+ Summary: Blackmagic Ultimatte 12 archive and restore over its native TCP protocol (Smart Remote 4 compatible zips)
5
+ Author: Lucas Romanenko
6
+ License: MIT
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest; extra == "test"
15
+ Dynamic: license-file
16
+
17
+ # ultimattewire
18
+
19
+ ultimattewire is a small, dependency-free Python library that archives and
20
+ restores the configuration of a Blackmagic **Ultimatte 12** or **Ultimatte
21
+ 12 4K** keyer over its native TCP protocol. It produces and consumes the same
22
+ zip archives that Blackmagic's Ultimatte Smart Remote 4 (also shipped as
23
+ "Ultimatte Software Control") writes with **Archive All** and reads with
24
+ **Restore**, so an archive taken with this library restores from the vendor
25
+ app and vice versa. Blackmagic does not document the protocol; everything here
26
+ was reverse-engineered from packet captures of the vendor app talking to real
27
+ hardware, which is why the wire details in the code are marked as not to be
28
+ changed without re-validating against a unit.
29
+
30
+ ## Features
31
+
32
+ - `archive_unit_to_bytes(host)`: pull every saved preset slot plus the `GPISettings` and `SavedSettings` resources into an in-memory zip in Smart Remote's exact layout, with a human-readable annotated state dump riding along.
33
+ - `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.
34
+ - 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.
35
+ - 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.
36
+ - Short reads raise; truncated blobs are never archived.
37
+ - One-retry connects to survive cold-ARP and first-packet hiccups.
38
+ - Per-parameter display ranges for about 150 control values, used to annotate the state dump (raw `0..10000` to percent, frames, pixels).
39
+ - Pure standard library. No logging; results and warnings are returned to the caller.
40
+
41
+ ## Install
42
+
43
+ ```sh
44
+ pip install "git+https://github.com/lucas-romanenko/ultimattewire.git@v0.1.0.dev0"
45
+ ```
46
+
47
+ Python 3.10 or newer. To run the tests from a checkout:
48
+
49
+ ```sh
50
+ pip install ".[test]"
51
+ python -m pytest
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ```python
57
+ from ultimattewire import archive_unit_to_bytes, restore_unit_from_bytes
58
+
59
+ # Archive one unit. The label comes from the unit's own Label field.
60
+ zip_bytes, info = archive_unit_to_bytes("192.0.2.21")
61
+ with open(f"{info['label']}.zip", "wb") as f:
62
+ f.write(zip_bytes)
63
+ print(info["slots"], info["warnings"])
64
+
65
+ # Restore it (or a Smart Remote "Archive All" zip) onto another unit.
66
+ with open("Keyer_A.zip", "rb") as f:
67
+ ok, report = restore_unit_from_bytes("192.0.2.22", f.read())
68
+ if not ok:
69
+ print("stopped at", report["stopped_at"])
70
+ ```
71
+
72
+ A zip produced by this library can be opened with Smart Remote 4's Restore
73
+ unchanged, and a Smart Remote "Archive All" zip can be passed straight to
74
+ `restore_unit_from_bytes`.
75
+
76
+ ## Protocol notes
77
+
78
+ This is the reverse-engineered part and the most useful thing to read before
79
+ changing anything.
80
+
81
+ ### How it was determined
82
+
83
+ The wire format was recovered from packet captures of Blackmagic's Ultimatte
84
+ Smart Remote 4 / Ultimatte Software Control performing **Archive All** and
85
+ **Restore** against a real Ultimatte 12 4K. The library was then validated by
86
+ round-tripping archives through the vendor app: a zip produced here restores
87
+ from Smart Remote, and a Smart Remote zip restores through this code. The
88
+ display ranges come from the Ultimatte 12 Operations Manual (February 2026
89
+ revision) and from Smart Remote 4 panel screenshots, not from the wire.
90
+
91
+ ### Two TCP channels
92
+
93
+ | Port | Role | Framing |
94
+ |---|---|---|
95
+ | 9998 | Text control channel | The unit streams a text prelude on connect |
96
+ | 9996 | Binary settings channel | One request per TCP connection, big-endian length-prefixed frames |
97
+
98
+ ### The 9998 prelude
99
+
100
+ On connect the unit immediately sends a text prelude and, if the client does
101
+ not continue with the interactive greeting the vendor app uses, may close the
102
+ connection partway through. The library reads until the literal
103
+ `END PRELUDE:` marker, then drains for a further 0.3 s, and returns whatever
104
+ arrived (decoded as UTF-8 with replacement). The prelude is a sequence of
105
+ sections introduced by an upper-case header ending in a colon, each holding
106
+ `key: value` lines. The parts the library uses:
107
+
108
+ - `Label: <name>`: the unit's user-assigned name. Sanitised to
109
+ `[A-Za-z0-9._-]` for use as the archive filename root.
110
+ - `Software Release: <x.y>`: the firmware version. Not used by the library
111
+ itself, but this is the field to read if you want it.
112
+ - `CONTROL:` and `CONTROL DEFAULT:`: the live and default control values as
113
+ `Name: raw` lines, raw integers mostly in `0..10000`. Only used for the
114
+ annotated text dump.
115
+ - `FILE LIST:`: one saved preset slot name per line. This is the list of
116
+ slots the archive will read.
117
+
118
+ Section extraction is a regular expression anchored on the header and
119
+ terminated by the next all-caps header or `END PRELUDE:`; two pitfalls
120
+ already hit and fixed are recorded in `_preamble.py` (a `$` anchor that
121
+ truncated multi-line FILE LISTs, and a `\s*` that let an empty section
122
+ swallow the next one).
123
+
124
+ ### The 9996 binary channel
125
+
126
+ Every request opens its own TCP connection; the unit closes it after
127
+ replying.
128
+
129
+ Read (`binary_request`):
130
+
131
+ ```
132
+ send [opcode: u16 BE][len: u32 BE][payload: len bytes]
133
+ recv [len: u32 BE][body: len bytes]
134
+ ```
135
+
136
+ Write (`upload_one`):
137
+
138
+ ```
139
+ send [opcode: u16 BE][name_len: u32 BE][name][data_len: u32 BE][data][0x00]
140
+ recv 6 bytes of 0x00 (success ACK)
141
+ ```
142
+
143
+ Known opcodes:
144
+
145
+ | Opcode | Direction | Payload / name | Meaning |
146
+ |---|---|---|---|
147
+ | `0x0000` | read | preset slot name from FILE LIST | read a saved preset blob |
148
+ | `0x0003` | read | `GPISettings` or `SavedSettings` | read a named resource |
149
+ | `0x0100` | write | `.../presets/<slot>` | write a preset slot (mirror of `0x0000`) |
150
+ | `0x0103` | write | `.../GPISettings` or `.../SavedSettings` | write a resource (mirror of `0x0003`) |
151
+
152
+ Details that matter and were established by experiment:
153
+
154
+ - The trailing `0x00` on a write frame is mandatory. Without it the unit
155
+ silently declines to ACK.
156
+ - The vendor app puts the full filesystem path of the extracted zip member
157
+ into the name field (a macOS temporary directory under
158
+ `Ultimatte Software Control-PYREST/...`). The unit appears to inspect only
159
+ the last path segment, and for slot writes the presence of a `presets/`
160
+ segment. The library sends the same path shape for parity.
161
+ - The ACK is six zero bytes, but the unit closes the socket so quickly after
162
+ sending it that a client can see EOF after fewer than six. The library
163
+ accepts any all-zero prefix as success; any non-zero byte is treated as a
164
+ rejection and raised as `IOError`.
165
+ - A short read of the 4-byte length header, including an immediate clean
166
+ EOF, and a short body read both raise `IOError`. An empty resource whose
167
+ header says length 0 returns `b""` cleanly.
168
+ - Blob contents are opaque. The library never parses preset or resource
169
+ bytes; it moves them verbatim.
170
+
171
+ ### Archive layout
172
+
173
+ The zip must match the vendor app byte for byte in structure, or its Restore
174
+ rejects it. Members in this order, with explicit directory entries carrying
175
+ `0o40755` external attributes:
176
+
177
+ ```
178
+ images/ (empty directory entry)
179
+ GPISettings (raw bytes, no length prefix)
180
+ SavedSettings (raw bytes, no length prefix)
181
+ quickfiles/ (empty directory entry)
182
+ presets/ (explicit directory entry; required)
183
+ presets/<slot> (one per FILE LIST entry)
184
+ <label>_config_readable.txt (this library's addition; ignored by the vendor app)
185
+ ```
186
+
187
+ ### Restore order
188
+
189
+ 1. Every `presets/<slot>` member, sorted by name, with opcode `0x0100`.
190
+ 2. `GPISettings` with opcode `0x0103`, if present.
191
+ 3. `SavedSettings` with opcode `0x0103`, if present. It overwrites the live
192
+ state, so it goes last.
193
+
194
+ The first failed write aborts the run; `restore_unit_from_bytes` returns
195
+ `(False, report)` with `report["stopped_at"]` naming the member. Members
196
+ that are not presets or the two resources are ignored, which is how the
197
+ annotated text dump rides along harmlessly.
198
+
199
+ ### Timeouts and retries
200
+
201
+ Connect timeout 5 s, read timeout 30 s on writes, 2 s quiet timeout while
202
+ reading the prelude. Every connect is retried once after 250 ms because a
203
+ cold unit regularly refuses or times out the very first packet and answers
204
+ the second.
205
+
206
+ ### Verified against
207
+
208
+ - Ultimatte 12 4K, the unit the captures were taken from and the round-trip
209
+ tests were run on. The docstrings also name the Ultimatte 12 (HD) as a
210
+ target of the same protocol.
211
+ - The firmware version of the bench unit was not recorded at capture time.
212
+ Read it from the prelude's `Software Release` line on your own unit and
213
+ treat any difference from the behaviour above as something to re-verify.
214
+
215
+ ### Unverified or guessed
216
+
217
+ - Whether the unit reads anything in the write name field beyond the last
218
+ segment and the `presets/` anchor. The full vendor path is sent to be safe.
219
+ - Whether non-zero ACK bytes carry an error code. They are treated as a
220
+ generic rejection.
221
+ - `images/` and `quickfiles/` are always written as empty directories. A unit
222
+ that actually holds images or quick files would not have them archived by
223
+ this library, and no opcode for them is known.
224
+ - Other opcodes almost certainly exist (the gaps between `0x0000` and
225
+ `0x0003`, and between `0x0100` and `0x0103`, are suggestive). None have been
226
+ probed.
227
+ - The interactive greeting the vendor app sends on 9998 after the prelude is
228
+ not implemented; the library only ever reads the prelude.
229
+ - `read_preamble` is bounded per `recv` but has no overall deadline or size
230
+ cap; a unit that never stops sending would keep it reading.
231
+ - Ultimatte 12 HD Mini and other models were not tested.
232
+ - The display-range table is an interpretation of the manual and the panel
233
+ UI, not wire data. It affects only the readable text dump.
234
+
235
+ ## License
236
+
237
+ MIT. See `LICENSE`.
@@ -0,0 +1,221 @@
1
+ # ultimattewire
2
+
3
+ ultimattewire is a small, dependency-free Python library that archives and
4
+ restores the configuration of a Blackmagic **Ultimatte 12** or **Ultimatte
5
+ 12 4K** keyer over its native TCP protocol. It produces and consumes the same
6
+ zip archives that Blackmagic's Ultimatte Smart Remote 4 (also shipped as
7
+ "Ultimatte Software Control") writes with **Archive All** and reads with
8
+ **Restore**, so an archive taken with this library restores from the vendor
9
+ app and vice versa. Blackmagic does not document the protocol; everything here
10
+ was reverse-engineered from packet captures of the vendor app talking to real
11
+ hardware, which is why the wire details in the code are marked as not to be
12
+ changed without re-validating against a unit.
13
+
14
+ ## Features
15
+
16
+ - `archive_unit_to_bytes(host)`: pull every saved preset slot plus the `GPISettings` and `SavedSettings` resources into an in-memory zip in Smart Remote's exact layout, with a human-readable annotated state dump riding along.
17
+ - `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.
18
+ - 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.
19
+ - 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.
20
+ - Short reads raise; truncated blobs are never archived.
21
+ - One-retry connects to survive cold-ARP and first-packet hiccups.
22
+ - Per-parameter display ranges for about 150 control values, used to annotate the state dump (raw `0..10000` to percent, frames, pixels).
23
+ - Pure standard library. No logging; results and warnings are returned to the caller.
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ pip install "git+https://github.com/lucas-romanenko/ultimattewire.git@v0.1.0.dev0"
29
+ ```
30
+
31
+ Python 3.10 or newer. To run the tests from a checkout:
32
+
33
+ ```sh
34
+ pip install ".[test]"
35
+ python -m pytest
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ from ultimattewire import archive_unit_to_bytes, restore_unit_from_bytes
42
+
43
+ # Archive one unit. The label comes from the unit's own Label field.
44
+ zip_bytes, info = archive_unit_to_bytes("192.0.2.21")
45
+ with open(f"{info['label']}.zip", "wb") as f:
46
+ f.write(zip_bytes)
47
+ print(info["slots"], info["warnings"])
48
+
49
+ # Restore it (or a Smart Remote "Archive All" zip) onto another unit.
50
+ with open("Keyer_A.zip", "rb") as f:
51
+ ok, report = restore_unit_from_bytes("192.0.2.22", f.read())
52
+ if not ok:
53
+ print("stopped at", report["stopped_at"])
54
+ ```
55
+
56
+ A zip produced by this library can be opened with Smart Remote 4's Restore
57
+ unchanged, and a Smart Remote "Archive All" zip can be passed straight to
58
+ `restore_unit_from_bytes`.
59
+
60
+ ## Protocol notes
61
+
62
+ This is the reverse-engineered part and the most useful thing to read before
63
+ changing anything.
64
+
65
+ ### How it was determined
66
+
67
+ The wire format was recovered from packet captures of Blackmagic's Ultimatte
68
+ Smart Remote 4 / Ultimatte Software Control performing **Archive All** and
69
+ **Restore** against a real Ultimatte 12 4K. The library was then validated by
70
+ round-tripping archives through the vendor app: a zip produced here restores
71
+ from Smart Remote, and a Smart Remote zip restores through this code. The
72
+ display ranges come from the Ultimatte 12 Operations Manual (February 2026
73
+ revision) and from Smart Remote 4 panel screenshots, not from the wire.
74
+
75
+ ### Two TCP channels
76
+
77
+ | Port | Role | Framing |
78
+ |---|---|---|
79
+ | 9998 | Text control channel | The unit streams a text prelude on connect |
80
+ | 9996 | Binary settings channel | One request per TCP connection, big-endian length-prefixed frames |
81
+
82
+ ### The 9998 prelude
83
+
84
+ On connect the unit immediately sends a text prelude and, if the client does
85
+ not continue with the interactive greeting the vendor app uses, may close the
86
+ connection partway through. The library reads until the literal
87
+ `END PRELUDE:` marker, then drains for a further 0.3 s, and returns whatever
88
+ arrived (decoded as UTF-8 with replacement). The prelude is a sequence of
89
+ sections introduced by an upper-case header ending in a colon, each holding
90
+ `key: value` lines. The parts the library uses:
91
+
92
+ - `Label: <name>`: the unit's user-assigned name. Sanitised to
93
+ `[A-Za-z0-9._-]` for use as the archive filename root.
94
+ - `Software Release: <x.y>`: the firmware version. Not used by the library
95
+ itself, but this is the field to read if you want it.
96
+ - `CONTROL:` and `CONTROL DEFAULT:`: the live and default control values as
97
+ `Name: raw` lines, raw integers mostly in `0..10000`. Only used for the
98
+ annotated text dump.
99
+ - `FILE LIST:`: one saved preset slot name per line. This is the list of
100
+ slots the archive will read.
101
+
102
+ Section extraction is a regular expression anchored on the header and
103
+ terminated by the next all-caps header or `END PRELUDE:`; two pitfalls
104
+ already hit and fixed are recorded in `_preamble.py` (a `$` anchor that
105
+ truncated multi-line FILE LISTs, and a `\s*` that let an empty section
106
+ swallow the next one).
107
+
108
+ ### The 9996 binary channel
109
+
110
+ Every request opens its own TCP connection; the unit closes it after
111
+ replying.
112
+
113
+ Read (`binary_request`):
114
+
115
+ ```
116
+ send [opcode: u16 BE][len: u32 BE][payload: len bytes]
117
+ recv [len: u32 BE][body: len bytes]
118
+ ```
119
+
120
+ Write (`upload_one`):
121
+
122
+ ```
123
+ send [opcode: u16 BE][name_len: u32 BE][name][data_len: u32 BE][data][0x00]
124
+ recv 6 bytes of 0x00 (success ACK)
125
+ ```
126
+
127
+ Known opcodes:
128
+
129
+ | Opcode | Direction | Payload / name | Meaning |
130
+ |---|---|---|---|
131
+ | `0x0000` | read | preset slot name from FILE LIST | read a saved preset blob |
132
+ | `0x0003` | read | `GPISettings` or `SavedSettings` | read a named resource |
133
+ | `0x0100` | write | `.../presets/<slot>` | write a preset slot (mirror of `0x0000`) |
134
+ | `0x0103` | write | `.../GPISettings` or `.../SavedSettings` | write a resource (mirror of `0x0003`) |
135
+
136
+ Details that matter and were established by experiment:
137
+
138
+ - The trailing `0x00` on a write frame is mandatory. Without it the unit
139
+ silently declines to ACK.
140
+ - The vendor app puts the full filesystem path of the extracted zip member
141
+ into the name field (a macOS temporary directory under
142
+ `Ultimatte Software Control-PYREST/...`). The unit appears to inspect only
143
+ the last path segment, and for slot writes the presence of a `presets/`
144
+ segment. The library sends the same path shape for parity.
145
+ - The ACK is six zero bytes, but the unit closes the socket so quickly after
146
+ sending it that a client can see EOF after fewer than six. The library
147
+ accepts any all-zero prefix as success; any non-zero byte is treated as a
148
+ rejection and raised as `IOError`.
149
+ - A short read of the 4-byte length header, including an immediate clean
150
+ EOF, and a short body read both raise `IOError`. An empty resource whose
151
+ header says length 0 returns `b""` cleanly.
152
+ - Blob contents are opaque. The library never parses preset or resource
153
+ bytes; it moves them verbatim.
154
+
155
+ ### Archive layout
156
+
157
+ The zip must match the vendor app byte for byte in structure, or its Restore
158
+ rejects it. Members in this order, with explicit directory entries carrying
159
+ `0o40755` external attributes:
160
+
161
+ ```
162
+ images/ (empty directory entry)
163
+ GPISettings (raw bytes, no length prefix)
164
+ SavedSettings (raw bytes, no length prefix)
165
+ quickfiles/ (empty directory entry)
166
+ presets/ (explicit directory entry; required)
167
+ presets/<slot> (one per FILE LIST entry)
168
+ <label>_config_readable.txt (this library's addition; ignored by the vendor app)
169
+ ```
170
+
171
+ ### Restore order
172
+
173
+ 1. Every `presets/<slot>` member, sorted by name, with opcode `0x0100`.
174
+ 2. `GPISettings` with opcode `0x0103`, if present.
175
+ 3. `SavedSettings` with opcode `0x0103`, if present. It overwrites the live
176
+ state, so it goes last.
177
+
178
+ The first failed write aborts the run; `restore_unit_from_bytes` returns
179
+ `(False, report)` with `report["stopped_at"]` naming the member. Members
180
+ that are not presets or the two resources are ignored, which is how the
181
+ annotated text dump rides along harmlessly.
182
+
183
+ ### Timeouts and retries
184
+
185
+ Connect timeout 5 s, read timeout 30 s on writes, 2 s quiet timeout while
186
+ reading the prelude. Every connect is retried once after 250 ms because a
187
+ cold unit regularly refuses or times out the very first packet and answers
188
+ the second.
189
+
190
+ ### Verified against
191
+
192
+ - Ultimatte 12 4K, the unit the captures were taken from and the round-trip
193
+ tests were run on. The docstrings also name the Ultimatte 12 (HD) as a
194
+ target of the same protocol.
195
+ - The firmware version of the bench unit was not recorded at capture time.
196
+ Read it from the prelude's `Software Release` line on your own unit and
197
+ treat any difference from the behaviour above as something to re-verify.
198
+
199
+ ### Unverified or guessed
200
+
201
+ - Whether the unit reads anything in the write name field beyond the last
202
+ segment and the `presets/` anchor. The full vendor path is sent to be safe.
203
+ - Whether non-zero ACK bytes carry an error code. They are treated as a
204
+ generic rejection.
205
+ - `images/` and `quickfiles/` are always written as empty directories. A unit
206
+ that actually holds images or quick files would not have them archived by
207
+ this library, and no opcode for them is known.
208
+ - Other opcodes almost certainly exist (the gaps between `0x0000` and
209
+ `0x0003`, and between `0x0100` and `0x0103`, are suggestive). None have been
210
+ probed.
211
+ - The interactive greeting the vendor app sends on 9998 after the prelude is
212
+ not implemented; the library only ever reads the prelude.
213
+ - `read_preamble` is bounded per `recv` but has no overall deadline or size
214
+ cap; a unit that never stops sending would keep it reading.
215
+ - Ultimatte 12 HD Mini and other models were not tested.
216
+ - The display-range table is an interpretation of the manual and the panel
217
+ UI, not wire data. It affects only the readable text dump.
218
+
219
+ ## License
220
+
221
+ MIT. See `LICENSE`.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ultimattewire"
7
+ version = "0.1.0.dev0"
8
+ description = "Blackmagic Ultimatte 12 archive and restore over its native TCP protocol (Smart Remote 4 compatible zips)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Lucas Romanenko" }]
13
+ dependencies = []
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ test = ["pytest"]
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["ultimattewire", "ultimattewire.*"]
25
+ exclude = ["ultimattewire.tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ ultimattewire — Blackmagic Ultimatte 12 / 12 4K control library.
4
+
5
+ Protocol layer reverse-engineered from packet captures against real
6
+ hardware; opcodes, frame format, byte order, and sequence must match
7
+ the unit's parser exactly. See ultimattewire.profile for the Smart
8
+ Remote-compatible Archive/Restore feature (the first public
9
+ implementation of it outside BMD's own tools).
10
+ """
11
+
12
+ from ultimattewire.profile import archive_unit_to_bytes, restore_unit_from_bytes
13
+
14
+ __all__ = [
15
+ "archive_unit_to_bytes",
16
+ "restore_unit_from_bytes",
17
+ ]
@@ -0,0 +1,76 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ Ultimatte 9998 text channel: connect, read the prelude, and parse named
4
+ sections out of it. Used by profile/archive (to build the zip) and by
5
+ any future feature that needs the unit's current state (label, video
6
+ mode, FILE LIST, etc.).
7
+ """
8
+
9
+ import re
10
+ import socket
11
+
12
+ from ultimattewire._protocol import CONNECT_TIMEOUT, CONTROL_PORT, _connect_with_retry
13
+
14
+
15
+ PREAMBLE_QUIET_TIMEOUT = 2.0
16
+
17
+
18
+ def read_preamble(host, connect_timeout=CONNECT_TIMEOUT,
19
+ quiet_timeout=PREAMBLE_QUIET_TIMEOUT):
20
+ """Connect to 9998 and read until 'END PRELUDE:'."""
21
+ s = _connect_with_retry(host, CONTROL_PORT, timeout=connect_timeout)
22
+ s.settimeout(quiet_timeout)
23
+ buf = bytearray()
24
+ try:
25
+ while True:
26
+ try:
27
+ chunk = s.recv(65536)
28
+ except socket.timeout:
29
+ break
30
+ if not chunk:
31
+ break
32
+ buf.extend(chunk)
33
+ if b"END PRELUDE:" in buf:
34
+ s.settimeout(0.3)
35
+ try:
36
+ while True:
37
+ more = s.recv(65536)
38
+ if not more:
39
+ break
40
+ buf.extend(more)
41
+ except socket.timeout:
42
+ pass
43
+ break
44
+ finally:
45
+ s.close()
46
+ return buf.decode("utf-8", errors="replace")
47
+
48
+
49
+ def parse_section(text, header):
50
+ """Extract the body of a named CAPS-header section from a prelude."""
51
+ # NOTE: use \Z (end-of-string only), not $ — with re.MULTILINE, $ matches
52
+ # at the end of every line, which would terminate the lazy capture after
53
+ # the first line of the section. This bug caused multi-line FILE LIST
54
+ # sections (e.g. with multiple presets) to be truncated to just the first
55
+ # entry.
56
+ # Also: use [ \t]* (not \s*) after the header colon — \s* matches newlines
57
+ # too, which would cause an empty section to swallow the next section.
58
+ pattern = rf"^{re.escape(header)}:[ \t]*\n(.*?)(?=\n[A-Z][A-Z0-9 ]*:\s*\n|\nEND PRELUDE:|\Z)"
59
+ m = re.search(pattern, text, re.MULTILINE | re.DOTALL)
60
+ return m.group(1).strip() if m else ""
61
+
62
+
63
+ def parse_file_list(text):
64
+ """Return the list of preset slot names from the FILE LIST section."""
65
+ section = parse_section(text, "FILE LIST")
66
+ if not section:
67
+ return []
68
+ return [ln.strip() for ln in section.splitlines() if ln.strip()]
69
+
70
+
71
+ def extract_label(text):
72
+ """Return the unit's Label field, sanitized for use as a filename root."""
73
+ m = re.search(r"^Label:\s*(.+)$", text, re.MULTILINE)
74
+ if m:
75
+ return re.sub(r"[^A-Za-z0-9._-]+", "_", m.group(1).strip())
76
+ return "unknown"