├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── pull_request_template.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── actionhandlers ├── chained.go ├── colourchange.go ├── custom.go ├── exec.go ├── numberprint.go ├── textlabel.go └── textprint.go ├── buttons ├── colour.go ├── imagefile.go └── text.go ├── comms.go ├── decorators └── border.go ├── devices ├── mini.go ├── orig.go ├── origmk2.go ├── origv2.go ├── shared.go └── xl.go ├── examples ├── brightness │ └── brightness.go ├── client │ └── client.go ├── client2 │ └── client2.go ├── client3 │ └── client3.go ├── screenshot-command │ └── main.go └── test │ ├── play.jpg │ └── test.go ├── go.mod ├── go.sum ├── image.go ├── streamdeck.go └── text.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: magicmonkey 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Streamdeck version** 14 | Mini (6 buttons), original (15 buttons), or XL (32 buttons) 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: what code you ran before hitting the bug, what code triggered the bug. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Environment (please complete the following information):** 23 | - OS: [e.g. Ubuntu 20.04] 24 | - Go version: [e.g. 1.13.1 from snap] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | 7 | --- 8 | 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "gomod" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Fixes # -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vscode-workspace.code-workspace 2 | .vscode/launch.json 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kevin@magicmonkey.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Go-Streamdeck 2 | 3 | All issues, pull requests and other feedback are very welcome in this project. Please do take the time to read the information below, and get involved! 4 | 5 | - [Use Go-Streamdeck From Source](#use-go-streamdeck-from-source) 6 | - [Share Issues, Bugs and Feature Requests](#share-issues-bugs-and-feature-requests) 7 | - [Submit a Patch or Pull Request](#submit-a-patch-or-pull-request) 8 | - [Try the Examples](#try-the-examples) 9 | - [Usage in Other Projects](#usage-in-other-projects) 10 | 11 | ## Use Go-Streamdeck From Source 12 | 13 | To use the `master` branch, another branch, or a branch in your own fork, you can use the `replace` syntax in your `go.mod` file. For more information and an example try this [excellent blog post from Pam Selle](https://thewebivore.com/using-replace-in-go-mod-to-point-to-your-local-module/). 14 | 15 | ## Share Issues, Bugs and Feature Requests 16 | 17 | Go ahead and open an issue, choosing whether you are making a bug report, or a feature request. If it's both, or neither, just pick one! The only difference is the template you are prompted to complete. It's fine to ask questions and support queries by opening an issue on this repository too. 18 | 19 | ## Submit a Patch or Pull Request 20 | 21 | You will need to fork the repository to offer patches. Please make sure your master branch is up to date, then start a new branch, named for the feature/bugfix it contains. 22 | 23 | For large changes, please open an issue for discussion so that we know we are not duplicating work or working on a feature that won't be accepted at the end of the process. 24 | 25 | ## Try the Examples 26 | 27 | The `examples/` directory has a series of examples that may be useful in your own applications. Try these out to see the various features in action. 28 | 29 | Please open an issue to update us for which operating systems and StreamDeck combinations are working well for you! 30 | 31 | ## Usage in Other Projects 32 | 33 | You are very welcome to use this project within the terms of the license. We would love to have you in our showcase, please open a pull request to update the `README.md` file with a link to your project. 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kevin Bowman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Streamdeck 2 | 3 | A Go interface to an Elgato Streamdeck (currently works with the 32-button XL only because that's what I have). 4 | 5 | [![GoDoc](https://godoc.org/github.com/magicmonkey/go-streamdeck?status.svg)](https://godoc.org/github.com/magicmonkey/go-streamdeck) 6 | 7 | _Designed for and tested with Ubuntu, Go 1.13+ and a Streamdeck XL. Images are the wrong size for other streamdecks; bug reports and patches are welcome!_ 8 | 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | * [Example high-level usage](#example-high-level-usage) 12 | * [Example low-level usage](#example-low-level-usage) 13 | - [Showcase](#showcase) 14 | - [Contributions](#contributions) 15 | 16 | ## Installation 17 | 18 | Either include the library in your project or install it with the following command: 19 | 20 | ``` 21 | go get github.com/magicmonkey/go-streamdeck 22 | ``` 23 | 24 | On Linux, you might also need to add some `udev` rules. Put this into `/etc/udev/rules.d/99-streamdeck.rules`: 25 | ``` 26 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", MODE:="666" 27 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", MODE:="666" 28 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", MODE:="666" 29 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006d", MODE:="666" 30 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0080", MODE:="666" 31 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0090", MODE:="666" 32 | ``` 33 | 34 | ## Usage 35 | 36 | There are 2 ways to use this: the low-level "comms-oriented" interface (using `streamdeck.Open`) which wraps the USB HID protocol, or the higher-level "button-oriented" interface (using `streamdeck.New`) which represents buttons and actions. 37 | 38 | If you want to implement your own actions, I suggest that you either instantiate a `CustomAction` or alternatively implement the `ButtonActionHandler` interface (basing your code on the `CustomAction`). 39 | 40 | ### Example high-level usage 41 | 42 | High level usage gives some helpers to set up buttons. This example has a few things to look at: 43 | 44 | * A button in position 2 that says "Hi world" and prints to the console when pressed 45 | 46 | * A button in position 7 displaying the number 7 - changes to number 8 when pressed. 47 | 48 | * A yellow button in position 26 49 | 50 | * A purple button in position 27, it changes colour _and_ prints to the console when pressed. 51 | 52 | ```go 53 | import ( 54 | "image/color" 55 | "time" 56 | 57 | streamdeck "github.com/magicmonkey/go-streamdeck" 58 | "github.com/magicmonkey/go-streamdeck/actionhandlers" 59 | "github.com/magicmonkey/go-streamdeck/buttons" 60 | _ "github.com/magicmonkey/go-streamdeck/devices" 61 | ) 62 | 63 | func main() { 64 | sd, err := streamdeck.New() 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | // A simple yellow button in position 26 70 | cButton := buttons.NewColourButton(color.RGBA{255, 255, 0, 255}) 71 | sd.AddButton(26, cButton) 72 | 73 | // A button with text on it in position 2, which echoes to the console when presesd 74 | myButton := buttons.NewTextButton("Hi world") 75 | myButton.SetActionHandler(&actionhandlers.TextPrintAction{Label: "You pressed me"}) 76 | sd.AddButton(2, myButton) 77 | 78 | // A button with text on it which changes when pressed 79 | myNextButton := buttons.NewTextButton("7") 80 | myNextButton.SetActionHandler(&actionhandlers.TextLabelChangeAction{NewLabel: "8"}) 81 | sd.AddButton(7, myNextButton) 82 | 83 | // A button which performs multiple actions when pressed 84 | multiActionButton := buttons.NewColourButton(color.RGBA{255, 0, 255, 255}) 85 | thisActionHandler := &actionhandlers.ChainedAction{} 86 | thisActionHandler.AddAction(&actionhandlers.TextPrintAction{Label: "Purple press"}) 87 | thisActionHandler.AddAction(&actionhandlers.ColourChangeAction{NewColour: color.RGBA{255, 0, 0, 255}}) 88 | multiActionButton.SetActionHandler(thisActionHandler) 89 | sd.AddButton(27, multiActionButton) 90 | 91 | time.Sleep(20 * time.Second) 92 | } 93 | ``` 94 | 95 | The program runs for 20 seconds and then exits. 96 | 97 | ### Example low-level usage 98 | 99 | The low-level usage gives more control over the operations of the streamdeck and buttons. 100 | 101 | This example shows an image on any pressed button, updating each time another button is pressed. 102 | 103 | ```go 104 | import streamdeck "github.com/magicmonkey/go-streamdeck" 105 | 106 | func main() { 107 | sd, err := streamdeck.Open() 108 | if err != nil { 109 | panic(err) 110 | } 111 | sd.ClearButtons() 112 | 113 | sd.SetBrightness(50) 114 | 115 | sd.ButtonPress(func(btnIndex int, sd *streamdeck.Device, err error) { 116 | if err != nil { 117 | panic(err) 118 | } 119 | sd.ClearButtons() 120 | sd.WriteImageToButton("play.jpg", btnIndex) 121 | }) 122 | 123 | time.Sleep(20 * time.Second) 124 | 125 | } 126 | ``` 127 | 128 | The program runs for 20 seconds and then exits. 129 | 130 | ## Showcase 131 | 132 | Projects using this library (pull request to add yours!) 133 | 134 | * [Streamdeck tricks](https://github.com/lornajane/streamdeck-tricks) 135 | 136 | ## Contributions 137 | 138 | This is a very new project but all feedback, comments, questions and patches are more than welcome. Please get in touch by opening an issue, it would be good to hear who is using the project and how things are going. 139 | 140 | For more, see [CONTRIBUTING.md](CONTRIBUTING.md). 141 | -------------------------------------------------------------------------------- /actionhandlers/chained.go: -------------------------------------------------------------------------------- 1 | package actionhandlers 2 | 3 | import streamdeck "github.com/magicmonkey/go-streamdeck" 4 | 5 | type ChainedAction struct { 6 | actions []streamdeck.ButtonActionHandler 7 | } 8 | 9 | func (act *ChainedAction) AddAction(newaction streamdeck.ButtonActionHandler) { 10 | act.actions = append(act.actions, newaction) 11 | } 12 | 13 | func (act *ChainedAction) Pressed(btn streamdeck.Button) { 14 | for _, a := range act.actions { 15 | a.Pressed(btn) 16 | } 17 | } 18 | 19 | func NewEmptyChainedAction() *ChainedAction { 20 | return &ChainedAction{} 21 | } 22 | 23 | func NewChainedAction(actions []streamdeck.ButtonActionHandler) *ChainedAction { 24 | return &ChainedAction{actions: actions} 25 | } 26 | -------------------------------------------------------------------------------- /actionhandlers/colourchange.go: -------------------------------------------------------------------------------- 1 | package actionhandlers 2 | 3 | import ( 4 | "image/color" 5 | 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | "github.com/magicmonkey/go-streamdeck/buttons" 8 | ) 9 | 10 | type ColourChangeAction struct { 11 | NewColour color.Color 12 | } 13 | 14 | func (action *ColourChangeAction) Pressed(btn streamdeck.Button) { 15 | mybtn := btn.(*buttons.ColourButton) 16 | mybtn.SetColour(action.NewColour) 17 | } 18 | 19 | func NewColourChangeAction(newColour color.Color) *ColourChangeAction { 20 | return &ColourChangeAction{NewColour: newColour} 21 | } 22 | -------------------------------------------------------------------------------- /actionhandlers/custom.go: -------------------------------------------------------------------------------- 1 | package actionhandlers 2 | 3 | import streamdeck "github.com/magicmonkey/go-streamdeck" 4 | 5 | type CustomAction struct { 6 | handler func(streamdeck.Button) 7 | } 8 | 9 | func (action *CustomAction) SetHandler(f func(streamdeck.Button)) { 10 | action.handler = f 11 | } 12 | 13 | func (action *CustomAction) Pressed(btn streamdeck.Button) { 14 | action.handler(btn) 15 | } 16 | 17 | func NewEmptyCustomAction() *CustomAction { 18 | return &CustomAction{} 19 | } 20 | 21 | func NewCustomAction(handler func(streamdeck.Button)) *CustomAction { 22 | return &CustomAction{handler: handler} 23 | } 24 | -------------------------------------------------------------------------------- /actionhandlers/exec.go: -------------------------------------------------------------------------------- 1 | package actionhandlers 2 | 3 | import ( 4 | streamdeck "github.com/magicmonkey/go-streamdeck" 5 | "os/exec" 6 | ) 7 | 8 | type ExecAction struct { 9 | Command *exec.Cmd 10 | } 11 | 12 | func (action *ExecAction) Pressed(btn streamdeck.Button) { 13 | action.Command.Start() 14 | } 15 | 16 | func NewExecAction(command *exec.Cmd) *ExecAction { 17 | return &ExecAction{Command: command} 18 | } 19 | -------------------------------------------------------------------------------- /actionhandlers/numberprint.go: -------------------------------------------------------------------------------- 1 | package actionhandlers 2 | 3 | import ( 4 | "fmt" 5 | streamdeck "github.com/magicmonkey/go-streamdeck" 6 | ) 7 | 8 | type NumberPrintAction struct { 9 | Number int 10 | } 11 | 12 | func (npa *NumberPrintAction) Pressed(btn streamdeck.Button) { 13 | fmt.Println(npa.Number) 14 | } 15 | 16 | func NewNumberPrintAction(number int) *NumberPrintAction { 17 | return &NumberPrintAction{Number: number} 18 | } 19 | -------------------------------------------------------------------------------- /actionhandlers/textlabel.go: -------------------------------------------------------------------------------- 1 | package actionhandlers 2 | 3 | import ( 4 | streamdeck "github.com/magicmonkey/go-streamdeck" 5 | "github.com/magicmonkey/go-streamdeck/buttons" 6 | ) 7 | 8 | type TextLabelChangeAction struct { 9 | NewLabel string 10 | } 11 | 12 | func (action *TextLabelChangeAction) Pressed(btn streamdeck.Button) { 13 | mybtn := btn.(*buttons.TextButton) 14 | mybtn.SetText(action.NewLabel) 15 | } 16 | 17 | func NewTextLabelChangeAction(newLabel string) *TextLabelChangeAction { 18 | return &TextLabelChangeAction{NewLabel: newLabel} 19 | } 20 | -------------------------------------------------------------------------------- /actionhandlers/textprint.go: -------------------------------------------------------------------------------- 1 | package actionhandlers 2 | 3 | import ( 4 | "fmt" 5 | streamdeck "github.com/magicmonkey/go-streamdeck" 6 | ) 7 | 8 | type TextPrintAction struct { 9 | Label string 10 | } 11 | 12 | func (action *TextPrintAction) Pressed(btn streamdeck.Button) { 13 | fmt.Println(action.Label) 14 | fmt.Print("The button pressed is: ") 15 | fmt.Println(btn) 16 | } 17 | 18 | func NewTextPrintAction(label string) *TextPrintAction { 19 | return &TextPrintAction{Label: label} 20 | } 21 | -------------------------------------------------------------------------------- /buttons/colour.go: -------------------------------------------------------------------------------- 1 | package buttons 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | 8 | streamdeck "github.com/magicmonkey/go-streamdeck" 9 | ) 10 | 11 | // ColourButton represents a button which is a solid block of a single colour 12 | type ColourButton struct { 13 | colour color.Color 14 | updateHandler func(streamdeck.Button) 15 | btnIndex int 16 | actionHandler streamdeck.ButtonActionHandler 17 | } 18 | 19 | // GetImageForButton is the interface implemention to get the button's image as an image.Image 20 | func (btn *ColourButton) GetImageForButton(btnSize int) image.Image { 21 | img := image.NewRGBA(image.Rect(0, 0, btnSize, btnSize)) 22 | //colour := color.RGBA{red, green, blue, 0} 23 | draw.Draw(img, img.Bounds(), image.NewUniform(btn.colour), image.Point{0, 0}, draw.Src) 24 | return img 25 | } 26 | 27 | // SetButtonIndex is the interface implemention to set which button on the Streamdeck this is 28 | func (btn *ColourButton) SetButtonIndex(btnIndex int) { 29 | btn.btnIndex = btnIndex 30 | } 31 | 32 | // GetButtonIndex is the interface implemention to get which button on the Streamdeck this is 33 | func (btn *ColourButton) GetButtonIndex() int { 34 | return btn.btnIndex 35 | } 36 | 37 | // SetColour allows the colour for the button to be changed on the fly 38 | func (btn *ColourButton) SetColour(colour color.Color) { 39 | btn.colour = colour 40 | btn.updateHandler(btn) 41 | } 42 | 43 | // RegisterUpdateHandler is the interface implemention to let the engine give this button a callback to 44 | // use to request that the button image is updated on the Streamdeck. 45 | func (btn *ColourButton) RegisterUpdateHandler(f func(streamdeck.Button)) { 46 | btn.updateHandler = f 47 | } 48 | 49 | // SetActionHandler allows a ButtonActionHandler implementation to be 50 | // set on this button, so that something can happen when the button is pressed. 51 | func (btn *ColourButton) SetActionHandler(a streamdeck.ButtonActionHandler) { 52 | btn.actionHandler = a 53 | } 54 | 55 | // Pressed is the interface implementation for letting the engine notify that the button has been 56 | // pressed. This hands-off to the specified ButtonActionHandler if it has been set. 57 | func (btn *ColourButton) Pressed() { 58 | if btn.actionHandler != nil { 59 | btn.actionHandler.Pressed(btn) 60 | } 61 | } 62 | 63 | // NewColourButton creates a new ColourButton of the specified colour 64 | func NewColourButton(colour color.Color) *ColourButton { 65 | btn := &ColourButton{colour: colour} 66 | return btn 67 | } 68 | -------------------------------------------------------------------------------- /buttons/imagefile.go: -------------------------------------------------------------------------------- 1 | package buttons 2 | 3 | import ( 4 | "github.com/disintegration/gift" 5 | "image" 6 | "image/draw" 7 | "os" 8 | 9 | streamdeck "github.com/magicmonkey/go-streamdeck" 10 | ) 11 | 12 | // ImageFileButton represents a button with an image on it, where the image is loaded 13 | // from a file. 14 | type ImageFileButton struct { 15 | filePath string 16 | img image.Image 17 | updateHandler func(streamdeck.Button) 18 | btnIndex int 19 | actionHandler streamdeck.ButtonActionHandler 20 | } 21 | 22 | // GetImageForButton is the interface implemention to get the button's image as an image.Image 23 | func (btn *ImageFileButton) GetImageForButton(btnSize int) image.Image { 24 | // Resize the image to what the button wants 25 | g := gift.New(gift.Resize(btnSize, btnSize, gift.LanczosResampling)) 26 | newimg := image.NewRGBA(image.Rect(0, 0, btnSize, btnSize)) 27 | g.Draw(newimg, btn.img) 28 | return newimg 29 | } 30 | 31 | // SetButtonIndex is the interface implemention to set which button on the Streamdeck this is 32 | func (btn *ImageFileButton) SetButtonIndex(btnIndex int) { 33 | btn.btnIndex = btnIndex 34 | } 35 | 36 | // GetButtonIndex is the interface implemention to get which button on the Streamdeck this is 37 | func (btn *ImageFileButton) GetButtonIndex() int { 38 | return btn.btnIndex 39 | } 40 | 41 | // SetFilePath allows the image file to be changed on the fly 42 | func (btn *ImageFileButton) SetFilePath(filePath string) error { 43 | btn.filePath = filePath 44 | err := btn.loadImage() 45 | if err != nil { 46 | return err 47 | } 48 | btn.updateHandler(btn) 49 | return nil 50 | } 51 | 52 | func (btn *ImageFileButton) loadImage() error { 53 | f, err := os.Open(btn.filePath) 54 | if err != nil { 55 | return err 56 | } 57 | img, _, err := image.Decode(f) 58 | 59 | // We want the image as an RGBA, so convert it if it isn't 60 | var newimg *image.RGBA 61 | newimg, ok := img.(*image.RGBA) 62 | if !ok { 63 | newimg = image.NewRGBA(image.Rect(0, 0, img.Bounds().Max.X, img.Bounds().Max.Y)) 64 | draw.Draw(newimg, newimg.Bounds(), img, image.Point{0, 0}, draw.Src) 65 | } 66 | 67 | if err != nil { 68 | return err 69 | } 70 | btn.img = newimg 71 | return nil 72 | } 73 | 74 | // RegisterUpdateHandler is the interface implemention to let the engine give this button a callback to 75 | // use to request that the button image is updated on the Streamdeck. 76 | func (btn *ImageFileButton) RegisterUpdateHandler(f func(streamdeck.Button)) { 77 | btn.updateHandler = f 78 | } 79 | 80 | // SetActionHandler allows a ButtonActionHandler implementation to be 81 | // set on this button, so that something can happen when the button is pressed. 82 | func (btn *ImageFileButton) SetActionHandler(a streamdeck.ButtonActionHandler) { 83 | btn.actionHandler = a 84 | } 85 | 86 | // Pressed is the interface implementation for letting the engine notify that the button has been 87 | // pressed. This hands-off to the specified ButtonActionHandler if it has been set. 88 | func (btn *ImageFileButton) Pressed() { 89 | if btn.actionHandler != nil { 90 | btn.actionHandler.Pressed(btn) 91 | } 92 | } 93 | 94 | // NewImageFileButton creates a new ImageFileButton with the specified image on it 95 | func NewImageFileButton(filePath string) (*ImageFileButton, error) { 96 | btn := &ImageFileButton{filePath: filePath} 97 | err := btn.loadImage() 98 | if err != nil { 99 | return nil, err 100 | } 101 | return btn, nil 102 | } 103 | -------------------------------------------------------------------------------- /buttons/text.go: -------------------------------------------------------------------------------- 1 | package buttons 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | 8 | "golang.org/x/image/font/gofont/gomedium" 9 | 10 | "github.com/golang/freetype" 11 | "github.com/golang/freetype/truetype" 12 | streamdeck "github.com/magicmonkey/go-streamdeck" 13 | ) 14 | 15 | // TextButton represents a button with text on it 16 | type TextButton struct { 17 | label string 18 | textColour color.Color 19 | backgroundColour color.Color 20 | updateHandler func(streamdeck.Button) 21 | btnIndex int 22 | actionHandler streamdeck.ButtonActionHandler 23 | } 24 | 25 | // GetImageForButton is the interface implemention to get the button's image as an image.Image 26 | func (btn *TextButton) GetImageForButton(btnSize int) image.Image { 27 | img := getImageWithText(btn.label, btn.textColour, btn.backgroundColour, btnSize) 28 | return img 29 | } 30 | 31 | // SetButtonIndex is the interface implemention to set which button on the Streamdeck this is 32 | func (btn *TextButton) SetButtonIndex(btnIndex int) { 33 | btn.btnIndex = btnIndex 34 | } 35 | 36 | // GetButtonIndex is the interface implemention to get which button on the Streamdeck this is 37 | func (btn *TextButton) GetButtonIndex() int { 38 | return btn.btnIndex 39 | } 40 | 41 | // SetText allows the text on the button to be changed on the fly 42 | func (btn *TextButton) SetText(label string) { 43 | btn.label = label 44 | btn.updateHandler(btn) 45 | } 46 | 47 | // SetTextColour allows the colour of the text on the button to be changed on the fly 48 | func (btn *TextButton) SetTextColour(textColour color.Color) { 49 | btn.textColour = textColour 50 | btn.updateHandler(btn) 51 | } 52 | 53 | // SetBackgroundColor allows the background colour on the button to be changed on the fly 54 | func (btn *TextButton) SetBackgroundColor(backgroundColour color.Color) { 55 | btn.backgroundColour = backgroundColour 56 | btn.updateHandler(btn) 57 | } 58 | 59 | // RegisterUpdateHandler is the interface implemention to let the engine give this button a callback to 60 | // use to request that the button image is updated on the Streamdeck. 61 | func (btn *TextButton) RegisterUpdateHandler(f func(streamdeck.Button)) { 62 | btn.updateHandler = f 63 | } 64 | 65 | // SetActionHandler allows a ButtonActionHandler implementation to be 66 | // set on this button, so that something can happen when the button is pressed. 67 | func (btn *TextButton) SetActionHandler(a streamdeck.ButtonActionHandler) { 68 | btn.actionHandler = a 69 | } 70 | 71 | // Pressed is the interface implementation for letting the engine notify that the button has been 72 | // pressed. This hands-off to the specified ButtonActionHandler if it has been set. 73 | func (btn *TextButton) Pressed() { 74 | if btn.actionHandler != nil { 75 | btn.actionHandler.Pressed(btn) 76 | } 77 | } 78 | 79 | // NewTextButton creates a new TextButton with the specified text on it, in white on a black 80 | // background. The text will be set on a single line, and auto-sized to fill the button as best 81 | // as possible. 82 | func NewTextButton(label string) *TextButton { 83 | btn := NewTextButtonWithColours(label, color.White, color.Black) 84 | return btn 85 | } 86 | 87 | // NewTextButtonWithColours creates a new TextButton with the specified text on it, in the specified 88 | // text and background colours. The text will be set on a single line, and auto-sized to fill the 89 | // button as best as possible. 90 | func NewTextButtonWithColours(label string, textColour color.Color, backgroundColour color.Color) *TextButton { 91 | btn := &TextButton{label: label, textColour: textColour, backgroundColour: backgroundColour} 92 | return btn 93 | } 94 | 95 | func getImageWithText(text string, textColour color.Color, backgroundColour color.Color, btnSize int) image.Image { 96 | 97 | size := float64(18) 98 | 99 | myfont, err := truetype.Parse(gomedium.TTF) 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | width := 0 105 | for size = 1; size < 60; size++ { 106 | width = getTextWidth(text, size) 107 | if width > 90 { 108 | size = size - 1 109 | break 110 | } 111 | } 112 | 113 | srcImg := image.NewUniform(textColour) 114 | 115 | dstImg := image.NewRGBA(image.Rect(0, 0, btnSize, btnSize)) 116 | draw.Draw(dstImg, dstImg.Bounds(), image.NewUniform(backgroundColour), image.Point{0, 0}, draw.Src) 117 | 118 | c := freetype.NewContext() 119 | c.SetFont(myfont) 120 | c.SetDst(dstImg) 121 | c.SetSrc(srcImg) 122 | c.SetFontSize(size) 123 | c.SetClip(dstImg.Bounds()) 124 | 125 | x := int((btnSize - width) / 2) // Horizontally centre text 126 | y := int(50 + (size / 3)) // Fudged vertical centre, erm, very "heuristic" 127 | 128 | pt := freetype.Pt(x, y) 129 | c.DrawString(text, pt) 130 | 131 | /* 132 | textWidth := 7 * len(text) 133 | fmt.Println(textWidth) 134 | 135 | f := &font.Drawer{ 136 | Dst: dstImg, 137 | Src: src_img, 138 | Face: basicfont.Face7x13, 139 | Dot: fixed.Point26_6{fixed.Int26_6(x * 64), fixed.Int26_6(y * 64)}, 140 | } 141 | f.DrawString(text) 142 | */ 143 | return dstImg 144 | } 145 | 146 | func getTextWidth(text string, size float64) int { 147 | 148 | myfont, err := truetype.Parse(gomedium.TTF) 149 | if err != nil { 150 | panic(err) 151 | } 152 | 153 | // Calculate width of string 154 | width := 0 155 | face := truetype.NewFace(myfont, &truetype.Options{Size: size}) 156 | for _, x := range text { 157 | awidth, _ := face.GlyphAdvance(rune(x)) 158 | iwidthf := int(float64(awidth) / 64) 159 | width += iwidthf 160 | } 161 | 162 | return width 163 | } 164 | -------------------------------------------------------------------------------- /comms.go: -------------------------------------------------------------------------------- 1 | package streamdeck 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "image" 7 | "image/color" 8 | 9 | "github.com/karalabe/hid" 10 | ) 11 | 12 | const vendorID = 0x0fd9 13 | 14 | // deviceType represents one of the various types of StreamDeck (mini/orig/orig2/xl) 15 | type deviceType struct { 16 | name string 17 | imageSize image.Point 18 | usbProductID uint16 19 | resetPacket []byte 20 | numberOfButtons uint 21 | buttonRows uint 22 | buttonCols uint 23 | brightnessPacket []byte 24 | buttonReadOffset uint 25 | imageFormat string 26 | imagePayloadPerPage uint 27 | imageHeaderFunc func(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte 28 | } 29 | 30 | var deviceTypes []deviceType 31 | 32 | // RegisterDevicetype allows the declaration of a new type of device, intended for use by subpackage "devices" 33 | func RegisterDevicetype( 34 | name string, 35 | imageSize image.Point, 36 | usbProductID uint16, 37 | resetPacket []byte, 38 | numberOfButtons uint, 39 | buttonRows uint, 40 | buttonCols uint, 41 | brightnessPacket []byte, 42 | buttonReadOffset uint, 43 | imageFormat string, 44 | imagePayloadPerPage uint, 45 | imageHeaderFunc func(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte, 46 | ) { 47 | d := deviceType{ 48 | name: name, 49 | imageSize: imageSize, 50 | usbProductID: usbProductID, 51 | resetPacket: resetPacket, 52 | numberOfButtons: numberOfButtons, 53 | buttonRows: buttonRows, 54 | buttonCols: buttonCols, 55 | brightnessPacket: brightnessPacket, 56 | buttonReadOffset: buttonReadOffset, 57 | imageFormat: imageFormat, 58 | imagePayloadPerPage: imagePayloadPerPage, 59 | imageHeaderFunc: imageHeaderFunc, 60 | } 61 | deviceTypes = append(deviceTypes, d) 62 | } 63 | 64 | // Device is a struct which represents an actual Streamdeck device, and holds its reference to the USB HID device 65 | type Device struct { 66 | fd *hid.Device 67 | deviceType deviceType 68 | buttonPressListeners []func(int, *Device, error) 69 | } 70 | 71 | // Open a Streamdeck device, the most common entry point 72 | func Open() (*Device, error) { 73 | return rawOpen(true) 74 | } 75 | 76 | // OpenWithoutReset will open a Streamdeck device, without resetting it 77 | func OpenWithoutReset() (*Device, error) { 78 | return rawOpen(false) 79 | } 80 | 81 | // Opens a new StreamdeckXL device, and returns a handle 82 | func rawOpen(reset bool) (*Device, error) { 83 | devices := hid.Enumerate(vendorID, 0) 84 | if len(devices) == 0 { 85 | return nil, errors.New("No elgato devices found") 86 | } 87 | 88 | retval := &Device{} 89 | for _, device := range devices { 90 | // Iterate over the known device types, matching to product ID 91 | for _, devType := range deviceTypes { 92 | if device.ProductID == devType.usbProductID { 93 | retval.deviceType = devType 94 | dev, err := device.Open() 95 | if err != nil { 96 | return nil, err 97 | } 98 | retval.fd = dev 99 | if reset { 100 | retval.ResetComms() 101 | } 102 | go retval.buttonPressListener() 103 | return retval, nil 104 | } 105 | } 106 | } 107 | return nil, errors.New("Found an Elgato device, but not one for which there is a definition; have you imported the devices package?") 108 | } 109 | 110 | // GetName returns the name of the type of Streamdeck 111 | func (d *Device) GetName() string { 112 | return d.deviceType.name 113 | } 114 | 115 | // Close the device 116 | func (d *Device) Close() { 117 | d.fd.Close() 118 | } 119 | 120 | // SetBrightness sets the button brightness 121 | // pct is an integer between 0-100 122 | func (d *Device) SetBrightness(pct int) error { 123 | if pct < 0 { 124 | pct = 0 125 | } 126 | if pct > 100 { 127 | pct = 100 128 | } 129 | 130 | preamble := d.deviceType.brightnessPacket 131 | payload := append(preamble, byte(pct)) 132 | _, err := d.fd.SendFeatureReport(payload) 133 | if err != nil { 134 | return err 135 | } 136 | return nil 137 | } 138 | 139 | // GetButtonImageSize returns the size of the images to uploaded to the buttons 140 | func (d* Device) GetButtonImageSize() image.Point { 141 | return d.deviceType.imageSize 142 | } 143 | 144 | // GetNumButtonsOnDevice returns the number of button this device has 145 | func (d* Device) GetNumButtonsOnDevice() uint { 146 | return d.deviceType.numberOfButtons 147 | } 148 | 149 | // ClearButtons writes a black square to all buttons 150 | func (d *Device) ClearButtons() error { 151 | numButtons := int(d.deviceType.numberOfButtons) 152 | for i := 0; i < numButtons; i++ { 153 | err := d.WriteColorToButton(i, color.Black) 154 | if err != nil { 155 | return err 156 | } 157 | } 158 | return nil 159 | } 160 | 161 | // WriteColorToButton writes a specified color to the given button 162 | func (d *Device) WriteColorToButton(btnIndex int, colour color.Color) error { 163 | img := getSolidColourImage(colour, d.deviceType.imageSize.X) 164 | imgForButton, err := getImageForButton(img, d.deviceType.imageFormat) 165 | if err != nil { 166 | return err 167 | } 168 | return d.rawWriteToButton(btnIndex, imgForButton) 169 | } 170 | 171 | // WriteImageToButton writes a specified image file to the given button 172 | func (d *Device) WriteImageToButton(btnIndex int, filename string) error { 173 | img, err := getImageFile(filename) 174 | if err != nil { 175 | return err 176 | } 177 | err = d.WriteRawImageToButton(btnIndex, img) 178 | if err != nil { 179 | return err 180 | } 181 | return nil 182 | } 183 | 184 | func (d *Device) buttonPressListener() { 185 | var buttonMask []bool 186 | buttonMask = make([]bool, d.deviceType.numberOfButtons) 187 | for { 188 | data := make([]byte, d.deviceType.numberOfButtons+d.deviceType.buttonReadOffset) 189 | _, err := d.fd.Read(data) 190 | if err != nil { 191 | d.sendButtonPressEvent(-1, err) 192 | break 193 | } 194 | for i := uint(0); i < d.deviceType.numberOfButtons; i++ { 195 | if data[d.deviceType.buttonReadOffset+i] == 1 { 196 | if !buttonMask[i] { 197 | d.sendButtonPressEvent(int(i), nil) 198 | } 199 | buttonMask[i] = true 200 | } else { 201 | buttonMask[i] = false 202 | } 203 | } 204 | } 205 | } 206 | 207 | func (d *Device) sendButtonPressEvent(btnIndex int, err error) { 208 | for _, f := range d.buttonPressListeners { 209 | f(btnIndex, d, err) 210 | } 211 | } 212 | 213 | // ButtonPress registers a callback to be called whenever a button is pressed 214 | func (d *Device) ButtonPress(f func(int, *Device, error)) { 215 | d.buttonPressListeners = append(d.buttonPressListeners, f) 216 | } 217 | 218 | // ResetComms will reset the comms protocol to the StreamDeck; useful if things have gotten de-synced, but it will also reboot the StreamDeck 219 | func (d *Device) ResetComms() error { 220 | payload := d.deviceType.resetPacket 221 | _, err := d.fd.SendFeatureReport(payload) 222 | return err 223 | } 224 | 225 | // WriteRawImageToButton takes an `image.Image` and writes it to the given button, after resizing and rotating the image to fit the button (for some reason the StreamDeck screens are all upside down) 226 | func (d *Device) WriteRawImageToButton(btnIndex int, rawImg image.Image) error { 227 | img := resizeAndRotate(rawImg, d.deviceType.imageSize.X, d.deviceType.imageSize.Y, d.deviceType.name) 228 | imgForButton, err := getImageForButton(img, d.deviceType.imageFormat) 229 | if err != nil { 230 | return err 231 | } 232 | return d.rawWriteToButton(btnIndex, imgForButton) 233 | } 234 | 235 | func (d *Device) rawWriteToButton(btnIndex int, rawImage []byte) error { 236 | // Based on set_key_image from https://github.com/abcminiuser/python-elgato-streamdeck/blob/master/src/StreamDeck/Devices/StreamDeckXL.py#L151 237 | 238 | if Min(Max(btnIndex, 0), int(d.deviceType.numberOfButtons)) != btnIndex { 239 | return errors.New(fmt.Sprintf("Invalid key index: %d", btnIndex)) 240 | } 241 | 242 | pageNumber := 0 243 | bytesRemaining := len(rawImage) 244 | halfImage := len(rawImage) / 2 245 | bytesSent := 0 246 | 247 | for bytesRemaining > 0 { 248 | 249 | header := d.deviceType.imageHeaderFunc(uint(bytesRemaining), uint(btnIndex), uint(pageNumber)) 250 | imageReportLength := int(d.deviceType.imagePayloadPerPage) 251 | imageReportHeaderLength := len(header) 252 | imageReportPayloadLength := imageReportLength - imageReportHeaderLength 253 | 254 | /* 255 | if halfImage > imageReportPayloadLength { 256 | log.Fatalf("image too large: %d", halfImage*2) 257 | } 258 | */ 259 | 260 | thisLength := 0 261 | if imageReportPayloadLength < bytesRemaining { 262 | if d.deviceType.name == "Stream Deck Original" { 263 | thisLength = halfImage 264 | } else { 265 | thisLength = imageReportPayloadLength 266 | } 267 | } else { 268 | thisLength = bytesRemaining 269 | } 270 | 271 | payload := append(header, rawImage[bytesSent:(bytesSent+thisLength)]...) 272 | padding := make([]byte, imageReportLength-len(payload)) 273 | 274 | thingToSend := append(payload, padding...) 275 | _, err := d.fd.Write(thingToSend) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | bytesRemaining = bytesRemaining - thisLength 281 | pageNumber = pageNumber + 1 282 | bytesSent = bytesSent + thisLength 283 | } 284 | return nil 285 | } 286 | 287 | // Golang Min/Max 288 | func Min(x, y int) int { 289 | if x < y { 290 | return x 291 | } 292 | return y 293 | } 294 | 295 | func Max(x, y int) int { 296 | if x > y { 297 | return x 298 | } 299 | return y 300 | } 301 | -------------------------------------------------------------------------------- /decorators/border.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | type Border struct { 9 | width int 10 | colour color.Color 11 | } 12 | 13 | func NewBorder(width int, colour color.Color) *Border { 14 | b := &Border{width: width, colour: colour} 15 | return b 16 | } 17 | 18 | func (b *Border) Apply(img image.Image, size int) image.Image { 19 | newimg := img.(*image.RGBA) 20 | // TODO base the 96 on the image bounds 21 | for i := 0; i < b.width; i++ { 22 | rect(i, i, size-i, size-i, newimg, b.colour) 23 | } 24 | return newimg 25 | } 26 | 27 | // Utility functions from https://stackoverflow.com/questions/28992396/draw-a-rectangle-in-golang 28 | 29 | func hLine(x1, y, x2 int, img *image.RGBA, colour color.Color) { 30 | for ; x1 <= x2; x1++ { 31 | img.Set(x1, y, colour) 32 | } 33 | } 34 | 35 | func vLine(x, y1, y2 int, img *image.RGBA, colour color.Color) { 36 | for ; y1 <= y2; y1++ { 37 | img.Set(x, y1, colour) 38 | } 39 | } 40 | 41 | func rect(x1, y1, x2, y2 int, img *image.RGBA, colour color.Color) { 42 | hLine(x1, y1, x2, img, colour) 43 | hLine(x1, y2, x2, img, colour) 44 | vLine(x1, y1, y2, img, colour) 45 | vLine(x2, y1, y2, img, colour) 46 | } 47 | -------------------------------------------------------------------------------- /devices/mini.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | import ( 4 | "image" 5 | 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | ) 8 | 9 | var ( 10 | miniName string 11 | miniButtonWidth uint 12 | miniButtonHeight uint 13 | miniImageReportPayloadLength uint 14 | miniImageReportHeaderLength uint 15 | miniImageReportLength uint 16 | ) 17 | 18 | // GetImageHeaderMini returns the USB comms header for a button image for the XL 19 | func GetImageHeaderMini(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte { 20 | var thisLength uint 21 | if miniImageReportPayloadLength < bytesRemaining { 22 | thisLength = miniImageReportPayloadLength 23 | } else { 24 | thisLength = bytesRemaining 25 | } 26 | header := []byte{ 27 | '\x02', 28 | '\x01', 29 | byte(pageNumber), 30 | 0, 31 | get_header_element(thisLength, bytesRemaining), 32 | byte(btnIndex + 1), 33 | '\x00', 34 | '\x00', 35 | '\x00', 36 | '\x00', 37 | '\x00', 38 | '\x00', 39 | '\x00', 40 | '\x00', 41 | '\x00', 42 | '\x00', 43 | } 44 | 45 | return header 46 | } 47 | 48 | func get_header_element(thisLength, bytesRemaining uint) byte { 49 | if thisLength == bytesRemaining { 50 | return '\x01' 51 | } else { 52 | return '\x00' 53 | } 54 | } 55 | 56 | func init() { 57 | miniName = "Streamdeck Mini" 58 | miniButtonWidth = 80 59 | miniButtonHeight = 80 60 | miniImageReportPayloadLength = 1024 61 | streamdeck.RegisterDevicetype( 62 | miniName, // Name 63 | image.Point{X: int(miniButtonWidth), Y: int(miniButtonHeight)}, // Width/height of a button 64 | 0x63, // USB productID 65 | resetPacket17(), // Reset packet 66 | 6, // Number of buttons 67 | 2, // Number of rows 68 | 3, // Number of cols 69 | brightnessPacket17(), // Brightness packet 70 | 1, // Button read offset 71 | "BMP", // Image format 72 | miniImageReportPayloadLength, // Amount of image payload allowed per USB packet 73 | GetImageHeaderMini, // Function to get the comms image header 74 | ) 75 | streamdeck.RegisterDevicetype( 76 | miniName, // Name 77 | image.Point{X: int(miniButtonWidth), Y: int(miniButtonHeight)}, // Width/height of a button 78 | 0x90, // USB productID 79 | resetPacket17(), // Reset packet 80 | 6, // Number of buttons 81 | 2, // Number of rows 82 | 3, // Number of cols 83 | brightnessPacket17(), // Brightness packet 84 | 1, // Button read offset 85 | "BMP", // Image format 86 | miniImageReportPayloadLength, // Amount of image payload allowed per USB packet 87 | GetImageHeaderMini, // Function to get the comms image header 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /devices/orig.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | import ( 4 | "image" 5 | 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | ) 8 | 9 | var ( 10 | originalName string 11 | originalButtonWidth uint 12 | originalButtonHeight uint 13 | originalImageReportPayloadLength uint 14 | ) 15 | 16 | // GetImageHeaderMini returns the USB comms header for a button image for the original 17 | func GetImageHeaderOriginal(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte { 18 | var thisLength uint 19 | if originalImageReportPayloadLength < bytesRemaining { 20 | thisLength = originalImageReportPayloadLength 21 | } else { 22 | thisLength = bytesRemaining 23 | } 24 | header := []byte{ 25 | '\x02', 26 | '\x01', 27 | byte(pageNumber + 1), 28 | 0, 29 | get_header_element(thisLength, bytesRemaining), 30 | byte(btnIndex + 1), 31 | '\x00', 32 | '\x00', 33 | '\x00', 34 | '\x00', 35 | '\x00', 36 | '\x00', 37 | '\x00', 38 | '\x00', 39 | '\x00', 40 | '\x00', 41 | } 42 | 43 | return header 44 | } 45 | 46 | func init() { 47 | originalName = "Stream Deck Original" 48 | originalButtonWidth = 72 49 | originalButtonHeight = 72 50 | originalImageReportPayloadLength = 8191 //8191 51 | streamdeck.RegisterDevicetype( 52 | originalName, // Name 53 | image.Point{X: int(originalButtonWidth), Y: int(originalButtonHeight)}, // Width/height of a button 54 | 0x60, // USB productID 55 | resetPacket17(), // Reset packet 56 | 15, // Number of buttons 57 | 3, // Number of rows 58 | 5, // Number of cols 59 | brightnessPacket17(), // Brightness packet 60 | 1, // Button read offset 61 | "BMP", // Image format 62 | originalImageReportPayloadLength, // Amount of image payload allowed per USB packet 63 | GetImageHeaderOriginal, // Function to get the comms image header 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /devices/origmk2.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | import ( 4 | "image" 5 | 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | ) 8 | 9 | var ( 10 | omk2Name string 11 | omk2ButtonWidth uint 12 | omk2ButtonHeight uint 13 | omk2ImageReportPayloadLength uint 14 | ) 15 | 16 | // GetImageHeaderOv2 returns the USB comms header for a button image for the XL 17 | func GetImageHeaderOMK2(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte { 18 | thisLength := uint(0) 19 | if ov2ImageReportPayloadLength < bytesRemaining { 20 | thisLength = ov2ImageReportPayloadLength 21 | } else { 22 | thisLength = bytesRemaining 23 | } 24 | header := []byte{'\x02', '\x07', byte(btnIndex)} 25 | if thisLength == bytesRemaining { 26 | header = append(header, '\x01') 27 | } else { 28 | header = append(header, '\x00') 29 | } 30 | 31 | header = append(header, byte(thisLength&0xff)) 32 | header = append(header, byte(thisLength>>8)) 33 | 34 | header = append(header, byte(pageNumber&0xff)) 35 | header = append(header, byte(pageNumber>>8)) 36 | 37 | return header 38 | } 39 | 40 | func init() { 41 | omk2Name = "Stream Deck MK.2" 42 | omk2ButtonWidth = 72 43 | omk2ButtonHeight = 72 44 | omk2ImageReportPayloadLength = 1024 45 | streamdeck.RegisterDevicetype( 46 | omk2Name, // Name 47 | image.Point{X: int(omk2ButtonWidth), Y: int(omk2ButtonHeight)}, // Width/height of a button 48 | 0x80, // USB productID 49 | resetPacket32(), // Reset packet 50 | 15, // Number of buttons 51 | 3, // Number of rows 52 | 5, // Number of columns 53 | brightnessPacket32(), // Set brightness packet preamble 54 | 4, // Button read offset 55 | "JPEG", // Image format 56 | omk2ImageReportPayloadLength, // Amount of image payload allowed per USB packet 57 | GetImageHeaderOMK2, // Function to get the comms image header 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /devices/origv2.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | import ( 4 | "image" 5 | 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | ) 8 | 9 | var ( 10 | ov2Name string 11 | ov2ButtonWidth uint 12 | ov2ButtonHeight uint 13 | ov2ImageReportPayloadLength uint 14 | ) 15 | 16 | // GetImageHeaderOv2 returns the USB comms header for a button image for the XL 17 | func GetImageHeaderOv2(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte { 18 | thisLength := uint(0) 19 | if ov2ImageReportPayloadLength < bytesRemaining { 20 | thisLength = ov2ImageReportPayloadLength 21 | } else { 22 | thisLength = bytesRemaining 23 | } 24 | header := []byte{'\x02', '\x07', byte(btnIndex)} 25 | if thisLength == bytesRemaining { 26 | header = append(header, '\x01') 27 | } else { 28 | header = append(header, '\x00') 29 | } 30 | 31 | header = append(header, byte(thisLength&0xff)) 32 | header = append(header, byte(thisLength>>8)) 33 | 34 | header = append(header, byte(pageNumber&0xff)) 35 | header = append(header, byte(pageNumber>>8)) 36 | 37 | return header 38 | } 39 | 40 | func init() { 41 | ov2Name = "Streamdeck (original v2)" 42 | ov2ButtonWidth = 72 43 | ov2ButtonHeight = 72 44 | ov2ImageReportPayloadLength = 1024 45 | streamdeck.RegisterDevicetype( 46 | ov2Name, // Name 47 | image.Point{X: int(ov2ButtonWidth), Y: int(ov2ButtonHeight)}, // Width/height of a button 48 | 0x6d, // USB productID 49 | resetPacket32(), // Reset packet 50 | 15, // Number of buttons 51 | 3, // Number of rows 52 | 5, // Number of columns 53 | brightnessPacket32(), // Set brightness packet preamble 54 | 4, // Button read offset 55 | "JPEG", // Image format 56 | ov2ImageReportPayloadLength, // Amount of image payload allowed per USB packet 57 | GetImageHeaderOv2, // Function to get the comms image header 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /devices/shared.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | // resetPacket17 gives the reset packet for devices which need it to be 17 bytes long 4 | func resetPacket17() []byte { 5 | pkt := make([]byte, 17) 6 | pkt[0] = 0x0b 7 | pkt[1] = 0x63 8 | return pkt 9 | } 10 | 11 | // resetPacket32 gives the reset packet for devices which need it to be 32 bytes long 12 | func resetPacket32() []byte { 13 | pkt := make([]byte, 32) 14 | pkt[0] = 0x03 15 | pkt[1] = 0x02 16 | return pkt 17 | } 18 | 19 | // brightnessPacket17 gives the brightness packet for devices which need it to be 17 bytes long 20 | func brightnessPacket17() []byte { 21 | pkt := make([]byte, 5) 22 | pkt[0] = 0x05 23 | pkt[1] = 0x55 24 | pkt[2] = 0xaa 25 | pkt[3] = 0xd1 26 | pkt[4] = 0x01 27 | return pkt 28 | } 29 | 30 | // brightnessPacket32 gives the brightness packet for devices which need it to be 32 bytes long 31 | func brightnessPacket32() []byte { 32 | pkt := make([]byte, 2) 33 | pkt[0] = 0x03 34 | pkt[1] = 0x08 35 | return pkt 36 | } 37 | -------------------------------------------------------------------------------- /devices/xl.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | import ( 4 | "image" 5 | 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | ) 8 | 9 | var ( 10 | xlName string 11 | xlButtonWidth uint 12 | xlButtonHeight uint 13 | xlImageReportPayloadLength uint 14 | ) 15 | 16 | // GetImageHeaderXl returns the USB comms header for a button image for the XL 17 | func GetImageHeaderXl(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte { 18 | thisLength := uint(0) 19 | if xlImageReportPayloadLength < bytesRemaining { 20 | thisLength = xlImageReportPayloadLength 21 | } else { 22 | thisLength = bytesRemaining 23 | } 24 | header := []byte{'\x02', '\x07', byte(btnIndex)} 25 | if thisLength == bytesRemaining { 26 | header = append(header, '\x01') 27 | } else { 28 | header = append(header, '\x00') 29 | } 30 | 31 | header = append(header, byte(thisLength&0xff)) 32 | header = append(header, byte(thisLength>>8)) 33 | 34 | header = append(header, byte(pageNumber&0xff)) 35 | header = append(header, byte(pageNumber>>8)) 36 | 37 | return header 38 | } 39 | 40 | func init() { 41 | xlName = "Streamdeck XL" 42 | xlButtonWidth = 96 43 | xlButtonHeight = 96 44 | xlImageReportPayloadLength = 1024 45 | streamdeck.RegisterDevicetype( 46 | xlName, // Name 47 | image.Point{X: int(xlButtonWidth), Y: int(xlButtonHeight)}, // Width/height of a button 48 | 0x6c, // USB productID 49 | resetPacket32(), // Reset packet 50 | 32, // Number of buttons 51 | 4, // Number of rows 52 | 8, // Number of cols 53 | brightnessPacket32(), // Set brightness packet preamble 54 | 4, // Button read offset 55 | "JPEG", // Image format 56 | xlImageReportPayloadLength, // Amount of image payload allowed per USB packet 57 | GetImageHeaderXl, // Function to get the comms image header 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /examples/brightness/brightness.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | "github.com/magicmonkey/go-streamdeck/buttons" 8 | _ "github.com/magicmonkey/go-streamdeck/devices" 9 | ) 10 | 11 | func main() { 12 | // initialise the device 13 | sd, err := streamdeck.New() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | sd.SetBrightness(40) 19 | 20 | // create buttons 21 | btn1 := buttons.NewTextButton("Brightness") 22 | sd.AddButton(1, btn1) 23 | btn2 := buttons.NewTextButton("40") 24 | sd.AddButton(2, btn2) 25 | 26 | // wait for one second 27 | time.Sleep(1 * time.Second) 28 | 29 | // set brightness 30 | sd.SetBrightness(100) 31 | btn2.SetText("100") 32 | 33 | // program exits 34 | } 35 | -------------------------------------------------------------------------------- /examples/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "time" 7 | 8 | streamdeck "github.com/magicmonkey/go-streamdeck" 9 | "github.com/magicmonkey/go-streamdeck/actionhandlers" 10 | "github.com/magicmonkey/go-streamdeck/buttons" 11 | "github.com/magicmonkey/go-streamdeck/decorators" 12 | _ "github.com/magicmonkey/go-streamdeck/devices" 13 | ) 14 | 15 | func main() { 16 | sd, err := streamdeck.New() 17 | if err != nil { 18 | panic(err) 19 | } 20 | fmt.Printf("Found device [%s]\n", sd.GetName()) 21 | 22 | // Button in position 2, changes to "Bye!" at the end of the program 23 | // When pressed, this prints "You pressed me" to the terminal 24 | myButton := buttons.NewTextButton("Hi world") 25 | myButton.SetActionHandler(&actionhandlers.TextPrintAction{Label: "You pressed me"}) 26 | sd.AddButton(2, myButton) 27 | 28 | // Button in position 3, prints "5" to the terminal when pressed 29 | myOtherButton := buttons.NewTextButton("4") 30 | myOtherButton.SetActionHandler(&actionhandlers.NumberPrintAction{Number: 5}) 31 | sd.AddButton(3, myOtherButton) 32 | 33 | // Button in position 7 (top right on Streamdeck XL), says 7 34 | // When pressed, changes to display "8" 35 | myNextButton := buttons.NewTextButton("7") 36 | myNextButton.SetActionHandler(&actionhandlers.TextLabelChangeAction{NewLabel: "8"}) 37 | sd.AddButton(7, myNextButton) 38 | 39 | // Image button, no action handler 40 | anotherButton, err := buttons.NewImageFileButton("examples/test/play.jpg") 41 | if err != nil { 42 | panic(err) 43 | } 44 | sd.AddButton(9, anotherButton) 45 | 46 | // Yellow button, no action handler but it goes to blue at the end of the program 47 | cButton := buttons.NewColourButton(color.RGBA{255, 255, 0, 255}) 48 | sd.AddButton(26, cButton) 49 | 50 | // One button, two actions (uses ChainedAction) 51 | // Purple button, prints to the console and turns red when pressed 52 | multiActionButton := buttons.NewColourButton(color.RGBA{255, 0, 255, 255}) 53 | thisActionHandler := &actionhandlers.ChainedAction{} 54 | thisActionHandler.AddAction(&actionhandlers.TextPrintAction{Label: "Purple press"}) 55 | thisActionHandler.AddAction(&actionhandlers.ColourChangeAction{NewColour: color.RGBA{255, 0, 0, 255}}) 56 | multiActionButton.SetActionHandler(thisActionHandler) 57 | sd.AddButton(27, multiActionButton) 58 | 59 | // Text button, gets a red highlight after 2 seconds, then a green 60 | // highlight after another 2 seconds 61 | decoratedButton := buttons.NewTextButton("ABC") 62 | sd.AddButton(19, decoratedButton) 63 | time.Sleep(2 * time.Second) 64 | decorator1 := decorators.NewBorder(10, color.RGBA{0, 255, 0, 255}) 65 | sd.SetDecorator(19, decorator1) 66 | time.Sleep(2 * time.Second) 67 | decorator2 := decorators.NewBorder(5, color.RGBA{255, 0, 0, 255}) 68 | sd.SetDecorator(19, decorator2) 69 | time.Sleep(2 * time.Second) 70 | 71 | // When this button says "Bye!", the program ends 72 | myButton.SetText("Bye!") 73 | // When this button goes blue, the program ends 74 | cButton.SetColour(color.RGBA{0, 255, 255, 255}) 75 | sd.UnsetDecorator(19) 76 | } 77 | -------------------------------------------------------------------------------- /examples/client2/client2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "strconv" 6 | "sync" 7 | 8 | streamdeck "github.com/magicmonkey/go-streamdeck" 9 | "github.com/magicmonkey/go-streamdeck/actionhandlers" 10 | "github.com/magicmonkey/go-streamdeck/buttons" 11 | "github.com/magicmonkey/go-streamdeck/decorators" 12 | _ "github.com/magicmonkey/go-streamdeck/devices" 13 | ) 14 | 15 | func main() { 16 | // store the currently selected button 17 | var current int 18 | 19 | // initialise the streamdeck 20 | sd, err := streamdeck.New() 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | // put a number onto each button 26 | btns := make([]*buttons.TextButton, 32) 27 | for i := 0; i < 32; i++ { 28 | btns[i] = buttons.NewTextButton(strconv.Itoa(i)) 29 | sd.AddButton(i, btns[i]) 30 | } 31 | 32 | // create some decorators for later use 33 | greenBorder := decorators.NewBorder(10, color.RGBA{0, 255, 0, 255}) 34 | redBorder := decorators.NewBorder(5, color.RGBA{255, 0, 0, 255}) 35 | 36 | // add red borders to all buttons 37 | for i := 0; i < 32; i++ { 38 | sd.SetDecorator(i, redBorder) 39 | } 40 | 41 | // add action handlers as an inline function 42 | for i := 0; i < 32; i++ { 43 | h := actionhandlers.NewCustomAction(func(btn streamdeck.Button) { 44 | sd.SetDecorator(current, redBorder) 45 | sd.SetDecorator(btn.GetButtonIndex(), greenBorder) 46 | current = btn.GetButtonIndex() 47 | }) 48 | btns[i].SetActionHandler(h) 49 | } 50 | 51 | // don't end the program, keep running 52 | var wg sync.WaitGroup 53 | wg.Add(1) 54 | wg.Wait() 55 | } 56 | -------------------------------------------------------------------------------- /examples/client3/client3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "time" 6 | streamdeck "github.com/magicmonkey/go-streamdeck" 7 | "github.com/magicmonkey/go-streamdeck/buttons" 8 | "github.com/magicmonkey/go-streamdeck/decorators" 9 | _ "github.com/magicmonkey/go-streamdeck/devices" 10 | ) 11 | 12 | func main() { 13 | // initialise the device 14 | sd, err := streamdeck.New() 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | // create a text button 20 | btn1 := buttons.NewTextButton("Hello") 21 | sd.AddButton(1, btn1) 22 | // create an image button 23 | btn2, _ := buttons.NewImageFileButton("examples/test/play.jpg") 24 | sd.AddButton(2, btn2) 25 | 26 | 27 | // set a green border on both buttons 28 | greenBorder := decorators.NewBorder(10, color.RGBA{0, 255, 0, 255}) 29 | sd.SetDecorator(1, greenBorder) 30 | sd.SetDecorator(2, greenBorder) 31 | 32 | // wait for one second 33 | time.Sleep(1 * time.Second) 34 | 35 | // remove decorators 36 | sd.UnsetDecorator(1) 37 | sd.UnsetDecorator(2) 38 | 39 | // program exits 40 | } 41 | -------------------------------------------------------------------------------- /examples/screenshot-command/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os/exec" 7 | "sync" 8 | 9 | "github.com/magicmonkey/go-streamdeck" 10 | "github.com/magicmonkey/go-streamdeck/actionhandlers" 11 | "github.com/magicmonkey/go-streamdeck/buttons" 12 | // needed to get the device definitions 13 | _ "github.com/magicmonkey/go-streamdeck/devices" 14 | ) 15 | 16 | func main() { 17 | // add a streamdeck 18 | sd, err := streamdeck.New() 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | // create a text button labelled "Pic" 24 | myButton := buttons.NewTextButton("Pic") 25 | 26 | // create a custom action with a function as the action handler 27 | shotaction := &actionhandlers.CustomAction{} 28 | shotaction.SetHandler(func(btn streamdeck.Button) { 29 | // a goroutine so that we don't wait for the command to return 30 | go takeScreenshot() 31 | }) 32 | 33 | // attach action to button 34 | myButton.SetActionHandler(shotaction) 35 | // put button in top left slot 36 | sd.AddButton(0, myButton) 37 | 38 | // now run and keep running 39 | var wg sync.WaitGroup 40 | wg.Add(1) 41 | wg.Wait() 42 | } 43 | 44 | func takeScreenshot() { 45 | fmt.Println("Taking screenshot with delay...") 46 | cmd := exec.Command("/usr/bin/gnome-screenshot", "-w", "-d", "2") 47 | stderr, _ := cmd.StderrPipe() 48 | stdout, _ := cmd.StdoutPipe() 49 | if err := cmd.Run(); err != nil { 50 | panic(err) 51 | } 52 | 53 | slurp, _ := ioutil.ReadAll(stderr) 54 | fmt.Printf("%s\n", slurp) 55 | slurp2, _ := ioutil.ReadAll(stdout) 56 | fmt.Printf("%s\n", slurp2) 57 | 58 | fmt.Println("Taken screenshot") 59 | } 60 | -------------------------------------------------------------------------------- /examples/test/play.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicmonkey/go-streamdeck/478057861949e28ac4172424ed7e73104550498c/examples/test/play.jpg -------------------------------------------------------------------------------- /examples/test/test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "sync" 7 | 8 | streamdeck "github.com/magicmonkey/go-streamdeck" 9 | _ "github.com/magicmonkey/go-streamdeck/devices" 10 | ) 11 | 12 | func main() { 13 | // connect to a streamdeck 14 | sd, err := streamdeck.Open() 15 | if err != nil { 16 | panic(err) 17 | } 18 | fmt.Printf("Found device [%s]\n", sd.GetName()) 19 | 20 | // remove all button content 21 | sd.ClearButtons() 22 | 23 | // manage brightness level 24 | sd.SetBrightness(50) 25 | 26 | // show text in increasing lengths on three buttons 27 | sd.WriteTextToButton(2, "Hi!", color.RGBA{0, 0, 0, 255}, color.RGBA{0, 255, 255, 255}) 28 | sd.WriteTextToButton(3, "Hi again!", color.RGBA{0, 0, 0, 255}, color.RGBA{0, 255, 255, 255}) 29 | sd.WriteTextToButton(4, "Hi again again!", color.RGBA{0, 0, 0, 255}, color.RGBA{0, 255, 255, 255}) 30 | 31 | // when any button is pressed, clear all buttons, and set an image on the pressed button 32 | sd.ButtonPress(func(btnIndex int, sd *streamdeck.Device, err error) { 33 | if err != nil { 34 | panic(err) 35 | } 36 | sd.ClearButtons() 37 | sd.WriteImageToButton(btnIndex, "examples/test/play.jpg") 38 | }) 39 | 40 | // keep the program running 41 | var wg sync.WaitGroup 42 | wg.Add(1) 43 | wg.Wait() 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/magicmonkey/go-streamdeck 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/disintegration/gift v1.2.1 7 | github.com/disintegration/imaging v1.6.2 // indirect 8 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 9 | github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 10 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= 2 | github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= 3 | github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= 4 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 5 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 7 | github.com/karalabe/hid v1.0.0 h1:+/CIMNXhSU/zIJgnIvBD2nKHxS/bnRHhhs9xBryLpPo= 8 | github.com/karalabe/hid v1.0.0/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8= 9 | github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 h1:AP5krei6PpUCFOp20TSmxUS4YLoLvASBcArJqM/V+DY= 10 | github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8= 11 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 12 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= 13 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 14 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 15 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package streamdeck 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | _ "image/gif" // Allow gifs to be loaded 11 | "image/jpeg" 12 | _ "image/png" // Allow pngs to be loaded 13 | "os" 14 | 15 | "github.com/disintegration/gift" 16 | "golang.org/x/image/bmp" 17 | ) 18 | 19 | func resizeAndRotate(img image.Image, width, height int, devname string) image.Image { 20 | g, _ := deviceSpecifics(devname, width, height) 21 | res := image.NewRGBA(g.Bounds(img.Bounds())) 22 | g.Draw(res, img) 23 | return res 24 | } 25 | 26 | func deviceSpecifics(devName string, width, height int) (*gift.GIFT, error) { 27 | switch devName { 28 | case "Streamdeck XL", "Streamdeck (original v2)", "Stream Deck MK.2": 29 | return gift.New( 30 | gift.Resize(width, height, gift.LanczosResampling), 31 | gift.Rotate180(), 32 | ), nil 33 | case "Streamdeck Mini": 34 | return gift.New( 35 | gift.Resize(width, height, gift.LanczosResampling), 36 | gift.Rotate90(), 37 | gift.FlipVertical(), 38 | ), nil 39 | case "Stream Deck Original": 40 | return gift.New( 41 | gift.Resize(width, height, gift.LanczosResampling), 42 | gift.Rotate180(), 43 | ), nil 44 | default: 45 | return nil, errors.New(fmt.Sprintf("Unsupported Device: %s", devName)) 46 | } 47 | } 48 | 49 | func getImageForButton(img image.Image, btnFormat string) ([]byte, error) { 50 | var b bytes.Buffer 51 | switch btnFormat { 52 | case "JPEG": 53 | jpeg.Encode(&b, img, &jpeg.Options{Quality: 100}) 54 | case "BMP": 55 | bmp.Encode(&b, img) 56 | default: 57 | return nil, errors.New("Unknown button image format: " + btnFormat) 58 | } 59 | return b.Bytes(), nil 60 | } 61 | 62 | func getSolidColourImage(colour color.Color, btnSize int) *image.RGBA { 63 | img := image.NewRGBA(image.Rect(0, 0, btnSize, btnSize)) 64 | // colour := color.RGBA{red, green, blue, 0} 65 | draw.Draw(img, img.Bounds(), image.NewUniform(colour), image.Point{0, 0}, draw.Src) 66 | return img 67 | } 68 | 69 | func getImageFile(filename string) (image.Image, error) { 70 | f, err := os.Open(filename) 71 | if err != nil { 72 | return nil, err 73 | } 74 | img, _, err := image.Decode(f) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return img, nil 79 | } 80 | -------------------------------------------------------------------------------- /streamdeck.go: -------------------------------------------------------------------------------- 1 | package streamdeck 2 | 3 | import "image" 4 | 5 | // ButtonDisplay is the interface to satisfy for displaying on a button 6 | type ButtonDisplay interface { 7 | GetImageForButton(int) image.Image 8 | GetButtonIndex() int 9 | SetButtonIndex(int) 10 | RegisterUpdateHandler(func(Button)) 11 | Pressed() 12 | } 13 | 14 | // ButtonActionHandler is the interface to satisfy for handling a button being pressed, generally via an `actionhandler` 15 | type ButtonActionHandler interface { 16 | Pressed(Button) 17 | } 18 | 19 | // Button is the interface to satisfy for being a button; currently this is a direct proxy for the `ButtonDisplay` interface as there isn't a requirement to handle being pressed 20 | type Button interface { 21 | ButtonDisplay 22 | } 23 | 24 | // ButtonDecorator represents a way to modify the button image, for example to add a highlight or an "on/off" hint 25 | type ButtonDecorator interface { 26 | Apply(image.Image, int) image.Image 27 | } 28 | 29 | // StreamDeck is the main struct to represent a StreamDeck device, and internally contains the reference to a `Device` 30 | type StreamDeck struct { 31 | dev *Device 32 | buttons map[int]Button 33 | decorators map[int]ButtonDecorator 34 | } 35 | 36 | // New will return a new instance of a `StreamDeck`, and is the main entry point for the higher-level interface. It will return an error if there is no StreamDeck plugged in. 37 | func New() (*StreamDeck, error) { 38 | sd := &StreamDeck{} 39 | d, err := Open() 40 | if err != nil { 41 | return nil, err 42 | } 43 | sd.dev = d 44 | sd.buttons = make(map[int]Button) 45 | sd.decorators = make(map[int]ButtonDecorator) 46 | sd.dev.ButtonPress(sd.pressHandler) 47 | return sd, nil 48 | } 49 | 50 | // GetName returns the name of the type of Streamdeck 51 | func (sd *StreamDeck) GetName() string { 52 | return sd.dev.deviceType.name 53 | } 54 | 55 | // AddButton adds a `Button` object to the StreamDeck at the specified index 56 | func (sd *StreamDeck) AddButton(btnIndex int, b Button) { 57 | b.RegisterUpdateHandler(sd.ButtonUpdateHandler) 58 | b.SetButtonIndex(btnIndex) 59 | sd.buttons[btnIndex] = b 60 | sd.updateButton(b) 61 | } 62 | 63 | // SetDecorator imposes a ButtonDecorator onto a given button 64 | func (sd *StreamDeck) SetDecorator(btnIndex int, d ButtonDecorator) { 65 | sd.decorators[btnIndex] = d 66 | // If there's a button there, update it 67 | btn, ok := sd.buttons[btnIndex] 68 | if ok { 69 | sd.updateButton(btn) 70 | } 71 | } 72 | 73 | // UnsetDecorator removes a ButtonDecorator from a given button 74 | func (sd *StreamDeck) UnsetDecorator(btnIndex int) { 75 | delete(sd.decorators, btnIndex) 76 | // If there's a button there, update it 77 | btn, ok := sd.buttons[btnIndex] 78 | if ok { 79 | sd.updateButton(btn) 80 | } 81 | } 82 | 83 | // ButtonUpdateHandler allows a user of this library to signal when something external has changed, such that this button should be update 84 | func (sd *StreamDeck) ButtonUpdateHandler(b Button) { 85 | sd.buttons[b.GetButtonIndex()] = b 86 | sd.updateButton(b) 87 | } 88 | 89 | // GetButtonByIndex returns a button for the given index 90 | func (sd *StreamDeck) GetButtonIndex(btnIndex int) Button { 91 | b, ok := sd.buttons[btnIndex] 92 | if !ok { 93 | return nil 94 | } 95 | return b 96 | } 97 | 98 | func (sd *StreamDeck) pressHandler(btnIndex int, d *Device, err error) { 99 | if err != nil { 100 | panic(err) 101 | } 102 | b := sd.buttons[btnIndex] 103 | if b != nil { 104 | sd.buttons[btnIndex].Pressed() 105 | } 106 | } 107 | 108 | func (sd *StreamDeck) updateButton(b Button) error { 109 | img := b.GetImageForButton(sd.dev.deviceType.imageSize.X) 110 | decorator, ok := sd.decorators[b.GetButtonIndex()] 111 | if ok { 112 | img = decorator.Apply(img, sd.dev.deviceType.imageSize.X) 113 | } 114 | e := sd.dev.WriteRawImageToButton(b.GetButtonIndex(), img) 115 | return e 116 | } 117 | 118 | func (sd *StreamDeck) SetBrightness(brightness int) { 119 | sd.dev.SetBrightness(brightness) 120 | } 121 | -------------------------------------------------------------------------------- /text.go: -------------------------------------------------------------------------------- 1 | package streamdeck 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/golang/freetype" 8 | "github.com/golang/freetype/truetype" 9 | 10 | "golang.org/x/image/font/gofont/gomedium" 11 | ) 12 | 13 | // WriteTextToButton is a low-level way to write text directly onto a button on the StreamDeck 14 | func (d *Device) WriteTextToButton(btnIndex int, text string, textColour color.Color, backgroundColour color.Color) { 15 | img := getImageWithText(text, textColour, backgroundColour, d.deviceType.imageSize.X) 16 | d.WriteRawImageToButton(btnIndex, img) 17 | } 18 | 19 | func getImageWithText(text string, textColour color.Color, backgroundColour color.Color, btnSize int) image.Image { 20 | 21 | size := float64(18) 22 | 23 | myfont, err := truetype.Parse(gomedium.TTF) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | width := 0 29 | for size = 1; size < 60; size++ { 30 | width = getTextWidth(text, size) 31 | if width > 90 { 32 | size = size - 1 33 | break 34 | } 35 | } 36 | 37 | srcImg := image.NewUniform(textColour) 38 | dstImg := getSolidColourImage(backgroundColour, btnSize) 39 | 40 | c := freetype.NewContext() 41 | c.SetFont(myfont) 42 | c.SetDst(dstImg) 43 | c.SetSrc(srcImg) 44 | c.SetFontSize(size) 45 | c.SetClip(dstImg.Bounds()) 46 | 47 | x := int((btnSize - width) / 2) // Horizontally centre text 48 | y := int((float64(btnSize) / 2) + (size / 3)) // Fudged vertical centre, erm, very "heuristic" 49 | 50 | pt := freetype.Pt(x, y) 51 | c.DrawString(text, pt) 52 | 53 | /* 54 | textWidth := 7 * len(text) 55 | fmt.Println(textWidth) 56 | 57 | f := &font.Drawer{ 58 | Dst: dstImg, 59 | Src: srcImg, 60 | Face: basicfont.Face7x13, 61 | Dot: fixed.Point26_6{fixed.Int26_6(x * 64), fixed.Int26_6(y * 64)}, 62 | } 63 | f.DrawString(text) 64 | */ 65 | return dstImg 66 | } 67 | 68 | func getTextWidth(text string, size float64) int { 69 | 70 | myfont, err := truetype.Parse(gomedium.TTF) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | // Calculate width of string 76 | width := 0 77 | face := truetype.NewFace(myfont, &truetype.Options{Size: size}) 78 | for _, x := range text { 79 | awidth, _ := face.GlyphAdvance(rune(x)) 80 | iwidthf := int(float64(awidth) / 64) 81 | width += iwidthf 82 | } 83 | 84 | return width 85 | } 86 | --------------------------------------------------------------------------------