Repository navigation
1.4.0: transport recovery, HyperDeck answers per command, macro and profile fixes
Fixes for issues #8 to #21, most of them found while porting bmdwire to Go (bmdctl). atemwire and hyperdeckwire change; ultimattewire and videohubwire are unchanged. Nothing is removed. Four of the issues stay open, for a check on hardware or a packet trace; they are listed at the end.
Behaviour changes
- atemwire: the silence timeout now fires for a client that keeps sending (#11). The transport re-handshakes after 5 s without hearing from the switcher. Until now, the client's own outgoing packets, and datagrams it then dropped, restarted those 5 s. So a client that sent at least once every 5 s to a switcher that had gone away never timed out: no
disconnectedevent,is_readystayed True over stale state, and commands went nowhere. Now only a packet accepted from the switcher, or a new handshake, restarts the clock. A switcher that stops answering a control surface, a script or a profile apply is noticed within 5 to 6 s, so expectdisconnected(in WebATEM, "connection lost") where there was none before. - hyperdeckwire: a timeout ends the connection (#18). When an answer doesn't come within
read_timeout,request()closes the connection before raisingTimeoutError, and so does every method, since they all go through it. Later calls raiseOSError('not connected')until you callconnect()again. Before, the connection stayed open: the late answer was returned to the next command, and every answer after it was one behind. Code that catches the timeout and carries on with the sameHyperdeckmust now reconnect.
Changes, by issue
atemwire: connection and transport
#11: The silence timeout counts only the switcher (#25). See Behaviour changes. last_rx_monotonic keeps its meaning: the time of the last packet from the switcher.
#12: A re-handshake ignores the session it replaces (#26). After a silence, the switcher goes on resending the old session for a few seconds. Those packets restarted the silence window. The first of them could also become the new session's sequence baseline, so the new state dump was dropped as duplicates and connected didn't come until the next timeout. And one could be adopted as the session id. Now packets carrying the replaced session's id are dropped before any receive state sees them, and so is anything but the handshake reply while the handshake is out.
#13: A state dump without a video mode re-handshakes at once (#27). If the first packet of a state dump is lost, it is lost for good. When that packet carried the video mode, ATEMConnection.connect waited out its 15 s and failed. Now the protocol raises disconnected and re-handshakes for a fresh dump as soon as a dump ends (InCm) without a video mode, up to 3 times in a row, then carries on as before. This is the fail-fast half of the issue; the real fix is still open (below).
#14: *XFC on a UDP session is ignored (#28). This OpenSwitcher TCP-relay event is never sent by a switcher. If one arrived anyway, its handler either raised KeyError, losing the rest of the packet, or popped a real transfer in flight. On UDP it is now dropped before parsing.
#15: A lock taken is stored and raised (#29). LKST was stored only for a release, so mixerstate['lock-state'] never showed the media store held. Both states are now stored and raise change:lock-state. The transfer engine still reacts to releases only. Another client holds the store when state is True and protocol.locks.get(store) is falsy. The key is still bare: the latest LKST wins, whatever its store.
#8: Transfers are refused while the session is re-established (#31). download_still, download_macro and clear_still now check is_ready, and raise ConnectionDeadError at once during a transport re-handshake. Before, they queued into the session being replaced and waited out 30 s, 10 s or 5 s, and a still's timeout blamed "another client" for the lock. profile.save.download_media_pool_images refuses up front, and stops when the session goes, returning the slots done so far. send() still checks is_connected.
atemwire: macros and profiles
#16: A Fairlight strip macro op with no sourceId encodes the stereo strip (#30). The default was 0xFFFFFFFFFFFF0000, an address no switcher uses. It is now 0xFFFFFFFFFFFF0100, which ATEM Software Control (ASC) writes as 18446744073709486336.
#17: Macro floats decode to six significant digits, as ASC writes them (#24). A silenced fader now decodes to -327.68, not -327.679993. Across every macro op in twelve ASC exports, all 399 distinct values now match ASC's strings; 349 did before.
- Every float the decoder writes changes form.
- Above 10, six significant digits are coarser than the switcher's fixed point, so a value decoded and encoded again can move by a few 1/65536 steps, as it does when you save and restore through ASC.
#9: Profiles save and apply dual-mono Fairlight inputs per half (#23). Apply used to send both halves of a split-mono input to the stereo strip, the second overwriting the first. Save wrote them as one stereo input.
- Save now writes
DualMono, with ASC's id for each half (−256 left, −255 right), and apply sends each half to its own channel. - The XLR input's strip (id −256) is now addressed as channel 0, as ASC's export names it, rather than −1.
- Older saves of a dual-mono input apply as before.
- Apply doesn't switch an input to Dual Mono; it must already be in Dual Mono.
#10: Fly-key locations are one enum, with MiddleCentre at 5 (#22). A capture of ASC settled it: RFlK byte 5, KeFS byte 5 and macro op 0x0056 share one table, atemwire.helpers.FLYKEY_LOCATION_NAMES (0 CentreOfKey, then 1 to 9 for the 3×3 grid, row by row). The macro codec now encodes MiddleCentre, and decodes byte 5 to it rather than '5'. run_flying_key_infinite_direction takes ASC's location names as well as the number.
hyperdeckwire
#18: A timeout ends the connection (#32). See Behaviour changes.
#19: A response nobody asked for is dropped (#32). Responses that arrive before a command is sent are no longer returned as its answer; they are logged and dropped. One that arrives after the command is sent still can't be told from the real answer, since the protocol has no request ids.
#20: A command with a line break in it is refused (#32). A clip name or record prefix containing CR or LF used to be sent as two commands: clips_add('a.mov\nclips clear') cleared the timeline. Every method, and request(), now raises ValueError before anything is sent.
#21: connect() fails when the deck refuses the connection (#32). A deck at its client limit answers 120 connection failed and closes. connect() used to succeed anyway, with an empty model. Any 1xx greeting now raises HyperdeckError, and any greeting other than 500 raises OSError.
atemwire/docs/SURFACE.md §5.4 records each atemwire fix with its commit, and hyperdeckwire/docs/API.md has a new section, "One answer per command".
Still open
- #8, #11, #12: fixed here; each closes after a check on a bench switcher (a cable pull).
- #13: the fail-fast guard is in this release. The real fix is to know a new session's first sequence number, so that a lost first packet is resent like any other. It needs a packet trace of a few connects first.
Wheels for Linux x86_64 and aarch64, macOS arm64 and x86_64, Windows x86 and x86_64 on CPython 3.10 to 3.14, plus the sdist.