Denon Reverse Engineering Help

Engine gRPC Reference

A reconstruction of the gRPC and Protocol Buffers surface of /usr/Engine/Engine, intended for anyone writing software that talks to Engine OS hardware.

Everything here was recovered from the SC6000 5.0.4 binary using the method in Engine Ghidra Analysis. Transport behavior was then verified against two live SC6000 units. This is not an official schema, and no vendor documentation was consulted.

Transport

Property

Value

Port

TCP 50010, hardcoded (movw #0xc35a in Engine)

Discovery

UDP 11224 beacon, 6 bytes: EAAS + 0x01 0x00, broadcast ~2.5 s per device

Bind address

Every address returned by QNetworkInterface::addressEntries() that passes an interface filter. Confirmed reachable on the Wi-Fi address

Protocol

gRPC over cleartext HTTP/2, no TLS, no ALPN negotiation

Server preface

Sends SETTINGS immediately on connect (MAX_CONCURRENT_STREAMS=0x7fffffff, INITIAL_WINDOW_SIZE=4 MiB)

Content type

application/grpc

Reflection

Not available: grpc.reflection.v1alpha.ServerReflection returns UNIMPLEMENTED

Compression

Not observed; send the message with the compression flag clear

Required metadata

uuid: <host uuid> on most methods (see below)

Since there is no TLS and no reflection, a client is a plain HTTP/2 connection with content-type: application/grpc and standard 5-byte-prefixed messages. No credentials plugin, no channel certificates.

Which services are actually served on 50010

Only one. This trips people up, because the binary contains client stubs and server bases for several packages:

Service

On port 50010

enginesync.v1.EngineSyncService

✅ served

enginelibrary.v1.EngineLibraryService

UNIMPLEMENTED

networktrust.v1.NetworkTrustService

UNIMPLEMENTED

grpc.reflection.v1alpha.ServerReflection

UNIMPLEMENTED

UNIMPLEMENTED is what gRPC returns for a service that was never registered, so the library and pairing services are either exposed elsewhere, exposed only under some condition, or used purely as clients on this device. Where they live is not yet known. See the open questions below.

The uuid metadata header

Two of the three enginesync methods require a metadata header whose key is literally uuid and whose value is the caller's host UUID. Omitting it produces:

grpc-status : 9 (FAILED_PRECONDITION) grpc-message: The client did not pass the uuid in the metadata.

A second server-side check exists whose message is in .rodata:

The requesting host has not been discovered on the server side yet. Try again later.

Both stages were confirmed live, and they are distinguishable by the error you get:

What you send

grpc-status

grpc-message

no uuid header

9 FAILED_PRECONDITION

The client did not pass the uuid in the metadata.

a uuid the device has not discovered

9 FAILED_PRECONDITION

The requesting host has not been discovered on the server side yet. Try again later.

So the gate is: present a uuid, and that UUID must already be in the device's discovered-host table (maintained by SyncServiceDiscoverer, whose onHostAdded/onHostRemoved(uuid) slots appear in the Qt metadata). The device's own UUID is user-visible as the /Network/NetworkId state property.

Service definitions

enginesync.v1.EngineSyncService: beat/tempo sync

Method

Kind

Request

Response

SetSyncState

unary

SyncState

google.protobuf.Empty

ExchangeTimestamps

bidi streaming

TimestampSync

TimestampSync

UpdateSyncLeadAudioInfo

client streaming

SyncLeadAudioInfo

google.protobuf.Empty

// Reconstructed from /usr/Engine/Engine (Engine OS 5.0.4, SC6000). // NOT an official schema. Field numbers and wire types are recovered from the compiled // _InternalSerialize functions and are reliable; field NAMES exist only for `string` // fields (protobuf emits a UTF-8 verification literal for those and nothing else), and // scalar types are narrowed only as far as the wire type allows. // // Wire-type -> possible declared types: // varint : int32 int64 uint32 uint64 sint32 sint64 bool enum // fixed64 : double fixed64 sfixed64 // fixed32 : float fixed32 sfixed32 // // Verified live against SC6000 units on 5.0.4: this service is served on TCP 50010 // over cleartext HTTP/2. syntax = "proto3"; package enginesync.v1; import "google/protobuf/empty.proto"; service EngineSyncService { // Unary. NOTE: on 5.0.4 this method does NOT require the `uuid` metadata header that // the other two enforce. See the Security page. rpc SetSyncState (SyncState) returns (google.protobuf.Empty); // Bidirectional streaming. Requires metadata `uuid: <host uuid>`. rpc ExchangeTimestamps (stream TimestampSync) returns (stream TimestampSync); // Client streaming. Requires metadata `uuid: <host uuid>`. rpc UpdateSyncLeadAudioInfo (stream SyncLeadAudioInfo) returns (google.protobuf.Empty); } message DeckID { // field 1 is written before hostUuid via an outlined varint writer; the field number // is not visible as an immediate, so 1 is inferred from ordering. Semantically this // is almost certainly the deck number (compare DeckID.hostUuid + deck index usage in // the QML channel-assignment strings). uint32 deck_number_UNCONFIRMED = 1; // varint, name unknown string hostUuid = 2; // NOTE: camelCase, unlike the other packages } message DeckState { DeckID deck_id_UNCONFIRMED = 1; // message uint32 field2_UNKNOWN = 2; // varint uint32 field3_UNKNOWN = 3; // varint uint32 field4_UNKNOWN = 4; // varint // field 5 is a nested message; type not determined bytes field5_UNKNOWN = 5; // message } // Request type of SetSyncState. An empty SyncState is accepted by the device and // returns an empty response, which is why an empty-body call succeeds. message SyncState { repeated DeckState deck_states_UNCONFIRMED = 1; // repeated message } message SyncLeadAudioInfo { bytes field1_UNKNOWN = 1; // message uint32 field2_UNKNOWN = 2; // varint double field3_UNKNOWN = 3; // fixed64 double field4_UNKNOWN = 4; // fixed64 } message TimestampSync { uint64 field1_UNKNOWN = 1; // varint uint64 field2_UNKNOWN = 2; // varint uint64 field3_UNKNOWN = 3; // varint string hostUuid = 4; }

enginelibrary.v1.EngineLibraryService: library sharing

Method

Kind

Request

Response

GetCredentials

unary

GetCredentialsRequest

GetCredentialsResponse

GetLibraries

unary

GetLibrariesRequest

GetLibrariesResponse

GetLibrary

unary

GetLibraryRequest

GetLibraryResponse

GetTrack

unary

GetTrackRequest

GetTrackResponse

GetSearchFilters

unary

GetSearchFiltersRequest

GetSearchFiltersResponse

GetHistorySessions

unary

GetHistorySessionsRequest

GetHistorySessionsResponse

GetHistoryPlayedTracks

unary

GetHistoryPlayedTracksRequest

GetHistoryPlayedTracksResponse

PutEvents

unary

PutEventsRequest

PutEventsResponse

GetTracks

server streaming

GetTracksRequest

GetTracksResponse

SearchTracks

server streaming

SearchTracksRequest

SearchTracksResponse

EventStream

server streaming

EventStreamRequest

EventStreamResponse

// Reconstructed from /usr/Engine/Engine (Engine OS 5.0.4, SC6000). NOT an official schema. // See the enginesync snippet header for how to read the type annotations. // // NOT observed listening on TCP 50010 - the device returns UNIMPLEMENTED for this service // on that port. Engine contains both the client Stub and the server Service base classes, // so it can act as either end. syntax = "proto3"; package enginelibrary.v1; service EngineLibraryService { rpc GetCredentials (GetCredentialsRequest) returns (GetCredentialsResponse); rpc GetLibraries (GetLibrariesRequest) returns (GetLibrariesResponse); rpc GetLibrary (GetLibraryRequest) returns (GetLibraryResponse); rpc GetTrack (GetTrackRequest) returns (GetTrackResponse); rpc GetSearchFilters (GetSearchFiltersRequest) returns (GetSearchFiltersResponse); rpc GetHistorySessions (GetHistorySessionsRequest) returns (GetHistorySessionsResponse); rpc GetHistoryPlayedTracks (GetHistoryPlayedTracksRequest) returns (GetHistoryPlayedTracksResponse); rpc PutEvents (PutEventsRequest) returns (PutEventsResponse); // server streaming rpc GetTracks (GetTracksRequest) returns (stream GetTracksResponse); rpc SearchTracks (SearchTracksRequest) returns (stream SearchTracksResponse); rpc EventStream (EventStreamRequest) returns (stream EventStreamResponse); } // ---------------------------------------------------------------- requests / responses message GetCredentialsRequest { string library_id = 1; uint32 field2_UNKNOWN = 2; } message GetCredentialsResponse { bytes credentials_UNCONFIRMED = 1; bytes field2_UNKNOWN = 2; } message Credentials { string access_token = 1; } message GetLibrariesRequest { } // no serialized fields message GetLibrariesResponse { repeated Library libraries_UNCONFIRMED = 1; } message GetLibraryRequest { string library_id = 1; } message GetLibraryResponse { bytes library_UNCONFIRMED = 1; } message Library { string id = 1; string title = 2; uint32 field3_UNKNOWN = 3; // varint } message GetTracksRequest { string library_id = 1; string playlist_id = 2; uint32 field3_UNKNOWN = 3; // varint (plausibly a page size / offset) Sort sort_UNCONFIRMED = 4; // message uint32 field5_UNKNOWN = 5; // varint bytes field6_UNKNOWN = 6; // message } message GetTracksResponse { repeated ListTrack tracks_UNCONFIRMED = 1; } message GetTrackRequest { string library_id = 1; string track_id = 2; } message GetTrackResponse { bytes field1_UNKNOWN = 1; // message (probably TrackMetadata) bytes field2_UNKNOWN = 2; // message (probably TrackPerformanceData) bytes field3_UNKNOWN = 3; // message (probably TrackLocation / TrackBlob) } message SearchTracksRequest { string library_id = 1; string query = 2; bytes field3_UNKNOWN = 3; bytes field4_UNKNOWN = 4; // message (probably Sort or SearchFilter list) uint32 field5_UNKNOWN = 5; // varint bytes field6_UNKNOWN = 6; // message } message SearchTracksResponse { repeated ListTrack tracks_UNCONFIRMED = 1; } message GetSearchFiltersRequest { string library_id = 1; string query = 2; bytes field3_UNKNOWN = 3; } message GetSearchFiltersResponse { bytes options_UNCONFIRMED = 1; } message SearchFilterOptions { bytes field1_UNKNOWN = 1; // message bytes field2_UNKNOWN = 2; // message bytes field3_UNKNOWN = 3; // message bytes field4_UNKNOWN = 4; // message bytes field5_UNKNOWN = 5; // message } message SearchFilter { uint32 field1_UNKNOWN = 1; string value = 2; } message SearchFilterValue { string value = 1; } message Sort { uint32 field1_UNKNOWN = 1; uint32 field2_UNKNOWN = 2; } message GetHistorySessionsRequest { string library_id = 1; } message GetHistorySessionsResponse { repeated HistorySession sessions_UNCONFIRMED = 1; } message HistorySession { string id = 1; string title = 2; bytes field3_UNKNOWN = 3; // message string timezone = 4; uint64 field5_UNKNOWN = 5; // varint uint64 field6_UNKNOWN = 6; // varint uint64 field7_UNKNOWN = 7; // varint uint64 field8_UNKNOWN = 8; // varint } message GetHistoryPlayedTracksRequest { string library_id = 1; string session_id = 2; } message GetHistoryPlayedTracksResponse { repeated HistoryPlayedTrack tracks_UNCONFIRMED = 1; } message HistoryPlayedTrack { uint64 field1_UNKNOWN = 1; // varint bytes field2_UNKNOWN = 2; // message uint64 field3_UNKNOWN = 3; // varint } message PlayedTrack { string played_track_id = 1; bytes field2_UNKNOWN = 2; // message bytes field3_UNKNOWN = 3; // message } message PutEventsRequest { string library_id = 1; repeated Event events_UNCONFIRMED = 2; } message PutEventsResponse { uint32 field1_UNKNOWN = 1; } message EventStreamRequest { string library_id = 2; string device_id = 3; } message EventStreamResponse { bytes event_UNCONFIRMED = 1; } message Event { string library_id = 1; string device_id = 2; bytes field3_UNKNOWN = 3; // message bytes field4_UNKNOWN = 4; // message bytes field5_UNKNOWN = 5; // message bytes field6_UNKNOWN = 6; // message } message EventAck { uint32 field1_UNKNOWN = 1; string device_id = 2; } message EventPlaylistsContentChanged { string playlist_id = 1; } message EventPlaylistHierarchyChanged { } message EventTrackMetadataChanged { bytes field1_UNKNOWN = 1; } message EventTrackPerformanceDataChanged { bytes field1_UNKNOWN = 1; } // ---------------------------------------------------------------- track model message Track { bytes field1_UNKNOWN = 1; // message bytes field2_UNKNOWN = 2; // message bytes field3_UNKNOWN = 3; // message } message ListTrack { bytes field1_UNKNOWN = 1; // message string field4_UNKNOWN = 4; } // The most completely recovered message in the package. message TrackMetadata { string id = 1; // field 2 is never serialized - reserved or removed string title = 3; string artist = 4; string album = 5; double field6_UNKNOWN = 6; // fixed64 (plausibly length or bpm) string key = 7; uint32 field8_UNKNOWN = 8; // varint uint32 field9_UNKNOWN = 9; // varint string genre = 10; string comment = 11; string label = 12; uint32 field13_UNKNOWN = 13; // varint (plausibly year or rating) string composer = 14; string remixer = 15; bytes field16_UNKNOWN = 16; // message } // Fully resolved by reading the serializer directly. message TrackPerformanceData { string id = 1; // field 2 is never serialized - reserved or removed bytes field3_UNKNOWN = 3; // bytes, not string: written with no UTF-8 verification // Two protobuf map fields, matching the generated TrackPerformanceData_LoopsEntry_DoNotUse // and TrackPerformanceData_QuickCuesEntry_DoNotUse helper types. Both serialize as // <key = varint field 1, value = message field 2>. Which of 4/5 is loops and which is // quick_cues could not be determined - the serializer emits field 4's map first. map<uint32, QuickCue> map_field_4_UNCONFIRMED = 4; map<uint32, Loop> map_field_5_UNCONFIRMED = 5; MainCue main_cue_UNCONFIRMED = 6; // message bytes field7_UNKNOWN = 7; // bytes (no UTF-8 verification) int64 field8_UNKNOWN = 8; // varint, sign-extended -> signed double field9_UNKNOWN = 9; // fixed64 bytes field10_UNKNOWN = 10; // bytes (no UTF-8 verification) } message Loop { string name = 1; double field2_UNKNOWN = 2; // fixed64 (start position) double field3_UNKNOWN = 3; // fixed64 (end position) Color color_UNCONFIRMED = 4; uint32 field5_UNKNOWN = 5; // varint } message QuickCue { string name = 1; double field2_UNKNOWN = 2; // fixed64 (position) Color color_UNCONFIRMED = 3; } message MainCue { uint32 field2_UNKNOWN = 2; // varint double field3_UNKNOWN = 3; // fixed64 } message Color { uint32 field1_UNKNOWN = 1; // varint (r?) uint32 field2_UNKNOWN = 2; // varint (g?) uint32 field3_UNKNOWN = 3; // varint (b?) uint32 field4_UNKNOWN = 4; // varint (a?) } message TrackLocation { bytes field1_UNKNOWN = 1; string key = 2; } message TrackBlob { bytes field1_UNKNOWN = 1; } message TrackBlobUrl { string url = 1; uint32 field2_UNKNOWN = 2; } message BlobProvider { uint32 field1_UNKNOWN = 1; string id = 2; } message PlaylistMetadata { string id = 1; string title = 2; uint32 field3_UNKNOWN = 3; // varint bytes field4_UNKNOWN = 4; // message uint32 field5_UNKNOWN = 5; // varint }

networktrust.v1.NetworkTrustService: device pairing

Method

Kind

Request

Response

CreateTrust

unary

CreateTrustRequest

CreateTrustResponse

The request carries an Ed25519 public key as a string (not bytes; so it is hex or base64 text) plus a device_name that is shown to the user. The response is one of CreateTrustGranted, CreateTrustDenied or CreateTrustBusy, and the strings Password Received: fields: and Cannot enter a password if there is not an active request indicate an interactive prompt on the device with a single request in flight at a time.

// Reconstructed from /usr/Engine/Engine (Engine OS 5.0.4, SC6000). NOT an official schema. // See the enginesync snippet header for how to read the type annotations. // // NOT observed listening on TCP 50010 - the device returns UNIMPLEMENTED for this service // on that port. Where (or whether) it is exposed on a network interface is unknown; the // client side lives in eaas::GRPCNetworkTrustClient. syntax = "proto3"; package networktrust.v1; service NetworkTrustService { rpc CreateTrust (CreateTrustRequest) returns (CreateTrustResponse); } message CreateTrustRequest { // Declared as `string`, not `bytes` - protobuf only emits a UTF-8 verification literal // for string fields, and one exists for this field. So the public key is carried as // text (hex or base64), not as 32 raw bytes. string ed25519_pk = 1; uint32 field2_UNKNOWN = 2; // varint. Possibly a port or a protocol/device-type enum string device_name = 3; // shown to the user in the pairing prompt } // Serializes a single message field at number 3. The three outcome messages below are the // candidates for it; a `oneof` is the natural reading but the wrapper only ever wrote one // field number, so this is the least certain part of the reconstruction. message CreateTrustResponse { // oneof result { // CreateTrustGranted granted = ?; // CreateTrustDenied denied = ?; // CreateTrustBusy busy = ?; // } bytes result_UNCONFIRMED = 3; // message } message CreateTrustGranted { // No serialized fields recovered: may be an empty marker message. } message CreateTrustDenied { // No serialized fields recovered: may be an empty marker message. } message CreateTrustBusy { uint32 field1_UNKNOWN = 1; // varint. Plausibly a retry-after hint }

remotehostscreen.v1 is not gRPC

Worth stating plainly, because the package looks like the others: there is no remotehostscreen service and no method-path strings. All 75 message types are fields of one envelope, HybridModeMessage, exchanged as framed protobuf over its own transport. The relevant binary is /usr/bin/planck-remote-screen a JUCE application (juce::String, SmexControlHost, SmexControlClient), a completely different stack from the Qt-based Engine launched by /usr/Engine/Scripts/remote-screen.sh.

The envelope has 77 message-typed fields at sparse numbers (1-4, 10-22, 31-35, a contiguous 40-77, then 90-107), which suggests commands were appended over successive releases. The individual command messages are well recovered; the mapping from envelope field number to command type is not, because a serialized message field records only its number and length.

// Reconstructed from /usr/Engine/Engine (Engine OS 5.0.4, SC6000). NOT an official schema. // // IMPORTANT: remotehostscreen.v1 is NOT a gRPC service. There is no service definition and // no /package.Service/Method path string anywhere in the binary. Instead every command is a // field of the single envelope message HybridModeMessage, which is exchanged as framed // protobuf over its own transport (see /usr/bin/planck-remote-screen and // /usr/Engine/Scripts/remote-screen.sh). // // Field numbers below are recovered from the compiled serializers. The envelope's member // types are NOT recoverable from the wire code (a message field only records its number and // length), so the mapping from envelope field number to command message is UNKNOWN except // where noted. The numbering is sparse, which suggests fields were added over time. syntax = "proto3"; package remotehostscreen.v1; // The envelope. 77 message-typed fields at these numbers: // 1 2 3 4 10 12 14 15 16 17 18 19 20 21 22 31 32 34 35 // 40..77 (contiguous), 90 91 92 94 95 96 98 99 // 100 101 102 103 104 105 106 107 message HybridModeMessage { // Each field is a distinct command message. Numbers confirmed; payload types unknown. // e.g. bytes field_1 = 1; ... bytes field_107 = 107; } // ------------------------------------------------------------------ command messages // "deck" below is the leading varint present on nearly every deck-scoped command // (field 1). It is almost certainly a deck index. message KeepAlive { string appName = 1; string appVersion = 2; } message Goodbye { } message DebugString { string message = 1; } message SetDeviceConfig { uint32 f1 = 1; uint32 f2 = 2; uint32 f3 = 3; uint32 f4 = 4; // all varint } // ---- deck state message SetDeckLoaded { uint32 deck = 1; } message SetDeckCleared { uint32 deck = 1; } message LoadTrackToDeck { uint32 deck = 1; } message EjectTrackFromDeck { uint32 deck = 1; } message InstantDoubleToDeck { uint32 deck = 1; } message SetDeckKeyLock { uint32 deck = 1; uint32 locked = 2; } message SetDeckTempo { uint32 deck = 1; float tempo = 2; } // fixed32 message SetDeckSpeedState { uint32 deck = 1; float speed = 2; } // fixed32 message SetDeckSpeedRange { uint32 deck = 1; string value = 2; } message SetDeckPlayheadPosition { uint32 deck = 1; double p1 = 2; double p2 = 3; double p3 = 4; } message DeckNeedleDrop { uint32 deck = 1; double position = 2; } message SetDeckTempCue { uint32 deck = 1; double position = 2; } message ChangeDeckKey { uint32 deck = 1; uint32 key = 2; } message SetDeckKey { uint32 deck = 1; uint32 f2 = 2; uint32 f3 = 3; uint32 f4 = 4; } message DeckInputSourceChanged { uint32 deck = 1; uint32 source = 2; } message SetMixerChannelMode { uint32 deck = 1; uint32 mode = 2; } // ---- deck track info message SetDeckTrackTitle { uint32 deck = 1; string title = 2; } message SetDeckTrackArtist { uint32 deck = 1; string artist = 2; } message SetDeckTrackData { uint32 deck = 1; string title = 2; string artist = 3; double f4 = 4; } message SetDeckAlbumArt { uint32 deck = 1; uint32 art_id = 2; } // ---- loops and cues message SetDeckAutoLoopEnabled { uint32 deck = 1; uint32 enabled = 2; } message SetDeckAutoLoopLabel { uint32 deck = 1; string value = 2; } message SetDeckBeatJumpLabel { uint32 deck = 1; string value = 2; } message SetDeckLoopRegion { uint32 deck = 1; // field 2 not serialized double start = 3; double end = 4; // fixed64 string name = 5; uint32 f6 = 6; uint32 f7 = 7; // varint } message ClearDeckLoopRegion { uint32 deck = 1; } message SetDeckQuickCue { uint32 deck = 1; uint32 index = 2; double position = 3; // fixed64 string name = 4; uint32 f5 = 5; // varint } message ClearDeckQuickCue { uint32 deck = 1; uint32 index = 2; } // ---- waveforms and beatgrid message SetDeckBeatGrid { uint32 deck = 1; bytes anchors = 2; double f3 = 3; } message SetDeckBeatGrid_BeatAnchor { } // no serialized fields recovered message SetShowBeatGrid { uint32 show = 1; } message SetDeckOverviewWaveform { uint32 deck = 1; bytes waveform = 2; } message UpdateWaveformChunk { uint32 deck = 1; uint32 index = 2; bytes chunk = 3; } // ---- images message SetAlbumArtImage { uint32 id = 1; bytes image = 2; } message SetIconImage { uint32 id = 1; bytes image = 2; uint32 f3 = 3; } // ---- playlist / browser panes message SetPlaylistsHeader { string title = 1; } message SetPlaylistsHeaderIcon { uint32 icon = 1; } message SetPlaylistsHeaderHasParent { } // no serialized fields recovered message SetPlaylistsHeaderSelectable { } // no serialized fields recovered message SetPlaylistsCurrentRow { uint32 row = 1; } message SetPlaylistsCurrentLevel { uint32 level = 1; } message SetPlaylistsRowCount { uint32 count = 1; } message SetPlaylistsViewportPosition { uint32 position = 1; } message SetPlaylistTitle { uint32 index = 1; string title = 2; } message SetPlaylistIcon { uint32 index = 1; uint32 icon = 2; } message SetPlaylistData { uint32 index = 1; string title = 2; uint32 f3 = 3; uint32 f4 = 4; } message PlaylistBankAction { uint32 f1 = 1; uint32 f2 = 2; } // ---- track list pane message SetTracklistRowCount { uint32 count = 1; } message SetTracklistCurrentRow { uint32 row = 1; } message SetTracklistViewportPosition { uint32 position = 1; } message SetTracklistShowPlayOrder { } // no serialized fields recovered message SetTracklistSortMode { uint32 f1 = 1; uint32 f2 = 2; } message SetTrackTitle { uint32 index = 1; string title = 2; } message SetTrackArtist { uint32 index = 1; string artist = 2; } message SetTrackAlbum { uint32 index = 1; string album = 2; } message SetTrackData { uint32 index = 1; string title = 2; string artist = 3; string album = 4; uint32 f5 = 5; uint32 f6 = 6; uint32 f7 = 7; // field 8 not serialized uint32 f9 = 9; uint32 f10 = 10; } message SetTrackKey { uint32 index = 1; uint32 key = 2; } message SetTrackTempo { uint32 index = 1; uint32 tempo = 2; } message SetTrackTimeLength { uint32 index = 1; uint32 length = 2; } message SetTrackPlayOrder { uint32 index = 1; uint32 order = 2; } message SetTrackState { uint32 index = 1; uint32 state = 2; } message SetTrackAlbumArt { uint32 index = 1; uint32 art_id = 2; } // ---- FX and behaviour toggles message FXBeatsChanged { uint32 beats = 1; } message FXDepthChanged { } // no serialized fields recovered message FXInputSourceChanged { uint32 source = 1; } message SetQuantizeEnabled { } // no serialized fields recovered message SetFocusArea { uint32 area = 1; } message SetCensorButtonBehavior { uint32 behavior = 1; } message SetKeyAdjustBehavior { uint32 behavior = 1; } message SetStemFxBehavior { uint32 behavior = 1; } message SetShowSilentCueIcon { uint32 f1 = 1; uint32 f2 = 2; }

Talking to the device

No .proto or generated stubs are needed. A raw HTTP/2 client is enough. Minimum viable call:

  1. TCP connect to <device>:50010.

  2. Send the HTTP/2 client preface PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n and an empty SETTINGS frame.

  3. Open a stream with HEADERS: :method POST, :scheme http, :path /enginesync.v1.EngineSyncService/SetSyncState, :authority <device>:50010, content-type application/grpc, te trailers, and uuid <your-host-uuid> for the methods that require it.

  4. Send DATA: one byte compression flag (0x00), four bytes big-endian message length, then the serialized protobuf.

  5. Read grpc-status from the response trailers (or headers, for an immediate failure).

A SyncState with no fields set is a valid, accepted message, which makes it a convenient reachability probe. Useful status codes seen in practice:

grpc-status

Meaning here

0 OK

Accepted

9 FAILED_PRECONDITION

Missing uuid metadata, or host not yet discovered

12 UNIMPLEMENTED

Service or method not registered on this port. Also returned for a malformed request body

Validation

The four .proto files in this page are not just plausible-looking.

  1. They compile. All four pass protoc 3.21.12 cleanly (--descriptor_set_out=/dev/null).

  2. They generate usable stubs. grpc_tools.protoc produces working Python bindings, and the encoder round-trips: a SyncState containing one DeckState with field2 = 7 serializes to 0a 02 10 07 (field 1, length 2, containing field 2 varint 7, exactly as the wire format requires.)

  3. A real device accepts messages built from them. Encoding SyncState values with the generated bindings and posting them to 10.2.0.209:50010 returns grpc-status 0 in every case:

Message sent

Encoded body

Result

empty SyncState

(empty)

0 OK

one DeckState with DeckID{deck=1, hostUuid=…}

0a2a0a280801122430303030…

0 OK

one DeckState with deck=2 and fields 2/3/4 set

0a0a0a020802100118012001

0 OK

four DeckState entries

0a040a0208010a040a020802…

0 OK

That confirms the field numbers and wire types for SyncState, DeckState and DeckID are correct as far as the device's parser is concerned. It does not confirm the semantics. The device accepted the messages, but what it did with them was not observed.

How this was recovered, and where it is weak

The binary uses protobuf-lite, which strips descriptors. There is no FileDescriptorProto to dump and no reflection to query. Each piece came from somewhere different:

What

Source

Confidence

Service and method names

/package.Service/Method string literals

Exact

Method kind, request/response types

C++ template parameters in RTTI typeinfo names, e.g. RpcMethodHandler<...Service, SyncState, protobuf::Empty>

Exact

Message inventory

Demangled typeinfo names per package

Exact

Field numbers and wire types

Tag bytes and field-number arguments in each message's _InternalSerialize (vtable slot 12)

Exact where recovered

string field names

WireFormatLite::VerifyUtf8String(..., "pkg.Msg.field") protobuf emits this only for string fields

Exact

string vs bytes

Same mechanism, used as a discriminator: a length-delimited field written without a UTF-8 verification call is bytes, not string

Exact

Map fields

Generated *_LoopsEntry_DoNotUse helper types

Exact that a map exists

Everything else

Not recoverable

Known weaknesses, so nobody trusts this further than it deserves:

  • Non-string field names are unknown. bytes, numeric, bool, enum and message fields leave no name behind. They are labeled fieldN_UNKNOWN.

  • Wire types under-determine declared types. A varint may be any of int32 int64 uint32 uint64 sint32 sint64 bool enum; fixed64 may be double fixed64 sfixed64.

  • Message field types are unknown. A message-typed field records only its number, so GetTrackResponse fields 1-3 are known to be messages, but their types are inferred at best.

  • Some serializers are shared. protobuf deduplicates identical layouts, so several "single repeated message field" messages resolve to the same function (0113fb90). The shape is right; the element type is inferred from the method signature.

  • A few messages report no fields. Either genuinely empty marker messages, or their fields are emitted through a path the extraction does not follow.

  • TrackPerformanceData has a conflicting recovery for field 1. Flagged inline.

  • Field numbers not listed are not necessarily unused; TrackMetadata skips 2 and SetTrackData skips 8, which usually means reserved or removed fields.

Open questions

Concrete things a contributor could close:

  1. Where are enginelibrary.v1 and networktrust.v1 exposed? They are not on 50010. Candidates: one of the ~12 per-device ephemeral ports, a different interface, or only after pairing.

  2. What are the ~12 high ports? They differ per device and are silent to an HTTP/2 preface, so they are probably airNetworkExchange directory/service sockets, not gRPC.

  3. Envelope field mapping for HybridModeMessage: a capture of a real remote-screen session would settle it quickly.

  4. Non-string field names would need either a debug build, the .proto files, or careful correlation of observed traffic against known UI state.

  5. Eaas has its own gRPC server (Libraries/private/Planck/Eaas/src/grpcServer/, including Handlers/TrackWriterHandler.h) which is not covered here at all.

12 August 2026