So thanks to some excellent troubleshooting from DeSwaggy on the Discord HA forum I can definitely say that the issue is hardware.
The Broadlink RM4 has a hardware limited packet size “we know the RM4’s ceiling is somewhere between 734 and 1462 bytes” and when sending:
“That GO STOP GO is likey it remote.send_command defaults delay_secs to 0.4 seconds between repeats. Your receiver sees 400ms of silence, assumes you released the button, stops the motor. Next repeat starts it again. Your physical remote transmits continuously with no gap, that’s the whole difference.”
“Broadlink packets carry their own repeat count at byte 1. Repeats done inside the packet run on the device itself, back to back, with no trip over the network and no delay_secs between them. Much tighter than num_repeats. python-broadlink · DocSearch MCP”
" Grab your down code from .storage/broadlink_remote_<MAC>_codes,"
“a learned RF code is a fixed-length recording of pulses.”
"There’s no “keep transmitting until I say stop” in the protocol at all, "
However overall it’s the hardware limitation of the packet size. The transmitting packet itself has gaps in it so it will always stutter with the GO STOP GO.:
"So the Broadlink is transmitting near-continuously, the problem is two oversized gaps baked into the recording:
85.7ms near the start
45.7ms at the end
That’s ~130ms of dead air every loop, against a normal inter-frame gap of 9.35ms. Your receiver almost certainly treats that as “button released”."
Even editing the packet itself made no difference but made the movement smoother.
“It’s rebuilt it with both gaps cut to the normal 9.35ms. If it runs smooth for roughly half a second, that’s the cause confirmed, and from there you stack sends for travel, or we build you a longer packet with more frames in one transmission.
And If it still chops, the gap is the Broadlink keying its transmitter between packets, which no amount of editing fixes. ESP32 then.”
" Hold the button and its chip transmits a frame, 9.35ms gap, another frame, gap, another, and it keeps going for as long as the contact is closed. Release and it stops mid-stream. There’s no fixed length because there’s no recording - the button is the duration. The Broadlink can’t do that. It only plays back what it captured, and your capture is a 675ms snapshot of that stream. That’s why hold_secs did nothing, you can’t hold a recording.
The sync gap matters because of how the receiver reads it. Long gap, decide “new transmission”, then decode frames. Shorten that gap and it never recognises a start.
Required part of the pattern, not padding… hence my trimmed version doing nothing."
The end result is it did not work even by modifying the packet.
“More frames should mean longer activation, not invalid. So the failure is more likely transmitter-side: your original payload is 734 bytes, the doubled one 1462, and the RM4 probably won’t accept a packet that size. It never went out at all.”
So the best thing to do is just wire an ESP32 to another remote to get a smooth action without stutters or gaps.