speedEditor

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

Speed Editor Client

The aim of this project is to provide a fully featured, easy to use library for building open source software for the Davinci Resolve Speed Editor.

There are a few existing solutions in Python, but these are not really designed for consumption as a library and any projects eg keymap editors would require the user to install Python, dependencies etc. I wanted a more polished solution in a compiled language, that anyone can use to expand the functionality of this hardware.

Examples

In the examples folder, there are a few projects to get you started:

  • volume wheel uses the Jog wheel as a volume controller for Windows, Mac and Linux
  • lightshow flashes all the LEDs in each column, then all the LEDs in each row, alternating
  • keypress sets a custom keypress handler, which illuminates the LEDs of the last pressed key
  • reset switches off the LEDs

Documentation

View docs on pkg.go.dev:

Usage

To import as a dependency:

go get github.com/JamesBalazs/speed-editor-client

The project depends on the go-hid library.

Before creating a Speed Editor client, we need to initialize the HID library:

if err := hid.Init(); err != nil {
	log.Fatal(err)
}
defer hid.Exit()

Don't forget to defer a call to Exit to avoid memory leaks.

Next we can initialize the client:

client, err := speedEditor.NewClient()
if err != nil {
	log.Fatal(err)
}

This connects to the Speed Editor, requests the manufacturer info, and device info such as serial number, and sets up the default event handlers.

Device info is cached on initialize, since it will never change once the device is connected:=

deviceInfo := client.GetDeviceInfo()

fmt.Printf("Manufacturer: %s\nProduct: %s\nSerial: %s\n", deviceInfo.MfrStr, deviceInfo.ProductStr, deviceInfo.SerialNbr)

Which will output something like:

Product: DaVinci Resolve Speed Editor

Serial: 1234567890ABCDEFGHIJKLMNOPQRSTUV

The Speed Editor won't work without the correct auth handshake. Luckily for us Sylvain Munaut reverse engineered and implemented the handshake here all the way back in 2021, and published the code under an Apache 2.0 License for the benefit of others.

I re-implemented his authentication algorithm in Go, and exported the underlying functions for consumers of the library to use as they see fit.

When using the client, you just need to call Authenticate before sending / receiving any messages, and the handshake will be handled for you:

if err := client.Authenticate(); err != nil {
	log.Fatal(err)
}

Finally, to receive messages from the Speed Editor, you can call Poll. This will start a loop which does a blocking read, waiting for either a keypress, battery report, or jog wheel movement from the device:

client.Poll()

When any of the aforementioned events happen, the corresponding Handler function is called.

The event handlers can be overridden by the user to implement custom functionality:

func customJogHandler(client speedEditor.SpeedEditorInt, report input.JogReport) {
  fmt.Printf("Jog wheel position: %d\n", report.Value)
}

client.SetJogHandler(customJogHandler)

func customKeyPressHandler(client speedEditor.SpeedEditorInt, report input.KeyPressReport) {
  for _, key := range report.Keys {
    fmt.Printf("Keys pressed: %s", key.Name)
  }
}

client.SetKeyPressHandler(customKeyPressHandler)

func customBatteryHandler(client speedEditor.SpeedEditorInt, report input.BatteryReport) {
  fmt.Printf("Battery level: %d", report.Battery)
}

The library also provides a complete list of keys for the device, their IDs, the IDs of their LEDs (if present for a key), their labels and their positions on the board.

This helps light LEDs based on their position such as in the lightshow and volume wheel examples.

You can light any combination of LEDs on the board:

keysByName := keys.ByName()

leds := []uint32{keysByName[keys.CAM7.Led], keysByName[keys.CAM5.Led], keysByName[keys.CAM3.Led]}
jogLeds := []uint8{keysByName[keys.SHTL.JogLed], keysByName[keys.JOG.JogLed], keysByName[keys.SCROLL.JogLed]}

if err := client.SetLeds(leds); err != nil {
	log.Fatal(err)
}
if err := client.SetJogLeds(jogLeds); err != nil {
	log.Fatal(err)
}

JOG/SCRL/SHTL are on a different system to the other LEDs, so a different function is required to light them.

Finally, there are a few different jog modes available on the device:

  • RELATIVE
    • Reports relative position, since last report
  • ABSOLUTE
    • Reports absolute position, where 0 is the position when the mode was set. -4096 -> 4096 = 180deg
  • RELATIVE_2
    • Same as RELATIVE, but I think Davinci Resolve uses this to enable faster scrolling when jog is double pressed in later versions (according to one obscure forum post). You could replicate this in software by applying some multiplier to the relative position received from the device (or use it for any feature you like)
  • ABSOLUTE_DEADZONE
    • Same as ABSOLUTE but with a deadzone around 0, so less sensitive to accidental knocks / easier to reset to 0

You can switch modes via the client:

if err := client.SetJogMode(jogModes.ABSOLUTE); err != nil {
	log.Fatal(err)
}

You will have to handle lighting the buttons yourself, if you want the modes to work like they do with the editor connected to Davinci.

You can also get a list of keys and their attributes, with deterministic ordering:

keys.Get()

And maps / indexes of keys by name, ID, LED ID etc so you can easily retrieve key details given only a single attribute, in constant time:

keys.ById()
keys.ByName()
keys.ByLedId()
keys.ByJogLedId()
keys.ByText()
keys.BySubText()
keys.ByCol()
keys.ByRow()

The same goes for jog modes:

jogModes.Get()

jogModes.ById()
jogModes.ByName()

All of the above getters do a "copy on read" so it's a good idea to grab them once, store in a variable and refer to the variable rather than grabbing another copy.

This is done since manipulating the maps could mess up the underlying key data in the library, since maps store references to the underlying data. By copying on read, you get a different copy of the data in the map at the expense of a small performance hit.

Dev notes

My setup is weird (WSL remote via Zed) so some extra steps are required to pass the Speed Editor through to WSL

Installing usbipd:

winget install usbipd

Listing devices:

usbipd list

Binding the Speed Editor (persists reboot, your BUSID will be different to mine):

sudo usbipd bind --busid=4-9

Attaching to WSL (does not persist reboot):

sudo usbipd attach --wsl --busid=4-9

To confirm w/ lshid within WSL:

$HOME/go/bin/lshid

Should output something like /dev/hidraw0: ID 1edb:da0e Blackmagic Design DaVinci Resolve Speed Editor

Deps
sudo dnf install systemd-devel

To get libudev.h on Fedora (required for lshid)

I then had permission issues reading from /dev/hidraw0 so had to create a udev rule:

KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0660", GROUP="plugdev"

in /etc/udev/rules.d/99-hidraw-permissions.rules, then:

sudo groupadd plugdev
sudo usermod -a -G plugdev james
sudo udevadm control --reload
sudo udevadm trigger

After this stat /dev/hidraw0 should list the new plugdev group.

Cross platform builds for Windows

mingw-w64 is required to compile the HID library on Linux for Windows with CGO. Installation:

sudo dnf install mingw64-gcc

To build the examples:

GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc go build main.go

Disclaimer

I am not affiliated with Blackmagic Design.

Blackmagic Design, Davinci Resolve, and the Speed Editor are all registered trademarks of Blackmagic Design.

I did not reverse engineer or write the original handshake algorithm.

The EU Software Directive (2009/24/EC) explicitly permits reverse engineering for interoperability. In this case the handshake algorithm is being used purely for interoperability between the Speed Editor and other software.

Thanks

Thanks to smunaut for reverse engineering the Speed Editor authentication algorithm and publishing it.

This is by far the hard part of getting the device working with software other than Davinci Resolve, which I had dreamed of doing but would have never realistically had time (nor probably the skills) to do.

Without their work this library would not have been possible.

Documentation

Index

Constants

View Source
const (
	VID = 0x1edb
	PID = 0xda0e

	LedReportId     = 2
	JogModeReportId = 3
	JogLedReportId  = 4
)

Variables

This section is empty.

Functions

func NullBatteryHandler

func NullBatteryHandler(client SpeedEditorInt, report input.BatteryReport)

func NullJogHandler

func NullJogHandler(client SpeedEditorInt, report input.JogReport)

func NullKeyPressHandler

func NullKeyPressHandler(client SpeedEditorInt, report input.KeyPressReport)

Types

type AuthHandler

type AuthHandler struct {
	// contains filtered or unexported fields
}

func (AuthHandler) Authenticate

func (ah AuthHandler) Authenticate() (time.Duration, error)

Authenticate handles the entire handshake between the host and the Speed Editor.

It returns the duration before the Speed Editor expects a reauth.

func (AuthHandler) GetAuthChallengeResult

func (ah AuthHandler) GetAuthChallengeResult() (uint16, error)

func (AuthHandler) GetHostChallengeResponse

func (ah AuthHandler) GetHostChallengeResponse() ([]byte, error)

func (AuthHandler) GetKeyboardChallenge

func (ah AuthHandler) GetKeyboardChallenge() (uint64, error)

func (AuthHandler) ResetAuthState

func (ah AuthHandler) ResetAuthState() error

func (AuthHandler) SendAuthChallengeResponse

func (ah AuthHandler) SendAuthChallengeResponse(response uint64) error

func (AuthHandler) SendHostChallenge

func (ah AuthHandler) SendHostChallenge() error

sendHostChallenge requests a challenge response from the device. Presumably this step exists to confirm it's a real Speed Editor.

type AuthHandlerInt

type AuthHandlerInt interface {
	Authenticate() (time.Duration, error)
	ResetAuthState() error
	GetKeyboardChallenge() (uint64, error)
	SendHostChallenge() error
	GetHostChallengeResponse() ([]byte, error)
	SendAuthChallengeResponse(response uint64) error
	GetAuthChallengeResult() (uint16, error)
}

type SpeedEditor

type SpeedEditor struct {
	AuthHandler AuthHandlerInt
	// contains filtered or unexported fields
}

func (SpeedEditor) Authenticate

func (se SpeedEditor) Authenticate() error

func (SpeedEditor) GetDeviceInfo

func (se SpeedEditor) GetDeviceInfo() hid.DeviceInfo

func (SpeedEditor) HandleBattery

func (se SpeedEditor) HandleBattery(report input.BatteryReport)

func (SpeedEditor) HandleJog

func (se SpeedEditor) HandleJog(report input.JogReport)

func (SpeedEditor) HandleKeyPress

func (se SpeedEditor) HandleKeyPress(report input.KeyPressReport)

func (SpeedEditor) HandleReport

func (se SpeedEditor) HandleReport(genericReport any)

func (SpeedEditor) Poll

func (se SpeedEditor) Poll()

func (SpeedEditor) Read

func (se SpeedEditor) Read() ([]byte, int, error)

func (*SpeedEditor) SetBatteryHandler

func (se *SpeedEditor) SetBatteryHandler(handler func(SpeedEditorInt, input.BatteryReport))

func (*SpeedEditor) SetJogHandler

func (se *SpeedEditor) SetJogHandler(handler func(SpeedEditorInt, input.JogReport))

func (SpeedEditor) SetJogLeds

func (se SpeedEditor) SetJogLeds(leds []uint8) error

func (SpeedEditor) SetJogMode

func (se SpeedEditor) SetJogMode(mode uint8) error

func (*SpeedEditor) SetKeyPressHandler

func (se *SpeedEditor) SetKeyPressHandler(handler func(SpeedEditorInt, input.KeyPressReport))

func (SpeedEditor) SetLeds

func (se SpeedEditor) SetLeds(leds []uint32) error

type SpeedEditorInt

type SpeedEditorInt interface {
	// Authenticate does the initial handshake with the Speed Editor,
	// and re-auths periodically in the background when requested by the device.
	Authenticate() error

	// GetDeviceInfo returns the serial number, manufacturer string etc published
	// by the device via HID. This info is cached on init, so we don't have to
	// request it on every call.
	GetDeviceInfo() hid.DeviceInfo

	// Read pulls a single input report from the Speed Editor, and returns the
	// data (list of keys currently held down, jog wheel position etc.) and
	// the data length.
	//
	// The first byte indicates which report type was received.
	Read() ([]byte, int, error)

	// Poll starts a Read loop, parses each input report and calls Handle on each
	// via the Report interface.
	Poll()

	// SetLeds accepts the bitmask for a list of LEDs, and binary ORs the bitmask
	// to enable all LEDs in the mask.
	//
	// SetLeds does not keep any state, so it will reset any previously enabled
	// LEDs if they aren't included in the next call.
	SetLeds(leds []uint32) error

	// SetJogMode switches between the 4 jog modes:
	// RELATIVE - Relative position
	// ABSOLUTE - Absolute position from -4096 to 4096
	// RELATIVE2 - Relative position, I think this is used to enable a faster scroll mode when the SCRL button is pressed twice in Resolve: https://www.reddit.com/r/blackmagicdesign/comments/1dv56d4/speed_editor_firmware_update_dial_speed_change/
	// ABSOLUTE_0 - Absolute position from -4096 to 4096 with deadzone around 0
	SetJogMode(mode uint8) error

	// SetJogLeds accepts the bitmask for a list of LEDs, and binary ORs the bitmask
	// to enable all LEDs in the mask. Jog LEDs are on a separate system, and overlap
	// with some of the normal LED IDs so we need a separate message to enable them.
	//
	// SetJogLeds does not keep any state, so it will reset any previously enabled
	// LEDs if they aren't included in the next call.
	SetJogLeds(leds []uint8) error

	// SetJogHandler allows replacing the handler function that will be called on Poll()
	// when a JogReport is received.
	SetJogHandler(handler func(SpeedEditorInt, input.JogReport))
	// SetBatteryHandler allows replacing the handler function that will be called on Poll()
	// when a BatteryReport is received.
	SetBatteryHandler(handler func(SpeedEditorInt, input.BatteryReport))
	// SetKeyPressHandler allows replacing the handler function that will be called on Poll()
	// when a KeyPressReport is received.
	SetKeyPressHandler(handler func(SpeedEditorInt, input.KeyPressReport))
}

func NewClient

func NewClient() (SpeedEditorInt, error)

NewClient connects to a Speed Editor via the HID library and returns a SpeedEditorInt to interact with the device.

It is recommended to manually initialise the HID library before creating the Speed Editor client, with `hid.Init()`.

Ensure to use `defer hid.Exit()` to avoid memory leaks.

Directories

Path Synopsis
examples
keypress command
lightshow command
reset command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL