diff --git a/events_received.go b/events_received.go new file mode 100644 index 0000000..db49191 --- /dev/null +++ b/events_received.go @@ -0,0 +1,301 @@ +package streamdeck + +import ( + "encoding/json" + "reflect" +) + +var receivedEventTypeMap = map[string]reflect.Type{} + +func init() { + receivedEventTypeMap["keyUp"] = reflect.TypeOf(ERKeyUp{}) + receivedEventTypeMap["didReceiveSettingsPayload"] = reflect.TypeOf(ERDidReceiveSettingsPayload{}) + receivedEventTypeMap["didReceiveSettings"] = reflect.TypeOf(ERDidReceiveSettings{}) + receivedEventTypeMap["globalSettings"] = reflect.TypeOf(ERGlobalSettings{}) + receivedEventTypeMap["didReceiveDeepLink"] = reflect.TypeOf(ERDidReceiveDeepLink{}) + receivedEventTypeMap["touchTap"] = reflect.TypeOf(ERTouchTap{}) + receivedEventTypeMap["dialDown"] = reflect.TypeOf(ERDialDown{}) + receivedEventTypeMap["dialUp"] = reflect.TypeOf(ERDialUp{}) + receivedEventTypeMap["dialRotate"] = reflect.TypeOf(ERDialRotate{}) + receivedEventTypeMap["keyDown"] = reflect.TypeOf(ERKeyDown{}) + receivedEventTypeMap["willAppear"] = reflect.TypeOf(ERWillAppear{}) + receivedEventTypeMap["willDisappear"] = reflect.TypeOf(ERWillDisappear{}) + receivedEventTypeMap["titleParametersDidChange"] = reflect.TypeOf(ERTitleParametersDidChange{}) + receivedEventTypeMap["deviceDidConnect"] = reflect.TypeOf(ERDeviceDidConnect{}) + receivedEventTypeMap["deviceDidDisconnect"] = reflect.TypeOf(ERDeviceDidDisconnect{}) + receivedEventTypeMap["applicationDidLaunch"] = reflect.TypeOf(ERApplicationDidLaunch{}) + receivedEventTypeMap["applicationDidTerminate"] = reflect.TypeOf(ERApplicationDidTerminate{}) + receivedEventTypeMap["applicationSystemDidWakeUp"] = reflect.TypeOf(ERApplicationSystemDidWakeUp{}) + receivedEventTypeMap["applicationPropertyInspectorDidAppear"] = reflect.TypeOf(ERApplicationPropertyInspectorDidAppear{}) + receivedEventTypeMap["applicationPropertyInspectorDidDisappear"] = reflect.TypeOf(ERApplicationPropertyInspectorDidDisappear{}) + receivedEventTypeMap["applicationPropertySendToPlugin"] = reflect.TypeOf(ERApplicationPropertySendToPlugin{}) + receivedEventTypeMap["applicationPropertySendToPropertyInspector"] = reflect.TypeOf(ERApplicationPropertySendToPropertyInspector{}) +} + +type ERBase struct { + Event string `json:"event"` +} + +//{Action: Event:deviceDidConnect Context: Device:A1DA463F033AD2616E05636CD16F064F Payload:[]}"} + +type ERCommon struct { + Action string `json:"action"` + Event string `json:"event"` + Context string `json:"context"` + Device string `json:"device"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#didreceivesettings + +type ERDidReceiveSettingsPayload struct { + Settings json.RawMessage `json:"settings"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + IsInMultiAction bool `json:"isInMultiAction"` +} + +type ERDidReceiveSettings struct { + ERCommon + Payload ERDidReceiveSettingsPayload `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#didreceiveglobalsettings + +type ERGlobalSettings struct { + Event string `json:"event"` + Payload json.RawMessage `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#didreceivedeeplink + +type ERDidReceiveDeepLink struct { + Event string `json:"event"` + Payload struct { + Url string `json:"url"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#touchtap-sd + +type ERTouchTap struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Controller string `json:"controller"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + TapPosition []int `json:"tapPos"` + Hold bool `json:"hold"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#dialdown-sd + +type ERDialDown struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Controller string `json:"controller"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#dialup-sd + +type ERDialUp struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Controller string `json:"controller"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#dialrotate-sd + +type ERDialRotate struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Controller string `json:"controller"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + Ticks int `json:"ticks"` + Pressed bool `json:"pressed"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#keydown + +type ERKeyDown struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + State int `json:"state"` + UserDesiredState int `json:"userDesiredState"` + IsInMultiAction bool `json:"isInMultiAction"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#keyup +type ERKeyUp struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + State int `json:"state"` + UserDesiredState int `json:"userDesiredState"` + IsInMultiAction bool `json:"isInMultiAction"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#willappear +type ERWillAppear struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + Controller string `json:"controller"` + State int `json:"state"` + IsInMultiAction bool `json:"isInMultiAction"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#willdisappear + +const ERWillDisappearAction = "willDisappear" + +type ERWillDisappear struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + Controller string `json:"controller"` + State int `json:"state"` + IsInMultiAction bool `json:"isInMultiAction"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#titleparametersdidchange + +type ERTitleParametersDidChange struct { + ERCommon + Payload struct { + Settings json.RawMessage `json:"settings"` + Coordinates struct { + Column int `json:"column"` + Row int `json:"row"` + } `json:"coordinates"` + State int `json:"state"` + Title string `json:"title"` + TitleParameters struct { + FontFamily string `json:"fontFamily"` + FontSize int `json:"fontSize"` + FontStyle string `json:"fontStyle"` + FontUnderline bool `json:"fontUnderline"` + ShowTitle bool `json:"showTitle"` + TitleAlignment string `json:"titleAlignment"` + TitleColor string `json:"titleColor"` + } `json:"titleParameters"` + } `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#devicedidconnect + +type ERDeviceDidConnect struct { + Event string `json:"event"` + + Device string `json:"device"` + DeviceInfo struct { + Name string `json:"name"` + DeviceType int `json:"type"` + Size struct { + Columns int `json:"columns"` + Rows int `json:"rows"` + } `json:"size"` + } `json:"deviceInfo"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#devicediddisconnect + +type ERDeviceDidDisconnect struct { + Event string `json:"event"` + Device string `json:"device"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#applicationdidlaunch + +type ERApplicationDidLaunch struct { + Event string `json:"event"` + Payload struct { + Application string `json:"application"` + } +} + +// https://docs.elgato.com/sdk/plugins/events-received#applicationdidterminate + +type ERApplicationDidTerminate struct { + Event string `json:"event"` + Payload struct { + Application string `json:"application"` + } +} + +// https://docs.elgato.com/sdk/plugins/events-received#systemdidwakeup + +type ERApplicationSystemDidWakeUp struct { + Event string `json:"event"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#propertyinspectordidappear +type ERApplicationPropertyInspectorDidAppear struct { + ERCommon +} + +// https://docs.elgato.com/sdk/plugins/events-received#propertyinspectordiddisappear + +type ERApplicationPropertyInspectorDidDisappear struct { + ERCommon +} + +// https://docs.elgato.com/sdk/plugins/events-received#sendtoplugin + +type ERApplicationPropertySendToPlugin struct { + Action string `json:"action"` + Event string `json:"event"` + Context string `json:"context"` + Payload json.RawMessage `json:"payload"` +} + +// https://docs.elgato.com/sdk/plugins/events-received#sendtopropertyinspector +type ERApplicationPropertySendToPropertyInspector struct { + Action string `json:"action"` + Event string `json:"event"` + Context string `json:"context"` + Payload json.RawMessage `json:"payload"` +} diff --git a/events_send.go b/events_send.go new file mode 100644 index 0000000..e9321e2 --- /dev/null +++ b/events_send.go @@ -0,0 +1,385 @@ +package streamdeck + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "image" + "image/png" +) + +type EventTarget int + +const EventTargetBoth = 0 +const EventTargetHardware = 1 +const EventTargetSoftware = 2 + +type ESCommon struct { + Event string `json:"event"` + Context string `json:"context"` +} + +type ESCommonNoContext struct { + Event string `json:"event"` +} + +type ESOpenMessage struct { + ESCommonNoContext + UUID string `json:"uuid"` +} + +// https://docs.elgato.com/sdk/plugins/events-sent#setsettings + +type ESSetSettings struct { + ESCommon + Payload json.RawMessage +} + +func NewESSetSettings(context string, payload json.RawMessage) ESSetSettings { + return ESSetSettings{ + ESCommon: ESCommon{ + Event: "setSettings", + Context: context, + }, + Payload: payload, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#getsettings + +type ESGetSettings struct { + ESCommon +} + +func NewESGetSettings(context string) ESGetSettings { + return ESGetSettings{ + ESCommon: ESCommon{ + Event: "getSettings", + Context: context, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#setglobalsettings + +type ESSetGlobalSettings struct { + ESCommon + Payload json.RawMessage +} + +func NewESSetGlobalSettings(context string, payload json.RawMessage) ESSetGlobalSettings { + return ESSetGlobalSettings{ + ESCommon: ESCommon{ + Event: "setGlobalSettings", + Context: context, + }, + Payload: payload, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#getglobalsettings + +type ESGetGlobalSettings struct { + ESCommon +} + +func NewESGetGlobalSettings(context string) ESGetGlobalSettings { + return ESGetGlobalSettings{ + ESCommon: ESCommon{ + Event: "getGlobalSettings", + Context: context, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#openurl + +type ESOpenURL struct { + ESCommonNoContext + Payload ESOpenURLPayload `json:"payload"` +} + +type ESOpenURLPayload struct { + URL string `json:"url"` +} + +func NewESOpenURL(url string) ESOpenURL { + return ESOpenURL{ + ESCommonNoContext: ESCommonNoContext{Event: "openUrl"}, + Payload: ESOpenURLPayload{ + URL: url, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#logmessage +type ESLogMessage struct { + ESCommonNoContext + Payload ESLogMessagePayload `json:"payload"` +} + +type ESLogMessagePayload struct { + Message string `json:"message"` +} + +func NewESLogMessage(message string) ESLogMessage { + return ESLogMessage{ + ESCommonNoContext: ESCommonNoContext{Event: "logMessage"}, + Payload: ESLogMessagePayload{Message: message}, + } +} + +// setTitle https://docs.elgato.com/sdk/plugins/events-sent#settitle +type ESSetTitle struct { + ESCommon + Payload ESSetTitlePayload `json:"payload"` +} + +type ESSetTitlePayload struct { + Title string `json:"title"` + Target EventTarget `json:"target"` + State int `json:"state"` +} + +func NewESSetTitle(context string, title string, target EventTarget, state int) ESSetTitle { + return ESSetTitle{ + ESCommon: ESCommon{ + Event: "setTitle", + Context: context, + }, + Payload: ESSetTitlePayload{ + Title: title, + Target: target, + State: state, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#setimage +type ESSetImage struct { + ESCommon + Payload ESSetImagePayload `json:"payload"` +} + +type ESSetImagePayload struct { + Image string `json:"title"` + Target EventTarget `json:"target"` + State int `json:"state"` +} + +func NewESSetImagePayload(context string, imageBase64 string, target EventTarget, state int) ESSetImage { + return ESSetImage{ + ESCommon: ESCommon{ + Event: "setImage", + Context: context, + }, + Payload: ESSetImagePayload{ + Image: imageBase64, + Target: target, + State: state, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#setfeedback-sd +type ESSetFeedback struct { + ESCommon + Payload json.RawMessage `json:"payload"` +} + +func NewESSetFeedback(context string, payload json.RawMessage) ESSetFeedback { + return ESSetFeedback{ + ESCommon: ESCommon{ + Event: "setFeedback", + Context: context, + }, + Payload: payload, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#setfeedbacklayout-sd +type ESSetFeedbackLayout struct { + ESCommon + Payload ESSetFeedbackLayoutPayload `json:"payload"` +} + +type ESSetFeedbackLayoutPayload struct { + Layout string `json:"layout"` +} + +func NewESSetFeedbackLayout(context string, layout string) ESSetFeedbackLayout { + return ESSetFeedbackLayout{ + ESCommon: ESCommon{ + Event: "setFeedbackLayout", + Context: context, + }, + Payload: ESSetFeedbackLayoutPayload{ + Layout: layout, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#settriggerdescription-sd +type ESSetTriggerDescription struct { + ESCommon + Payload ESSetTriggerDescriptionPayload `json:"payload"` +} + +type ESSetTriggerDescriptionPayload struct { + Rotate string `json:"rotate,omitempty"` + Push string `json:"push,omitempty"` + Touch string `json:"touch,omitempty"` + LongTouch string `json:"longTouch,omitempty"` +} + +func NewESSetTriggerDescription(context string, rotate, push, touch, longTouch string) ESSetTriggerDescription { + return ESSetTriggerDescription{ + ESCommon: ESCommon{ + Event: "setTriggerDescription", + Context: context, + }, + Payload: ESSetTriggerDescriptionPayload{ + Rotate: rotate, + Push: push, + Touch: touch, + LongTouch: longTouch, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#showalert +type ESShowAlert struct { + ESCommon +} + +func NewESShowAlert(context string) ESShowAlert { + return ESShowAlert{ + ESCommon: ESCommon{ + Event: "showAlert", + Context: context, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#showok +type ESShowOK struct { + ESCommon +} + +func NewESShowOK(context string) ESShowOK { + return ESShowOK{ + ESCommon: ESCommon{ + Event: "showOk", + Context: context, + }, + } +} + +//https://docs.elgato.com/sdk/plugins/events-sent#setstate + +type ESSetState struct { + ESCommon + Payload ESSetStatePayload `json:"payload"` +} + +type ESSetStatePayload struct { + State int `json:"state"` +} + +func NewESSetState(context string, state int) ESSetImage { + return ESSetImage{ + ESCommon: ESCommon{ + Event: "setState", + Context: context, + }, + Payload: ESSetImagePayload{ + State: state, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#switchtoprofile + +type ESSwitchToProfile struct { + ESCommon + Device string `json:"device"` + Payload ESSwitchToProfilePayload `json:"payload"` +} + +type ESSwitchToProfilePayload struct { + Profile string `json:"profile"` + Page int `json:"page"` +} + +func NewESSwitchToProfile(context string, device, profileName string, page int) ESSwitchToProfile { + return ESSwitchToProfile{ + ESCommon: ESCommon{ + Event: "switchToProfile", + Context: context, + }, + Device: device, + Payload: ESSwitchToProfilePayload{ + Profile: profileName, + Page: page, + }, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#sendtopropertyinspector + +type ESSendToPropertyInspector struct { + ESCommon + Action string `json:"action"` + Payload json.RawMessage +} + +func NewESSendToPropertyInspector(context string, action string, payload json.RawMessage) ESSendToPropertyInspector { + return ESSendToPropertyInspector{ + ESCommon: ESCommon{ + Event: "sendToPropertyInspector", + Context: context, + }, + Action: action, + Payload: payload, + } +} + +// https://docs.elgato.com/sdk/plugins/events-sent#sendtoplugin + +type ESSendToPlugin struct { + ESCommon + Action string `json:"action"` + Payload json.RawMessage +} + +func NewESSendToPlugin(context string, action string, payload json.RawMessage) ESSendToPlugin { + return ESSendToPlugin{ + ESCommon: ESCommon{ + Event: "sendToPlugin", + Context: context, + }, + Action: action, + Payload: payload, + } +} + +/// --------- helpers ---------- + +// Turns an image.Image into a string suitable for delivering +// via a ESSetImage struct +func ImageToPayload(i image.Image) string { + + out := bytes.Buffer{} + b64 := base64.NewEncoder(base64.RawStdEncoding, &out) + err := png.Encode(b64, i) + if err != nil { + panic(err) + } + return "data:image/png;base64," + out.String() +} + +// Turns an image.Image into a string suitable for delivering +// via a ESSetImage struct +func SVGToPayload(context string, svg string) string { + return "data:image/svg+xml;charset=utf8," + svg +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..14288aa --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/tardisx/streamdeck-plugin + +go 1.22.1 + +require github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..25a9fc4 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/streamdeck.go b/streamdeck.go new file mode 100644 index 0000000..3dcf0e0 --- /dev/null +++ b/streamdeck.go @@ -0,0 +1,202 @@ +package streamdeck + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "reflect" + + "github.com/gorilla/websocket" +) + +type logger interface { + Info(string, ...any) + Error(string, ...any) + Debug(string, ...any) +} + +type nullLogger struct{} + +func (nl nullLogger) Info(string, ...any) {} +func (nl nullLogger) Error(string, ...any) {} +func (nl nullLogger) Debug(string, ...any) {} + +type EventHandler struct { + MsgType string + Handler func() +} + +type Connection struct { + ws *websocket.Conn + logger logger + handlers map[reflect.Type]reflect.Value + done chan (bool) +} + +func New() Connection { + return Connection{ + handlers: make(map[reflect.Type]reflect.Value), + logger: nullLogger{}, + done: make(chan bool), + } +} + +func NewWithLogger(l logger) Connection { + c := New() + c.logger = l + return c +} + +func (conn *Connection) Connect(port int, openEvent string, uuid string) error { + + c, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://localhost:%d", port), nil) + if err != nil { + return err + } + + conn.ws = c + msg := ESOpenMessage{ + ESCommonNoContext: ESCommonNoContext{ + Event: openEvent, + }, + UUID: uuid, + } + conn.logger.Debug(fmt.Sprintf("writing openMessage: %+v", msg)) + err = c.WriteJSON(msg) + if err != nil { + conn.logger.Error(err.Error()) + panic(err) + } + + // run the reader forever + conn.logger.Info("starting reader") + go conn.reader() + + return nil +} + +func (c *Connection) WaitForPluginExit() { + <-c.done +} + +// RegisterHandler registers a function to be called for a particular event. The +// event to be handled is determined by the functions single parameter type. +// This should be called before Connect to be sure your application is ready to +// receive events. You can register as many handlers as you like, but only one +// function per event type. This function will panic if the wrong kind of +// function is passed in, or if you try to register more than one for a single +// event type. +func (r *Connection) RegisterHandler(handler any) { + hType := reflect.TypeOf(handler) + if hType.Kind() != reflect.Func { + panic("handler must be a function") + } + // Assuming the function takes exactly one argument + if hType.NumIn() != 1 { + panic("handler func must take exactly one argument") + } + + argType := hType.In(0) + + // check its a valid one (one that matches an event type) + valid := false + for i := range receivedEventTypeMap { + if receivedEventTypeMap[i] == argType { + valid = true + break + } + } + if !valid { + panic("you cannot register a handler with this argument type") + } + + _, alreadyExists := r.handlers[argType] + if alreadyExists { + panic("handler for " + argType.Name() + " already exists") + } + + r.handlers[argType] = reflect.ValueOf(handler) +} + +func (conn *Connection) Send(e any) error { + b, _ := json.Marshal(e) + conn.logger.Debug(fmt.Sprintf("sending: %s", string(b))) + + return conn.ws.WriteJSON(e) +} + +func (conn *Connection) handle(event any) { + conn.logger.Debug(fmt.Sprintf("handle: incoming a %T", event)) + argType := reflect.TypeOf(event) + handler, ok := conn.handlers[argType] + if !ok { + conn.logger.Debug(fmt.Sprintf("handle: no handler registered for type %s", argType)) + return + } else { + conn.logger.Debug(fmt.Sprintf("handle: found handler function for type %s", argType)) + + v := []reflect.Value{reflect.ValueOf(event)} + conn.logger.Debug(fmt.Sprintf("handle: handler func: %+v", handler)) + conn.logger.Debug(fmt.Sprintf("handle: handler var: %+v", v)) + + conn.logger.Debug("handle: calling handler function") + + handler.Call(v) + } +} + +func (conn *Connection) reader() { + + for { + _, r, err := conn.ws.NextReader() + if err != nil { + conn.logger.Error(err.Error()) + break + } + + b := bytes.Buffer{} + r = io.TeeReader(r, &b) + base := ERBase{} + err = json.NewDecoder(r).Decode(&base) + if err != nil { + conn.logger.Error("cannot decode: " + err.Error()) + continue + } + + t, ok := receivedEventTypeMap[base.Event] + if !ok { + conn.logger.Error(fmt.Sprintf("no type registered for event '%s'", base.Event)) + continue + } + + d, err := conn.unmarshalToConcrete(t, b.Bytes()) + if err != nil { + conn.logger.Error("cannot unmarshal: " + err.Error()) + continue + } + conn.handle(d) + } + conn.logger.Info("websocket closed, shutting down reader") + conn.done <- true +} + +// unmarshalToConcrete takes a reflect.Type and a byte slice, creates a concrete +// instance of the type and unmarshals the JSON byte slice into it. +func (conn *Connection) unmarshalToConcrete(t reflect.Type, b []byte) (any, error) { + // t is a reflect.Type of the thing we need to decode into + d := reflect.New(t).Interface() + conn.logger.Info(fmt.Sprintf("instance is a %T", d)) + + err := json.Unmarshal(b, &d) + if err != nil { + return nil, fmt.Errorf("could not unmarshal this:\n%s\ninto a %v (%v)\nbecause: %s", string(b), d, t, err.Error()) + } + + // get concrete instance of d into v + v := reflect.ValueOf(d).Elem().Interface() + conn.logger.Info(fmt.Sprintf("NOW instance is a %T", v)) + + conn.logger.Debug(fmt.Sprintf("reader: unmarshalled to: %+v", v)) + return v, nil +} diff --git a/streamdesk_test.go b/streamdesk_test.go new file mode 100644 index 0000000..bf2f2cd --- /dev/null +++ b/streamdesk_test.go @@ -0,0 +1,69 @@ +package streamdeck + +import ( + "testing" +) + +type testLogger struct { + t *testing.T +} + +func (tl testLogger) Info(s string, x ...any) { tl.t.Log(s, x) } +func (tl testLogger) Debug(s string, x ...any) { tl.t.Log(s, x) } +func (tl testLogger) Error(s string, x ...any) { tl.t.Log(s, x) } + +func TestReflection(t *testing.T) { + + c := NewWithLogger(testLogger{t: t}) + // incoming + in := ERDidReceiveSettingsPayload{} + + ranHandler := false + c.RegisterHandler(func(event ERDidReceiveSettingsPayload) { + ranHandler = true + }) + + c.handle(in) + + if !ranHandler { + t.Error("did not run handler") + } + +} + +func TestUmmarshal(t *testing.T) { + + b := []byte(` +{ + "action": "com.elgato.example.action1", + "event": "keyUp", + "context": "ABC123", + "device": "DEF456", + "payload": { + "settings": {}, + "coordinates": { + "column": 3, + "row": 1 + }, + "state": 0, + "userDesiredState": 1, + "isInMultiAction": false + } +}`) + + c := NewWithLogger(testLogger{t: t}) + keyUp, err := c.unmarshalToConcrete(receivedEventTypeMap["keyUp"], b) + + if err != nil { + t.Error(err) + } + + realKeyUp, ok := keyUp.(ERKeyUp) + if !ok { + t.Errorf("wrong type (is %T)", keyUp) + } + if realKeyUp.Context != "ABC123" { + t.Error("wrong value") + } + +}