├── .gitignore ├── .vscode └── settings.json ├── DejaVuSans-Bold.ttf ├── Makefile ├── README.md ├── build ├── com.exension.hwinfo.streamDeckPlugin └── images │ ├── clicksettings.png │ ├── configureaction.gif │ ├── contextquit.png │ ├── demo.gif │ ├── dragaction.gif │ ├── recommendedsettings.png │ ├── sensorsonly.png │ ├── sharedmemory.png │ ├── streamdeckactionlist.png │ └── streamdeckinstall.png ├── cmd ├── hwinfo-plugin │ └── main.go ├── hwinfo_debugger │ └── main.go └── hwinfo_streamdeck_plugin │ └── main.go ├── com.exension.hwinfo.sdPlugin ├── DejaVuSans-Bold.ttf ├── css │ ├── buttons.png │ ├── buttons@2x.png │ ├── caret.svg │ ├── check.png │ ├── check.svg │ ├── elg_calendar.svg │ ├── elg_calendar_inv.svg │ ├── elg_calendar_inv_13.svg │ ├── local.css │ ├── pi_required.svg │ ├── pi_required_ok.svg │ ├── rcheck.svg │ ├── reset.min.css │ ├── sdpi.css │ └── xsdpi.css ├── defaultImage.png ├── defaultImage@2x.png ├── icon.png ├── icon@2x.png ├── index_pi.html ├── index_pi.js ├── launch-hwinfo.png ├── manifest.json ├── pluginIcon.png └── pluginIcon@2x.png ├── com.exension.hwinfo.streamDeckPlugin ├── examples ├── bench │ ├── DejaVuSans-Bold.ttf │ ├── main.go │ └── main_test.go └── graph │ └── main.go ├── go.mod ├── go.sum ├── images ├── clicksettings.png ├── configureaction.gif ├── contextquit.png ├── demo.gif ├── dragaction.gif ├── recommendedsettings.png ├── sensorsonly.png ├── sharedmemory.png ├── streamdeckactionlist.png └── streamdeckinstall.png ├── install-plugin.bat ├── internal ├── app │ └── hwinfostreamdeckplugin │ │ ├── action_manager.go │ │ ├── delegate.go │ │ ├── handlers.go │ │ ├── plugin.go │ │ └── types.go └── hwinfo │ ├── hwinfo.go │ ├── hwisenssm2.h │ ├── mutex │ └── mutex.go │ ├── plugin │ ├── plugin.go │ └── service.go │ ├── reading.go │ ├── sensor.go │ ├── shmem │ └── shmem.go │ └── util │ └── util.go ├── kill-streamdeck.bat ├── make-release.bat ├── pkg ├── graph │ └── graph.go ├── service │ ├── grpc.go │ ├── interface.go │ └── proto │ │ ├── hwservice.pb.go │ │ ├── hwservice.proto │ │ └── hwservice_grpc.pb.go └── streamdeck │ ├── streamdeck.go │ └── types.go └── start-streamdeck.bat /.gitignore: -------------------------------------------------------------------------------- 1 | # Vendored libs 2 | /vendor 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # VSCode launch settings 18 | .vscode/launch.json 19 | 20 | # GoLand 21 | .idea 22 | 23 | # Test graph.png 24 | graph.png 25 | 26 | # Debug 27 | cpu.prof 28 | mem.prof 29 | cmd/hwinfostreamdeckplugin/debug 30 | 31 | # dep cache (protoc) 32 | .cache/ 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "editor.formatOnSave": true, 4 | "go.useLanguageServer": true, 5 | "taskExplorer.pathToMake": "make" 6 | } 7 | -------------------------------------------------------------------------------- /DejaVuSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/DejaVuSans-Bold.ttf -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=go 2 | GOBUILD=$(GOCMD) build 3 | GOCLEAN=$(GOCMD) clean 4 | 5 | SDPLUGINDIR=./com.exension.hwinfo.sdPlugin 6 | 7 | PROTOS=$(wildcard ./*/**/**/*.proto) 8 | PROTOPB=$(PROTOS:.proto=.pb.go) 9 | 10 | plugin: proto 11 | $(GOBUILD) -o $(SDPLUGINDIR)/hwinfo.exe ./cmd/hwinfo_streamdeck_plugin 12 | $(GOBUILD) -o $(SDPLUGINDIR)/hwinfo-plugin.exe ./cmd/hwinfo-plugin 13 | cp ../go-hwinfo-hwservice-plugin/bin/hwinfo-plugin.exe $(SDPLUGINDIR)/hwinfo-plugin.exe 14 | -@install-plugin.bat 15 | 16 | proto: $(PROTOPB) 17 | 18 | $(PROTOPB): $(PROTOS) 19 | .cache/protoc/bin/protoc \ 20 | --go_out=Mgrpc/service_config/service_config.proto=/internal/proto/grpc_service_config:. \ 21 | --go-grpc_out=Mgrpc/service_config/service_config.proto=/internal/proto/grpc_service_config:. \ 22 | --go_opt=paths=source_relative \ 23 | --go-grpc_opt=paths=source_relative \ 24 | $(<) 25 | 26 | # plugin: 27 | # -@kill-streamdeck.bat 28 | # @go build -o com.exension.hwinfo.sdPlugin\\hwinfo.exe github.com/shayne/hwinfo-streamdeck/cmd/hwinfo_streamdeck_plugin 29 | # @xcopy com.exension.hwinfo.sdPlugin $(APPDATA)\\Elgato\\StreamDeck\\Plugins\\com.exension.hwinfo.sdPlugin\\ /E /Q /Y 30 | # @start-streamdeck.bat 31 | 32 | debug: 33 | $(GOBUILD) -o $(SDPLUGINDIR)/hwinfo.exe ./cmd/hwinfo_debugger 34 | cp ../go-grpc-hardware-service/bin/hwinfo-plugin.exe $(SDPLUGINDIR)/hwinfo-plugin.exe 35 | -@install-plugin.bat 36 | # @xcopy com.exension.hwinfo.sdPlugin $(APPDATA)\\Elgato\\StreamDeck\\Plugins\\com.exension.hwinfo.sdPlugin\\ /E /Q /Y 37 | 38 | release: 39 | -@rm build/com.exension.hwinfo.streamDeckPlugin 40 | @DistributionTool.exe -b -i com.exension.hwinfo.sdPlugin -o build 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HWiNFO Stream Deck Plugin 2 | 3 | ## ⚠⚠ Major refactor landed in pre-release v2.0.0, plugin code open sourced, remote monitoring infrastructure support ⚠⚠ 4 | 5 | --- 6 | 7 | >## Thank you & Looking for Maintainers 8 | > 9 | >Thank you everyone who has used and enjoyed this plugin. It started as a passion project and I continue to use it day to day. I am happy to finally release the full source on GitHub. When I first built it, it was closed under agreement with the HWiNFO64 project. They have since opened up the shared memory interface and now the plugin is freely open. 10 | > 11 | >I haven't had the time to dedicate to this project in some time and appreciate everyone for hanging in there. I hope to work with some of you who are eager to take the project over. I am happy and ready to hand over the reigns. If there are development questions I'm happy to share my thoughts on the code and structure that exists. 12 | > 13 | >*-Shayne* 14 | 15 | --- 16 | 17 | ![alt text](images/demo.gif "HWiNFO64 Stream Deck Plugin Demo") 18 | 19 | > NOTICE: HWiNFO64 must be run in Sensors-only mode for the plugin to work. 20 | 21 | ## Enabling Support in HWiNFO64 22 | 23 | > NOTICE: It has been reported that running the "portable" version of HWiNFO64 doesn't work with this plugin. The recommendation is to run the version with the installer until I can figure out the issue. 24 | 25 | 1. Download and install HWiNFO64, if you haven't already 26 | 27 | [HWiNFO Website](https://www.hwinfo.com) 28 | 29 | 2. Choose "Sensors-only" mode 30 | 31 | ![alt text](images/sensorsonly.png "HWiNFO64 Sensors Only") 32 | 33 | 3. Click "Settings" 34 | 35 | ![alt text](images/clicksettings.png "HWiNFO64 Click Settings") 36 | 37 | 4. Ensure "Shared Memory Support" is checked 38 | 39 | ![alt text](images/sharedmemory.png "HWiNFO64 Settings") 40 | 41 | 5. (Optional) Recommended launch settings 42 | 43 | ![alt text](images/recommendedsettings.png "Quit HWiNFO64") 44 | 45 | 6. Click "OK" then, "Run" 46 | 47 | > If the plugin doesn't work immediately, you may have to quit and reopen HWiNFO64. 48 | > 49 | > From the system tray: 50 | > 51 | > ![alt text](images/contextquit.png "Quit HWiNFO64") 52 | 53 | 54 | ## Install and Setup the Plugin 55 | 56 | 1. Download the latest pre-compiled plugin 57 | 58 | [Plugin Releases](../../releases) 59 | 60 | > When upgrading, first uninstall: within the Stream Deck app choose "More Actions..." (bottom-right), locate "HWiNFO" and choose "Uninstall". Your tiles and settings will be preserved. 61 | 62 | 2. Double-click to install the plugin 63 | 64 | 3. Choose "Install" went prompted by Stream Deck 65 | 66 | ![alt text](images/streamdeckinstall.png "Stream Deck Plugin Installation") 67 | 68 | 4. Locate "HWiNFO" under "Custom" in the action list 69 | 70 | ![alt text](images/streamdeckactionlist.png "Stream Deck Action List") 71 | 72 | 5. Drag the "HWiNFO" action from the list to a tile in the canvas area 73 | 74 | ![alt text](images/dragaction.gif "Drag Action") 75 | 76 | 6. Configure the action to display the sensor reading you wish 77 | 78 | ![alt text](images/configureaction.gif "Configure Action") 79 | -------------------------------------------------------------------------------- /build/com.exension.hwinfo.streamDeckPlugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/com.exension.hwinfo.streamDeckPlugin -------------------------------------------------------------------------------- /build/images/clicksettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/clicksettings.png -------------------------------------------------------------------------------- /build/images/configureaction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/configureaction.gif -------------------------------------------------------------------------------- /build/images/contextquit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/contextquit.png -------------------------------------------------------------------------------- /build/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/demo.gif -------------------------------------------------------------------------------- /build/images/dragaction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/dragaction.gif -------------------------------------------------------------------------------- /build/images/recommendedsettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/recommendedsettings.png -------------------------------------------------------------------------------- /build/images/sensorsonly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/sensorsonly.png -------------------------------------------------------------------------------- /build/images/sharedmemory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/sharedmemory.png -------------------------------------------------------------------------------- /build/images/streamdeckactionlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/streamdeckactionlist.png -------------------------------------------------------------------------------- /build/images/streamdeckinstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/build/images/streamdeckinstall.png -------------------------------------------------------------------------------- /cmd/hwinfo-plugin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/hashicorp/go-plugin" 7 | hwinfoplugin "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/plugin" 8 | hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" 9 | ) 10 | 11 | func main() { 12 | service := hwinfoplugin.StartService() 13 | go func() { 14 | for { 15 | err := service.Recv() 16 | if err != nil { 17 | log.Printf("service recv failed: %v\n", err) 18 | } 19 | } 20 | }() 21 | 22 | plugin.Serve(&plugin.ServeConfig{ 23 | HandshakeConfig: hwsensorsservice.Handshake, 24 | Plugins: map[string]plugin.Plugin{ 25 | "hwinfoplugin": &hwsensorsservice.HardwareServicePlugin{Impl: &hwinfoplugin.Plugin{Service: service}}, 26 | }, 27 | 28 | // A non-nil value here enables gRPC serving for this plugin... 29 | GRPCServer: plugin.DefaultGRPCServer, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /cmd/hwinfo_debugger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | var port = flag.String("port", "", "The port that should be used to create the WebSocket") 12 | var pluginUUID = flag.String("pluginUUID", "", "A unique identifier string that should be used to register the plugin once the WebSocket is opened") 13 | var registerEvent = flag.String("registerEvent", "", "Registration event") 14 | var info = flag.String("info", "", "A stringified json containing the Stream Deck application information and devices information") 15 | 16 | func main() { 17 | appdata := os.Getenv("APPDATA") 18 | logpath := filepath.Join(appdata, "Elgato/StreamDeck/Plugins/com.exension.hwinfo.sdPlugin/hwinfo.log") 19 | f, err := os.OpenFile(logpath, os.O_RDWR|os.O_CREATE, 0666) 20 | f.Truncate(0) 21 | if err != nil { 22 | log.Fatalf("OpenFile Log: %v", err) 23 | } 24 | defer f.Close() 25 | log.SetOutput(f) 26 | log.SetFlags(0) 27 | 28 | flag.Parse() 29 | 30 | args := []string{ 31 | "-port", 32 | *port, 33 | "-pluginUUID", 34 | *pluginUUID, 35 | "-registerEvent", 36 | *registerEvent, 37 | "-info", 38 | *info, 39 | } 40 | bytes, err := json.MarshalIndent(args, "", " ") 41 | if err != nil { 42 | log.Fatal("Failed to marshal args", err) 43 | } 44 | 45 | log.Println(string(bytes)) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/hwinfo_streamdeck_plugin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "log" 7 | 8 | // "net/http" 9 | // _ "net/http/pprof" 10 | "os" 11 | "path/filepath" 12 | 13 | plugin "github.com/shayne/hwinfo-streamdeck/internal/app/hwinfostreamdeckplugin" 14 | ) 15 | 16 | var port = flag.String("port", "", "The port that should be used to create the WebSocket") 17 | var pluginUUID = flag.String("pluginUUID", "", "A unique identifier string that should be used to register the plugin once the WebSocket is opened") 18 | var registerEvent = flag.String("registerEvent", "", "Registration event") 19 | var info = flag.String("info", "", "A stringified json containing the Stream Deck application information and devices information") 20 | 21 | func main() { 22 | // go func() { 23 | // log.Println(http.ListenAndServe("localhost:6060", nil)) 24 | // }() 25 | 26 | // make sure files are read relative to exe 27 | err := os.Chdir(filepath.Dir(os.Args[0])) 28 | if err != nil { 29 | log.Fatalf("Unable to chdir: %v", err) 30 | } 31 | 32 | // PRODUCTION 33 | // LOGGING DISABLED: 34 | // 35 | log.SetOutput(ioutil.Discard) 36 | 37 | // DEBUG LOGGING: 38 | // 39 | // appdata := os.Getenv("APPDATA") 40 | // logpath := filepath.Join(appdata, "Elgato/StreamDeck/Plugins/com.exension.hwinfo.sdPlugin/hwinfo.log") 41 | // f, err := os.OpenFile(logpath, os.O_RDWR|os.O_CREATE, 0666) 42 | // if err != nil { 43 | // log.Fatalf("OpenFile Log: %v", err) 44 | // } 45 | // err = f.Truncate(0) 46 | // if err != nil { 47 | // log.Fatalf("Truncate Log: %v", err) 48 | // } 49 | // defer func() { 50 | // err := f.Close() 51 | // if err != nil { 52 | // log.Fatalf("File Close: %v", err) 53 | // } 54 | // }() 55 | // log.SetOutput(f) 56 | // log.SetFlags(0) 57 | 58 | flag.Parse() 59 | 60 | p, err := plugin.NewPlugin(*port, *pluginUUID, *registerEvent, *info) 61 | if err != nil { 62 | log.Fatal("NewPlugin failed:", err) 63 | } 64 | 65 | err = p.RunForever() 66 | if err != nil { 67 | log.Fatal("runForever", err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/DejaVuSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/DejaVuSans-Bold.ttf -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/css/buttons.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/buttons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/css/buttons@2x.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/css/check.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/elg_calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/elg_calendar_inv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/elg_calendar_inv_13.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/local.css: -------------------------------------------------------------------------------- 1 | body, 2 | .localbody { 3 | height: 100%; 4 | padding: 0; 5 | overflow-x: hidden; 6 | overflow-y: auto; 7 | margin: 0; 8 | -webkit-overflow-scrolling: touch; 9 | } 10 | 11 | .localbody { 12 | width: 350px; 13 | margin: 0 auto; 14 | } 15 | 16 | .hidden { 17 | display: block; 18 | } -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/pi_required.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/pi_required_ok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/rcheck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/reset.min.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, 2 | pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, 3 | q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, 4 | dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, 5 | thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, 6 | footer, header, hgroup, menu, nav, output, ruby, section, summary, time, 7 | mark, audio, video { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | font-size: 100%; 12 | font: inherit; 13 | vertical-align: baseline 14 | } 15 | 16 | article, aside, details, figcaption, figure, footer, header, hgroup, 17 | menu, nav, section { 18 | display: block 19 | } 20 | 21 | body { 22 | line-height: 1 23 | } 24 | 25 | ol, ul { 26 | list-style: none 27 | } 28 | 29 | blockquote, q { 30 | quotes: none 31 | } 32 | 33 | blockquote:before, blockquote:after, q:before, q:after { 34 | content: ''; 35 | content: none 36 | } 37 | 38 | table { 39 | border-collapse: collapse; 40 | border-spacing: 0 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/css/xsdpi.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | width: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | html, body { 8 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 9 | font-size: 9pt; 10 | background-color: #2D2D2D; 11 | color: #9a9a9a; 12 | } 13 | 14 | body, 15 | .localbody { 16 | height: 100%; 17 | padding: 0; 18 | overflow-x: hidden; 19 | overflow-y: auto; 20 | margin: 0; 21 | -webkit-overflow-scrolling: touch; 22 | } 23 | 24 | mark { 25 | background-color: #2D2D2D; 26 | color: #d8d8d8; 27 | } 28 | 29 | .localbody { 30 | width: 360px; 31 | /* height: 320px; */ 32 | } 33 | 34 | .hidden { 35 | display: none; 36 | } 37 | 38 | hr, hr2 { 39 | -webkit-margin-before: 1em; 40 | -webkit-margin-after: 1em; 41 | border-style: none; 42 | background: #3d3d3d; 43 | height: 1px; 44 | } 45 | 46 | hr2, 47 | .sdpi-heading { 48 | display: flex; 49 | flex-basis: 100%; 50 | align-items: center; 51 | color: inherit; 52 | font-size: 12px; 53 | margin: 8px 0px; 54 | } 55 | 56 | .sdpi-heading::before, 57 | .sdpi-heading::after { 58 | content: ""; 59 | flex-grow: 1; 60 | background: #3d3d3d; 61 | height: 1px; 62 | font-size: 0px; 63 | line-height: 0px; 64 | margin: 0px 16px; 65 | } 66 | 67 | hr2 { 68 | height: 2px; 69 | } 70 | 71 | hr, hr2 { 72 | margin-left:16px; 73 | margin-right:16px; 74 | } 75 | 76 | ::-webkit-progress-value, 77 | meter::-webkit-meter-optimum-value { 78 | border-radius: 2px; 79 | background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); 80 | } 81 | 82 | ::-webkit-progress-bar, 83 | meter::-webkit-meter-bar { 84 | border-radius: 3px; 85 | background: #3d3d3d; 86 | } 87 | 88 | ::-webkit-progress-bar:active, 89 | meter::-webkit-meter-bar:active { 90 | border-radius: 3px; 91 | background: #222222; 92 | } 93 | ::-webkit-progress-value:active, 94 | meter::-webkit-meter-optimum-value:active { 95 | background: #99f; 96 | } 97 | 98 | progress, 99 | progress.sdpi-item-value { 100 | min-height: 5px !important; 101 | height: 5px; 102 | background-color: #303030; 103 | } 104 | 105 | progress { 106 | margin-top: 8px !important; 107 | margin-bottom: 8px !important; 108 | } 109 | 110 | .full progress, 111 | progress.full { 112 | margin-top: 3px !important; 113 | } 114 | 115 | ::-webkit-progress-inner-element { 116 | background-color: transparent; 117 | } 118 | 119 | 120 | .sdpi-item[type="progress"] { 121 | margin-top: 4px !important; 122 | margin-bottom: 12px; 123 | min-height: 15px; 124 | } 125 | 126 | .sdpi-item-child.full:last-child { 127 | margin-bottom: 4px; 128 | } 129 | 130 | .tabs { 131 | /** 132 | * Setting display to flex makes this container lay 133 | * out its children using flexbox, the exact same 134 | * as in the above "Stepper input" example. 135 | */ 136 | display: flex; 137 | 138 | border-bottom: 1px solid #D7DBDD; 139 | } 140 | 141 | .tab { 142 | cursor: pointer; 143 | padding: 5px 30px; 144 | color: #16a2d7; 145 | font-size: 12px; 146 | border-bottom: 2px solid transparent; 147 | } 148 | 149 | .tab.is-tab-selected { 150 | border-bottom-color: #4ebbe4; 151 | } 152 | 153 | select { 154 | -webkit-appearance: none; 155 | -moz-appearance: none; 156 | -o-appearance: none; 157 | appearance: none; 158 | background: url(caret.svg) no-repeat 97% center; 159 | } 160 | 161 | label.sdpi-file-label, 162 | input[type="button"], 163 | input[type="submit"], 164 | input[type="reset"], 165 | input[type="file"], 166 | input[type=file]::-webkit-file-upload-button, 167 | button, 168 | select { 169 | color: #d8d8d8; 170 | border: 1pt solid #303030; 171 | font-size: 1em; 172 | font-weight: normal; 173 | background-color: #3d3d3d; 174 | border-radius: 0; 175 | } 176 | 177 | label.sdpi-file-label, 178 | input[type="button"], 179 | input[type="submit"], 180 | input[type="reset"], 181 | input[type="file"], 182 | input[type=file]::-webkit-file-upload-button, 183 | button { 184 | border: 1pt solid #d8d8d8; 185 | border-radius: 4px; 186 | font-size: 11px; 187 | min-height: 23px !important; 188 | height: 23px !important; 189 | margin-right: 8px; 190 | } 191 | 192 | input[type=number]::-webkit-inner-spin-button, 193 | input[type=number]::-webkit-outer-spin-button { 194 | -webkit-appearance: none; 195 | margin: 0; 196 | } 197 | 198 | input[type="file"] { 199 | border-radius: 0px; 200 | max-width: 220px; 201 | } 202 | 203 | 204 | option { 205 | height: 1.5em; 206 | padding: 4px; 207 | } 208 | 209 | /* SDPI */ 210 | 211 | .sdpi-wrapper { 212 | overflow-x: hidden; 213 | } 214 | 215 | .sdpi-item { 216 | display: flex; 217 | flex-direction: row; 218 | min-height: 32px; 219 | align-items: center; 220 | margin-top: 2px; 221 | max-width: 344px; 222 | } 223 | 224 | .sdpi-item:first-child { 225 | margin-top:0px; 226 | } 227 | 228 | .sdpi-item:last-child { 229 | margin-bottom: 0px; 230 | } 231 | 232 | .sdpi-item > *:not(.sdpi-item-label):not(meter) { 233 | min-height: 26px; 234 | padding: 0px 4px 0px 4px; 235 | } 236 | 237 | .sdpi-item-group { 238 | padding: 0 !important; 239 | } 240 | 241 | meter.sdpi-item-value { 242 | margin-left: 6px; 243 | } 244 | 245 | .sdpi-item[type="group"] { 246 | display: block; 247 | margin-top: 12px; 248 | margin-bottom: 12px; 249 | /* border: 1px solid white; */ 250 | flex-direction: unset; 251 | text-align: left; 252 | } 253 | 254 | .sdpi-item[type="group"] > .sdpi-item-label, 255 | .sdpi-item[type="group"].sdpi-item-label { 256 | width: 96%; 257 | text-align: left; 258 | font-weight: 700; 259 | margin-bottom: 4px; 260 | padding-left: 4px; 261 | } 262 | 263 | dl, 264 | ul, 265 | ol { 266 | -webkit-margin-before: 0px; 267 | -webkit-margin-after: 4px; 268 | -webkit-padding-start: 1em; 269 | max-height: 90px; 270 | overflow-y: scroll; 271 | cursor: pointer; 272 | user-select: none; 273 | } 274 | 275 | table.sdpi-item-value, 276 | dl.sdpi-item-value, 277 | ul.sdpi-item-value, 278 | ol.sdpi-item-value { 279 | -webkit-margin-before: 4px; 280 | -webkit-margin-after: 8px; 281 | -webkit-padding-start: 1em; 282 | width: 224px; 283 | text-align: center; 284 | } 285 | 286 | table > caption { 287 | margin: 2px; 288 | } 289 | 290 | .list, 291 | .sdpi-item[type="list"] { 292 | align-items: baseline; 293 | } 294 | 295 | .sdpi-item-label { 296 | text-align: right; 297 | flex: none; 298 | width: 94px; /* 27%; +++ */ 299 | padding-right: 4px; 300 | font-weight: bold; 301 | } 302 | 303 | .sdpi-item-label > small{ 304 | font-weight: normal; 305 | } 306 | 307 | .sdpi-item-label:after { 308 | content: ": "; 309 | } 310 | 311 | .sdpi-test, 312 | .sdpi-item-value { 313 | flex: 1 0 0; 314 | /* flex-grow: 1; 315 | flex-shrink: 0; */ 316 | margin-right: 14px; 317 | margin-left: 4px; 318 | justify-content: space-evenly; 319 | } 320 | 321 | input.sdpi-item-value { 322 | margin-left: 5px; 323 | } 324 | 325 | .sdpi-item-value button, 326 | button.sdpi-item-value { 327 | margin-left: 7px; 328 | margin-right: 19px; 329 | } 330 | 331 | .sdpi-item-value.range { 332 | margin-left: 0px; 333 | } 334 | 335 | table, 336 | dl.sdpi-item-value, 337 | ul.sdpi-item-value, 338 | ol.sdpi-item-value, 339 | .sdpi-item-value > dl, 340 | .sdpi-item-value > ul, 341 | .sdpi-item-value > ol 342 | { 343 | list-style-type: none; 344 | list-style-position: outside; 345 | margin-left: -4px; 346 | margin-right: -4px; 347 | padding: 4px; 348 | border: 1px solid #3a3a3a; 349 | } 350 | 351 | dl.sdpi-item-value, 352 | ul.sdpi-item-value, 353 | ol.sdpi-item-value, 354 | .sdpi-item-value > ol { 355 | list-style-type: none; 356 | list-style-position: inside; 357 | margin-left: 5px; 358 | margin-right: 18px; 359 | padding: 4px !important; 360 | } 361 | 362 | ol.sdpi-item-value, 363 | .sdpi-item-value > ol[listtype="none"] { 364 | list-style-type: none; 365 | } 366 | ol.sdpi-item-value[type="decimal"], 367 | .sdpi-item-value > ol[type="decimal"] { 368 | list-style-type: decimal; 369 | } 370 | 371 | ol.sdpi-item-value[type="decimal-leading-zero"], 372 | .sdpi-item-value > ol[type="decimal-leading-zero"] { 373 | list-style-type: decimal-leading-zero; 374 | } 375 | 376 | ol.sdpi-item-value[type="lower-alpha"], 377 | .sdpi-item-value > ol[type="lower-alpha"] { 378 | list-style-type: lower-alpha; 379 | } 380 | 381 | ol.sdpi-item-value[type="upper-alpha"], 382 | .sdpi-item-value > ol[type="upper-alpha"] { 383 | list-style-type: upper-alpha; 384 | } 385 | 386 | ol.sdpi-item-value[type="upper-roman"], 387 | .sdpi-item-value > ol[type="upper-roman"] { 388 | list-style-type: upper-roman; 389 | } 390 | 391 | ol.sdpi-item-value[type="lower-roman"], 392 | .sdpi-item-value > ol[type="lower-roman"] { 393 | list-style-type: upper-roman; 394 | } 395 | 396 | tr:nth-child(even), 397 | .sdpi-item-value > ul > li:nth-child(even), 398 | .sdpi-item-value > ol > li:nth-child(even), 399 | li:nth-child(even) { 400 | background-color: rgba(0,0,0,.2) 401 | } 402 | 403 | td:hover, 404 | .sdpi-item-value > ul > li:hover:nth-child(even), 405 | .sdpi-item-value > ol > li:hover:nth-child(even), 406 | li:hover:nth-child(even), 407 | li:hover { 408 | background-color: rgba(255,255,255,.1); 409 | } 410 | 411 | td.selected, 412 | td.selected:hover, 413 | li.selected:hover, 414 | li.selected { 415 | color: white; 416 | background-color: #77f; 417 | } 418 | 419 | tr { 420 | border: 1px solid #3a3a3a; 421 | } 422 | 423 | td { 424 | border-right: 1px solid #3a3a3a; 425 | } 426 | 427 | tr:last-child, 428 | td:last-child { 429 | border: none; 430 | } 431 | 432 | .sdpi-item-value.select, 433 | .sdpi-item-value > select { 434 | margin-right: 13px; 435 | margin-left: 4px; 436 | } 437 | 438 | .sdpi-item-child, 439 | .sdpi-item-group > .sdpi-item > input[type="color"] { 440 | margin-top: 0.4em; 441 | margin-right: 4px; 442 | } 443 | 444 | .full, 445 | .full *, 446 | .sdpi-item-value.full, 447 | .sdpi-item-child > full > *, 448 | .sdpi-item-child.full, 449 | .sdpi-item-child.full > *, 450 | .full > .sdpi-item-child, 451 | .full > .sdpi-item-child > *{ 452 | display: flex; 453 | flex: 1 1 0; 454 | margin-bottom: 4px; 455 | margin-left: 0px; 456 | width: 100%; 457 | 458 | justify-content: space-evenly; 459 | } 460 | 461 | .sdpi-item-group > .sdpi-item > input[type="color"] { 462 | margin-top: 0px; 463 | } 464 | 465 | ::-webkit-calendar-picker-indicator:focus, 466 | input[type=file]::-webkit-file-upload-button:focus, 467 | button:focus, 468 | textarea:focus, 469 | input:focus, 470 | select:focus, 471 | option:focus, 472 | details:focus, 473 | summary:focus, 474 | .custom-select select { 475 | outline: none; 476 | } 477 | 478 | input:not([type="range"]), 479 | textarea { 480 | -webkit-appearance: none; 481 | background: #3d3d3d; 482 | color: #d8d8d8; 483 | font-weight: normal; 484 | font-size: 9pt; 485 | border: none; 486 | } 487 | 488 | textarea + label { 489 | display: flex; 490 | justify-content: flex-end 491 | } 492 | 493 | input[type="checkbox"] { 494 | display: none; 495 | } 496 | input[type="radio"] + label, 497 | input[type="checkbox"] + label { 498 | font-size: 9pt; 499 | color: #d8d8d8; 500 | font-weight: normal; 501 | margin-right: 8px; 502 | } 503 | 504 | input[type="radio"] + label:after, 505 | input[type="checkbox"] + label:after { 506 | content: " " !important; 507 | } 508 | 509 | .sdpi-item[type="radio"] > .sdpi-item-value, 510 | .sdpi-item[type="checkbox"] > .sdpi-item-value { 511 | padding-top: 0.75em; 512 | } 513 | 514 | .sdpi-item[type="checkbox"] > .sdpi-item-value > * { 515 | margin-top: 4px; 516 | } 517 | 518 | .sdpi-item[type="checkbox"] .sdpi-item-child, 519 | .sdpi-item[type="radio"] .sdpi-item-child { 520 | display: inline-block; 521 | } 522 | 523 | .sdpi-item[type="range"] .sdpi-item-value, 524 | .sdpi-item[type="meter"] .sdpi-item-child, 525 | .sdpi-item[type="progress"] .sdpi-item-child { 526 | display: flex; 527 | } 528 | 529 | .vertical.sdpi-item[type="range"] .sdpi-item-value { 530 | display: block; 531 | } 532 | 533 | .sdpi-item[type="range"] .sdpi-item-value span, 534 | .sdpi-item[type="meter"] .sdpi-item-child span, 535 | .sdpi-item[type="progress"] .sdpi-item-child span { 536 | margin-top: -2px; 537 | min-width: 24px; 538 | text-align: right; 539 | user-select: none; 540 | cursor: pointer; 541 | } 542 | 543 | .sdpi-item[type="range"] .sdpi-item-value span { 544 | margin-top: 7px; 545 | text-align: right; 546 | } 547 | 548 | span + input[type="range"] { 549 | display: flex; 550 | max-width: 168px; 551 | 552 | } 553 | .sdpi-item[type="range"] .sdpi-item-value span:first-child, 554 | .sdpi-item[type="meter"] .sdpi-item-child span:first-child, 555 | .sdpi-item[type="progress"] .sdpi-item-child span:first-child { 556 | margin-right: 4px; 557 | } 558 | 559 | .sdpi-item[type="range"] .sdpi-item-value span:last-child, 560 | .sdpi-item[type="meter"] .sdpi-item-child span:last-child, 561 | .sdpi-item[type="progress"] .sdpi-item-child span:last-child { 562 | margin-left: 4px; 563 | } 564 | 565 | 566 | .sdpi-item[type="meter"] .sdpi-item-child meter + span:last-child, 567 | .sdpi-item[type="progress"] .sdpi-item-child meter + span:last-child { 568 | margin-left: -14px; 569 | } 570 | 571 | .sdpi-item[type="radio"] > .sdpi-item-value > * { 572 | margin-top: 2px; 573 | } 574 | 575 | details { 576 | padding: 8px 18px 8px 12px; 577 | } 578 | 579 | details > h4 { 580 | border-bottom: 1px solid #3a3a3a; 581 | } 582 | 583 | legend { 584 | display: none; 585 | } 586 | .sdpi-item-value > textarea { 587 | padding: 0px; 588 | width: 227px; 589 | margin-left: 1px; 590 | } 591 | 592 | input[type="radio"] + label span, 593 | input[type="checkbox"] + label span { 594 | display: inline-block; 595 | width: 19px; 596 | height: 19px; 597 | margin: -2px 4px 0 0; 598 | vertical-align: middle; 599 | /* background: url(buttons.png) left top no-repeat; */ 600 | background: #3d3d3d; 601 | cursor: pointer; 602 | } 603 | 604 | input[type="checkbox"] + label span { 605 | margin: 2px 4px 2px 0; 606 | width: 16px; 607 | height: 16px; 608 | border-radius: 3px; 609 | border: 1px solid rgb(0,0,0,0); 610 | } 611 | 612 | input[type="checkbox"]:checked + label span { 613 | /* background: url(buttons.png) -19px top no-repeat; */ 614 | background-color: #77f; 615 | background-image: url(check.svg); 616 | background-repeat: no-repeat; 617 | background-position: center center; 618 | border: 1px solid rgb(0,0,0,.4); 619 | } 620 | 621 | input[type="radio"] { 622 | display: none; 623 | } 624 | 625 | input[type="radio"] + label span { 626 | background: url(buttons.png) -38px top no-repeat; 627 | } 628 | 629 | input[type="radio"]:checked + label span { 630 | background: url(buttons.png) -57px top no-repeat; 631 | } 632 | 633 | input[type="range"] { 634 | width: 224px; 635 | height: 30px; 636 | overflow: hidden; 637 | cursor: pointer; 638 | background: transparent !important; 639 | } 640 | 641 | .sdpi-item > input[type="range"] { 642 | margin-left: 8px; 643 | max-width: 224px; 644 | width: 224px; 645 | padding: 0px; 646 | } 647 | 648 | /* 649 | input[type="range"], 650 | input[type="range"]::-webkit-slider-runnable-track, 651 | input[type="range"]::-webkit-slider-thumb { 652 | -webkit-appearance: none; 653 | } 654 | */ 655 | ::-webkit-slider-thumb, 656 | ::-webkit-slider-runnable-track, 657 | .vertical input[type="range"]::-webkit-slider-thumb, 658 | input[type="range"]::-webkit-slider-thumb { 659 | -webkit-appearance: none !important; 660 | } 661 | 662 | .vertical input[type="range"]::-webkit-slider-runnable-track, 663 | input[type="range"]::-webkit-slider-runnable-track { 664 | height: 6px; 665 | background: #979797; 666 | border-radius: 3px; 667 | padding:0px !important; 668 | border: 1px solid #3d3d3d; 669 | } 670 | 671 | .vertical input[type="range"]::-webkit-slider-runnable-track { 672 | height: auto; 673 | width: 6px; 674 | } 675 | 676 | input[type="range"]::-webkit-slider-thumb { 677 | position: relative; 678 | height: 6px; 679 | width: 6px; 680 | margin-top: -5px; 681 | background: rgb(255,0,0,.2); 682 | } 683 | 684 | /* new */ 685 | input[type="range"]::-webkit-slider-thumb { 686 | position: relative; 687 | height: 16px; 688 | width: 16px; 689 | margin-top: -5px; 690 | background: rgb(255,0,0,1); 691 | } 692 | 693 | 694 | input[type="range" i]{ 695 | -webkit-appearance: none; 696 | margin: 0; 697 | } 698 | 699 | input[type="range" i]::-webkit-slider-thumb, input[type="range" i]::-webkit-media-slider-thumb { 700 | -webkit-appearance: none; 701 | box-sizing: border-box; 702 | display: block; 703 | -webkit-user-modify: read-only !important; 704 | } 705 | 706 | 707 | 708 | #thumb, 709 | input[type="range"]::-webkit-slider-thumb::before { 710 | position: absolute; 711 | content: ""; 712 | height: 5px; /* equal to height of runnable track or 1 less */ 713 | width: 500px; /* make this bigger than the widest range input element */ 714 | left: -502px; /* this should be -2px - width */ 715 | top: 8px; /* don't change this */ 716 | background: #77f; 717 | } 718 | 719 | .vertical.sdpi-item:first-child, 720 | .vertical { 721 | margin-top: 12px; 722 | margin-bottom: 16px; 723 | } 724 | .vertical > .sdpi-item-value { 725 | margin-right: 16px; 726 | } 727 | 728 | .vertical .sdpi-item-group { 729 | width: 100%; 730 | display: flex; 731 | justify-content: space-evenly; 732 | } 733 | 734 | .vertical input[type="range"] { 735 | height: 100px; 736 | width: 21px; 737 | /*-webkit-appearance: slider-vertical;*/ 738 | display: flex; 739 | flex-flow: column; 740 | } 741 | 742 | .vertical input[type="range"]::-webkit-slider-runnable-track { 743 | height: auto; 744 | width: 5px; 745 | } 746 | 747 | .vertical input[type="range"]::-webkit-slider-thumb { 748 | margin-top: 0px; 749 | margin-left: -6px; 750 | } 751 | 752 | .vertical .sdpi-item-value { 753 | flex-flow: column; 754 | align-items: flex-start; 755 | } 756 | 757 | .vertical.sdpi-item[type="range"] .sdpi-item-value { 758 | align-items: center; 759 | margin-right: 16px; 760 | text-align: center; 761 | } 762 | 763 | .vertical.sdpi-item[type="range"] .sdpi-item-value span, 764 | .vertical input[type="range"] .sdpi-item-value span { 765 | text-align: center; 766 | margin: 4px 0px; 767 | } 768 | 769 | input[type="color"] { 770 | min-width: 32px; 771 | min-height: 32px; 772 | width: 32px; 773 | height: 32px; 774 | padding: 0; 775 | background-color: #2d2d2d; 776 | flex: none; 777 | } 778 | 779 | ::-webkit-color-swatch { 780 | min-width: 24px; 781 | } 782 | 783 | textarea { 784 | height: 3em; 785 | word-break: break-word; 786 | line-height: 1.5em; 787 | } 788 | 789 | .textarea { 790 | padding: 0px !important; 791 | } 792 | 793 | input[type="textxxx"], 794 | textareaxxx { 795 | border-radius: 0pt; 796 | border: #303030 1pt solid; 797 | } 798 | 799 | textarea { 800 | width: 221px; /*98%;*/ 801 | height: 96%; 802 | min-height: 6em; 803 | resize: none; 804 | border-radius: 0; 805 | } 806 | 807 | /* CAROUSEL */ 808 | 809 | .sdpi-item[type="carousel"]{ 810 | 811 | } 812 | 813 | .sdpi-item.card-carousel-wrapper, 814 | .sdpi-item > .card-carousel-wrapper { 815 | padding: 0; 816 | } 817 | 818 | 819 | .card-carousel-wrapper { 820 | display: flex; 821 | align-items: center; 822 | justify-content: center; 823 | margin: 12px auto; 824 | color: #666a73; 825 | } 826 | 827 | .card-carousel { 828 | display: flex; 829 | justify-content: center; 830 | width: 278px; 831 | } 832 | .card-carousel--overflow-container { 833 | overflow: hidden; 834 | } 835 | .card-carousel--nav__left, 836 | .card-carousel--nav__right { 837 | /* display: inline-block; */ 838 | width: 12px; 839 | height: 12px; 840 | border-top: 2px solid #42b883; 841 | border-right: 2px solid #42b883; 842 | cursor: pointer; 843 | margin: 0 4px; 844 | transition: transform 150ms linear; 845 | } 846 | .card-carousel--nav__left[disabled], 847 | .card-carousel--nav__right[disabled] { 848 | opacity: 0.2; 849 | border-color: black; 850 | } 851 | .card-carousel--nav__left { 852 | transform: rotate(-135deg); 853 | } 854 | .card-carousel--nav__left:active { 855 | transform: rotate(-135deg) scale(0.85); 856 | } 857 | .card-carousel--nav__right { 858 | transform: rotate(45deg); 859 | } 860 | .card-carousel--nav__right:active { 861 | transform: rotate(45deg) scale(0.85); 862 | } 863 | .card-carousel-cards { 864 | display: flex; 865 | transition: transform 150ms ease-out; 866 | transform: translatex(0px); 867 | } 868 | .card-carousel-cards .card-carousel--card { 869 | margin: 0 5px; 870 | cursor: pointer; 871 | /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */ 872 | background-color: #fff; 873 | border-radius: 4px; 874 | z-index: 3; 875 | } 876 | .xxcard-carousel-cards .card-carousel--card:first-child { 877 | margin-left: 0; 878 | } 879 | .xxcard-carousel-cards .card-carousel--card:last-child { 880 | margin-right: 0; 881 | } 882 | .card-carousel-cards .card-carousel--card img { 883 | vertical-align: bottom; 884 | border-top-left-radius: 4px; 885 | border-top-right-radius: 4px; 886 | transition: opacity 150ms linear; 887 | width: 60px; 888 | } 889 | .card-carousel-cards .card-carousel--card img:hover { 890 | opacity: 0.5; 891 | } 892 | .card-carousel-cards .card-carousel--card--footer { 893 | border-top: 0; 894 | max-width: 80px; 895 | overflow: hidden; 896 | display: flex; 897 | height: 100%; 898 | flex-direction: column; 899 | } 900 | .card-carousel-cards .card-carousel--card--footer p { 901 | padding: 3px 0; 902 | margin: 0; 903 | margin-bottom: 2px; 904 | font-size: 15px; 905 | font-weight: 500; 906 | color: #2c3e50; 907 | } 908 | .card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) { 909 | font-size: 12px; 910 | font-weight: 300; 911 | padding: 6px; 912 | color: #666a73; 913 | } 914 | 915 | 916 | h1 { 917 | font-size: 3em; 918 | font-weight: 100; 919 | text-align: center; 920 | margin-bottom: 0; 921 | color: #42b883; 922 | } 923 | 924 | /* debug 925 | div { 926 | background-color: rgba(64,128,255,0.2); 927 | } 928 | */ 929 | 930 | .min80 > .sdpi-item-child { 931 | min-width: 80px; 932 | } 933 | 934 | .min100 > .sdpi-item-child { 935 | min-width: 100px; 936 | } 937 | 938 | .min120 > .sdpi-item-child { 939 | min-width: 120px; 940 | } 941 | 942 | .min140 > .sdpi-item-child { 943 | min-width: 140px; 944 | } 945 | 946 | .min160 > .sdpi-item-child { 947 | min-width: 160px; 948 | } 949 | 950 | .min200 > .sdpi-item-child { 951 | min-width: 200px; 952 | } 953 | 954 | .max40 { 955 | flex-basis: 40%; 956 | flex-grow: 0; 957 | } 958 | 959 | .max30 { 960 | flex-basis: 30%; 961 | flex-grow: 0; 962 | } 963 | 964 | .max20 { 965 | flex-basis: 20%; 966 | flex-grow: 0; 967 | } 968 | 969 | .up20 { 970 | margin-top: -20px; 971 | } 972 | 973 | 974 | ::-webkit-datetime-edit { 975 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 976 | background: url(elg_calendar_inv.svg) no-repeat left center; 977 | padding-right: 1em; 978 | padding-left: 25px; 979 | background-position: 4px 0px; 980 | } 981 | ::-webkit-datetime-edit-fields-wrapper { 982 | 983 | } 984 | ::-webkit-datetime-edit-text { padding: 0 0.3em; } 985 | ::-webkit-datetime-edit-month-field { } 986 | ::-webkit-datetime-edit-day-field {} 987 | ::-webkit-datetime-edit-year-field {} 988 | ::-webkit-inner-spin-button { 989 | 990 | /* display: none; */ 991 | } 992 | ::-webkit-calendar-picker-indicator { 993 | background: transparent; 994 | font-size: 17px; 995 | } 996 | 997 | ::-webkit-calendar-picker-indicator:focus { 998 | background-color: rgba(0,0,0,0.2); 999 | } 1000 | 1001 | input[type="date"] { 1002 | -webkit-align-items: center; 1003 | display: -webkit-inline-flex; 1004 | font-family: monospace; 1005 | overflow: hidden; 1006 | padding: 0; 1007 | -webkit-padding-start: 1px; 1008 | } 1009 | 1010 | input::-webkit-datetime-edit { 1011 | -webkit-flex: 1; 1012 | -webkit-user-modify: read-only !important; 1013 | display: inline-block; 1014 | min-width: 0; 1015 | overflow: hidden; 1016 | } 1017 | 1018 | input::-webkit-datetime-edit-fields-wrapper { 1019 | /* -webkit-user-modify: read-only !important; 1020 | display: inline-block; 1021 | padding: 1px 0; 1022 | white-space: pre; */ 1023 | 1024 | } 1025 | 1026 | /* 1027 | input[type="date"] { 1028 | background-color: red; 1029 | outline: none; 1030 | } 1031 | 1032 | input[type="date"]::-webkit-clear-button { 1033 | font-size: 18px; 1034 | height: 30px; 1035 | position: relative; 1036 | } 1037 | 1038 | input[type="date"]::-webkit-inner-spin-button { 1039 | height: 28px; 1040 | } 1041 | 1042 | input[type="date"]::-webkit-calendar-picker-indicator { 1043 | font-size: 15px; 1044 | } */ 1045 | 1046 | input[type="file"] { 1047 | opacity: 0; 1048 | display: none; 1049 | } 1050 | 1051 | .sdpi-item > input[type="file"] { 1052 | opacity: 1; 1053 | display: flex; 1054 | } 1055 | 1056 | input[type="file"] + span { 1057 | display: flex; 1058 | flex: 0 1 auto; 1059 | background-color: #0000ff50; 1060 | } 1061 | 1062 | label.sdpi-file-label { 1063 | cursor: pointer; 1064 | user-select: none; 1065 | display: inline-block; 1066 | min-height: 21px !important; 1067 | height: 21px !important; 1068 | line-height: 20px; 1069 | padding: 0px 4px; 1070 | margin: auto; 1071 | margin-right: 0px; 1072 | float:right; 1073 | } 1074 | 1075 | .sdpi-file-label > label:active, 1076 | .sdpi-file-label.file:active, 1077 | label.sdpi-file-label:active, 1078 | label.sdpi-file-info:active, 1079 | input[type="file"]::-webkit-file-upload-button:active, 1080 | button:active { 1081 | background-color: #d8d8d8; 1082 | color:#303030; 1083 | } 1084 | 1085 | 1086 | input:required:invalid, input:focus:invalid { 1087 | /* border: 1px solid red; */ 1088 | /* background: #3d3d3d url(pi_required.svg) no-repeat 98% center; */ 1089 | background: #3d3d3d url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center; 1090 | } 1091 | 1092 | input:required:valid { 1093 | /* background: #3d3d3d url(pi_required_ok.svg) no-repeat 98% center; */ 1094 | background: #3d3d3d url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center; 1095 | } 1096 | 1097 | .tooltip, 1098 | :tooltip, 1099 | :title { 1100 | color: yellow; 1101 | } 1102 | 1103 | [title]:hover { 1104 | display: flex; 1105 | align-items: center; 1106 | justify-content: center; 1107 | } 1108 | 1109 | [title]:hover::after { 1110 | content: ''; 1111 | position: absolute; 1112 | bottom: -1000px; 1113 | left: 8px; 1114 | display: none; 1115 | color: #fff; 1116 | border: 8px solid transparent; 1117 | border-bottom: 8px solid #000; 1118 | } 1119 | [title]:hover::before { 1120 | content: attr(title); 1121 | display: flex; 1122 | justify-content: center; 1123 | align-self: center; 1124 | padding: 6px 12px; 1125 | border-radius: 5px; 1126 | background: rgba(0,0,0,0.8); 1127 | color: #d8d8d8; 1128 | font-size: 12px; 1129 | font-family: sans-serif; 1130 | opacity: 1; 1131 | position: absolute; 1132 | height: auto; 1133 | /* width: 50%; 1134 | left: 35%; */ 1135 | text-align: center; 1136 | bottom: 2px; 1137 | z-index: 100; 1138 | box-shadow: 0px 3px 6px rgba(0, 0, 0, .5); 1139 | /* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); */ 1140 | } 1141 | 1142 | .sdpi-item-group.file { 1143 | width: 232px; 1144 | display: flex; 1145 | align-items: center; 1146 | } 1147 | 1148 | .sdpi-file-info { 1149 | overflow-wrap: break-word; 1150 | word-wrap: break-word; 1151 | hyphens: auto; 1152 | 1153 | min-width: 132px; 1154 | max-width: 144px; 1155 | max-height: 32px; 1156 | margin-top: 0px; 1157 | margin-left: 5px; 1158 | display: inline-block; 1159 | overflow: hidden; 1160 | padding: 6px 4px; 1161 | background-color: #3d3d3d; 1162 | } 1163 | 1164 | ::-webkit-scrollbar { 1165 | width: 8px; 1166 | } 1167 | 1168 | ::-webkit-scrollbar-track { 1169 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 1170 | } 1171 | 1172 | ::-webkit-scrollbar-thumb { 1173 | background-color: #999999; 1174 | outline: 1px solid slategrey; 1175 | border-radius: 8px; 1176 | } 1177 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/defaultImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/defaultImage.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/defaultImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/defaultImage@2x.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/icon.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/icon@2x.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/index_pi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | Property Inspector Samples PI 11 | 12 | 15 | 16 | 41 | 42 | 43 | 44 |
45 |
Plugin Error
46 |
47 |
48 | Unable To Communicate With HWiNFO64 49 |

50 | The plugin is unable to communicate with HWiNFO64 51 |

52 |

Make sure it's running and configured properly

53 |

54 | For help on how to properly setup HWiNFO64, refer to the 55 | 57 | documentation 58 | 59 |

60 |
61 |
62 |
63 | 64 |
65 |
Font Sizes
66 | 67 |
68 |
Title
69 |
70 | 8 71 | 72 | 20 73 |
74 |
75 | 76 |
77 |
Value
78 |
79 | 8 80 | 81 | 20 82 |
83 |
84 | 85 |
HWiNFO Sensors
86 | 87 |
88 |
Sensor
89 | 92 |
93 | 94 |
95 |
Reading
96 | 99 |
100 | 101 |
Value Params
102 | 103 |
104 |
Min/Max
105 | 106 |
107 | 109 | 111 |
112 |
113 | 114 |
115 | Advanced 116 |
117 |
Format
118 | 119 |
120 |
121 |
122 | Help 123 |

Format can be used to modify format of the value.

124 |

125 | For more information on how to use this field, 126 | click here. 128 |

129 |
130 |
131 | 132 |
133 |
Divisor
134 | 136 |
137 |
138 |
139 | Help 140 |

Divisor can be used to convert the value.

141 |

142 | For example, converting bytes to megabits, set the divisor to: 143 | "125" 144 |

145 |
146 |
147 |
148 | 149 |
Graph Colors
150 | 151 |
152 |
Background
153 | 154 |
Value Text
155 | 156 |
157 | 158 |
159 |
Foreground
160 | 161 |
Highlight
162 | 163 |
164 |
165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/index_pi.js: -------------------------------------------------------------------------------- 1 | // this is our global websocket, used to communicate from/to Stream Deck software 2 | // and some info about our plugin, as sent by Stream Deck software 3 | var websocket = null, 4 | uuid = null, 5 | actionInfo = {}, 6 | inInfo = {}, 7 | runningApps = [], 8 | isQT = navigator.appVersion.includes("QtWebEngine"), 9 | onchangeevt = "onchange"; // 'oninput'; // change this, if you want interactive elements act on any change, or while they're modified 10 | 11 | function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) { 12 | uuid = inUUID; 13 | // please note: the incoming arguments are of type STRING, so 14 | // in case of the inActionInfo, we must parse it into JSON first 15 | actionInfo = JSON.parse(inActionInfo); // cache the info 16 | inInfo = JSON.parse(inInfo); 17 | websocket = new WebSocket("ws://localhost:" + inPort); 18 | 19 | /** Since the PI doesn't have access to native settings 20 | * Stream Deck sends some color settings to PI 21 | * We use these to adjust some styles (e.g. highlight-colors for checkboxes) 22 | */ 23 | addDynamicStyles(inInfo.colors, "connectSocket"); 24 | initPropertyInspector(5); 25 | 26 | // if connection was established, the websocket sends 27 | // an 'onopen' event, where we need to register our PI 28 | websocket.onopen = function () { 29 | var json = { 30 | event: inRegisterEvent, 31 | uuid: inUUID, 32 | }; 33 | // register property inspector to Stream Deck 34 | websocket.send(JSON.stringify(json)); 35 | sendValueToPlugin("propertyInspectorConnected", "property_inspector"); 36 | }; 37 | 38 | websocket.onmessage = function (evt) { 39 | // Received message from Stream Deck 40 | var jsonObj = JSON.parse(evt.data); 41 | var event = jsonObj["event"]; 42 | if ( 43 | "boolean" === typeof getPropFromString(jsonObj, "payload.error") && 44 | event === "sendToPropertyInspector" 45 | ) { 46 | if (jsonObj.payload.error === true) { 47 | document.querySelector("#ui").style = "display:none"; 48 | document.querySelector("#error").style = "display:block"; 49 | } else if (jsonObj.payload.message === "show_ui") { 50 | document.querySelector("#ui").style = "display:block"; 51 | document.querySelector("#error").style = "display:none"; 52 | sendValueToPlugin("propertyInspectorConnected", "property_inspector"); 53 | } 54 | } 55 | if ( 56 | getPropFromString(jsonObj, "payload.sensors") && 57 | event === "sendToPropertyInspector" 58 | ) { 59 | addSensors( 60 | document.querySelector("#sensorSelect"), 61 | jsonObj.payload.sensors, 62 | jsonObj.payload.settings 63 | ); 64 | } 65 | if ( 66 | getPropFromString(jsonObj, "payload.readings") && 67 | event === "sendToPropertyInspector" 68 | ) { 69 | addReadings( 70 | document.querySelector("#readingSelect"), 71 | jsonObj.payload.readings, 72 | jsonObj.payload.settings 73 | ); 74 | } 75 | if (getPropFromString(jsonObj, "payload.settings")) { 76 | var settings = jsonObj.payload.settings; 77 | if (settings.min === 0 && settings.max === 0) { 78 | // don't show 0, 0 min/max 79 | } else { 80 | document.querySelector("#min").value = settings.min; 81 | document.querySelector("#max").value = settings.max; 82 | } 83 | document.querySelector("#format input").value = settings.format; 84 | document.querySelector("#divisor input").value = settings.divisor || ""; 85 | if ( 86 | settings.format.length > 0 || 87 | (settings.divisor && settings.divisor.length > 0) 88 | ) { 89 | var attr = document.createAttribute("open"); 90 | attr.value = "open"; 91 | document 92 | .querySelector("#advanced_details") 93 | .attributes.setNamedItem(attr); 94 | } 95 | if (settings.foregroundColor !== "") { 96 | document.querySelector("#foreground").value = settings.foregroundColor; 97 | } 98 | if (settings.backgroundColor !== "") { 99 | document.querySelector("#background").value = settings.backgroundColor; 100 | } 101 | if (settings.highlightColor !== "") { 102 | document.querySelector("#highlight").value = settings.highlightColor; 103 | } 104 | if (settings.valueTextColor !== "") { 105 | document.querySelector("#valuetext").value = settings.valueTextColor; 106 | } 107 | if (settings.titleFontSize !== "") { 108 | document.querySelector("#titleFontSize input").value = 109 | settings.titleFontSize || 10.5; 110 | } 111 | if (settings.valueFontSize !== "") { 112 | document.querySelector("#valueFontSize input").value = 113 | settings.valueFontSize || 10.5; 114 | } 115 | } 116 | }; 117 | } 118 | 119 | function sortBy(key) { 120 | return function (a, b) { 121 | if (a[key] > b[key]) return 1; 122 | if (b[key] > a[key]) return -1; 123 | return 0; 124 | }; 125 | } 126 | 127 | function addSensors(el, sensors, settings) { 128 | var i; 129 | for (i = el.options.length - 1; i >= 0; i--) { 130 | el.remove(i); 131 | } 132 | 133 | el.removeAttribute("disabled"); 134 | 135 | var option = document.createElement("option"); 136 | option.text = "Choose a sensor"; 137 | option.disabled = true; 138 | if (settings.isValid !== true) { 139 | option.selected = true; 140 | } 141 | el.add(option); 142 | var sortByName = sortBy("name"); 143 | sensors.sort(sortByName).forEach((s) => { 144 | var option = document.createElement("option"); 145 | option.text = s.name; 146 | option.value = s.uid; 147 | if (settings.isValid === true && settings.sensorUid === s.uid) { 148 | option.selected = true; 149 | setTimeout(function () { 150 | var event = new Event("change"); 151 | el.dispatchEvent(event); 152 | }, 0); 153 | } 154 | el.add(option); 155 | }); 156 | } 157 | 158 | function addReadings(el, readings, settings) { 159 | var i; 160 | for (i = el.options.length - 1; i >= 0; i--) { 161 | el.remove(i); 162 | } 163 | 164 | el.removeAttribute("disabled"); 165 | 166 | var option = document.createElement("option"); 167 | option.text = "Choose a reading"; 168 | option.disabled = true; 169 | if (settings.isValid !== true) { 170 | option.selected = true; 171 | } 172 | el.add(option); 173 | 174 | var sortByLabel = sortBy("label"); 175 | var maxL = 0; 176 | readings.sort(sortByLabel).forEach((r) => { 177 | var l = r.prefix.length; 178 | if (l > maxL) { 179 | maxL = l; 180 | } 181 | }); 182 | readings.sort(sortByLabel).forEach((r) => { 183 | var option = document.createElement("option"); 184 | option.style = "white-space: pre"; 185 | var spaces = " "; 186 | for (i = 0; i < maxL - r.prefix.length; ++i) { 187 | spaces += " "; 188 | } 189 | option.innerHTML = `${r.prefix}${spaces}${r.label}`; 190 | option.value = r.id; 191 | if (settings.isValid === true && settings.readingId === r.id) { 192 | option.selected = true; 193 | } 194 | el.add(option); 195 | }); 196 | } 197 | 198 | function initPropertyInspector(initDelay) { 199 | prepareDOMElements(document); 200 | } 201 | 202 | function revealSdpiWrapper() { 203 | const el = document.querySelector(".sdpi-wrapper"); 204 | el && el.classList.remove("hidden"); 205 | } 206 | 207 | // openUrl in default browser 208 | function openUrl(url) { 209 | if (websocket && websocket.readyState === 1) { 210 | const json = { 211 | event: "openUrl", 212 | payload: { 213 | url: url, 214 | }, 215 | }; 216 | websocket.send(JSON.stringify(json)); 217 | } 218 | } 219 | 220 | // our method to pass values to the plugin 221 | function sendValueToPlugin(value, param) { 222 | if (websocket && websocket.readyState === 1) { 223 | const json = { 224 | action: actionInfo["action"], 225 | event: "sendToPlugin", 226 | context: uuid, 227 | payload: { 228 | [param]: value, 229 | }, 230 | }; 231 | websocket.send(JSON.stringify(json)); 232 | } 233 | } 234 | 235 | if (!isQT) { 236 | document.addEventListener("DOMContentLoaded", function () { 237 | initPropertyInspector(100); 238 | }); 239 | } 240 | 241 | /** the beforeunload event is fired, right before the PI will remove all nodes */ 242 | window.addEventListener("beforeunload", function (e) { 243 | e.preventDefault(); 244 | sendValueToPlugin("propertyInspectorWillDisappear", "property_inspector"); 245 | // Don't set a returnValue to the event, otherwise Chromium with throw an error. // e.returnValue = ''; 246 | }); 247 | 248 | /** the pagehide event is fired, when the view disappears */ 249 | /* 250 | window.addEventListener('pagehide', function (event) { 251 | console.log('%c%s','background: green; font-size: 22px; font-weight: bold;','window --->> pagehide.'); 252 | sendValueToPlugin('propertyInspectorPagehide', 'property_inspector'); 253 | 254 | }); 255 | */ 256 | 257 | /** the unload event is fired, when the PI will finally disappear */ 258 | /* 259 | window.addEventListener('unload', function (event) { 260 | console.log('%c%s','background: orange; font-size: 22px; font-weight: bold;','window --->> onunload.'); 261 | sendValueToPlugin('propertyInspectorDisconnected', 'property_inspector'); 262 | }); 263 | */ 264 | 265 | /** if you prefer, you can apply these listeners to PI's body, like so: 266 | * 267 | * 268 | * 269 | * 270 | */ 271 | 272 | /** CREATE INTERACTIVE HTML-DOM 273 | * where elements can be clicked or act on their 'change' event. 274 | * Messages are then processed using the 'handleSdpiItemClick' method below. 275 | */ 276 | 277 | function prepareDOMElements(baseElement) { 278 | baseElement = baseElement || document; 279 | Array.from(baseElement.querySelectorAll(".sdpi-item-value")).forEach( 280 | (el, i) => { 281 | const elementsToClick = [ 282 | "BUTTON", 283 | "OL", 284 | "UL", 285 | "TABLE", 286 | "METER", 287 | "PROGRESS", 288 | "CANVAS", 289 | ].includes(el.tagName); 290 | const evt = elementsToClick ? "onclick" : onchangeevt || "onchange"; 291 | // console.log(el.type, el.tagName, elementsToClick, el, evt); 292 | 293 | /** Look for combinations, where we consider the span as label for the input 294 | * we don't use `labels` for that, because a range could have 2 labels. 295 | */ 296 | const inputGroup = el.querySelectorAll("input, span"); 297 | if (inputGroup.length === 2) { 298 | const offs = inputGroup[0].tagName === "INPUT" ? 1 : 0; 299 | inputGroup[offs].innerText = inputGroup[1 - offs].value; 300 | inputGroup[1 - offs]["oninput"] = function () { 301 | inputGroup[offs].innerText = inputGroup[1 - offs].value; 302 | }; 303 | } 304 | /** We look for elements which have an 'clickable' attribute 305 | * we use these e.g. on an 'inputGroup' () to adjust the value of 306 | * the corresponding range-control 307 | */ 308 | Array.from(el.querySelectorAll(".clickable")).forEach((subel, subi) => { 309 | subel["onclick"] = function (e) { 310 | handleSdpiItemClick(e.target, subi); 311 | }; 312 | }); 313 | el[evt] = function (e) { 314 | handleSdpiItemClick(e.target, i); 315 | }; 316 | } 317 | ); 318 | 319 | baseElement.querySelectorAll("textarea").forEach((e) => { 320 | const maxl = e.getAttribute("maxlength"); 321 | e.targets = baseElement.querySelectorAll(`[for='${e.id}']`); 322 | if (e.targets.length) { 323 | let fn = () => { 324 | for (let x of e.targets) { 325 | x.innerText = maxl 326 | ? `${e.value.length}/${maxl}` 327 | : `${e.value.length}`; 328 | } 329 | }; 330 | fn(); 331 | e.onkeyup = fn; 332 | } 333 | }); 334 | } 335 | 336 | function handleSdpiItemClick(e, idx) { 337 | /** Following items are containers, so we won't handle clicks on them */ 338 | if (["OL", "UL", "TABLE"].includes(e.tagName)) { 339 | return; 340 | } 341 | // console.log('--- handleSdpiItemClick ---', e, `type: ${e.type}`, e.tagName, `inner: ${e.innerText}`); 342 | 343 | /** SPANS are used inside a control as 'labels' 344 | * If a SPAN element calls this function, it has a class of 'clickable' set and is thereby handled as 345 | * clickable label. 346 | */ 347 | 348 | if (e.tagName === "SPAN") { 349 | const inp = e.parentNode.querySelector("input"); 350 | if (e.getAttribute("value")) { 351 | return inp && (inp.value = e.getAttribute("value")); 352 | } 353 | } 354 | 355 | const selectedElements = []; 356 | const isList = ["LI", "OL", "UL", "DL", "TD"].includes(e.tagName); 357 | const sdpiItem = e.closest(".sdpi-item"); 358 | const sdpiItemGroup = e.closest(".sdpi-item-group"); 359 | let sdpiItemChildren = isList 360 | ? sdpiItem.querySelectorAll(e.tagName === "LI" ? "li" : "td") 361 | : sdpiItem.querySelectorAll(".sdpi-item-child > input"); 362 | 363 | if (isList) { 364 | const siv = e.closest(".sdpi-item-value"); 365 | if (!siv.classList.contains("multi-select")) { 366 | for (let x of sdpiItemChildren) x.classList.remove("selected"); 367 | } 368 | if (!siv.classList.contains("no-select")) { 369 | e.classList.toggle("selected"); 370 | } 371 | } 372 | 373 | if (sdpiItemGroup && !sdpiItemChildren.length) { 374 | for (let x of ["input", "meter", "progress"]) { 375 | sdpiItemChildren = sdpiItemGroup.querySelectorAll(x); 376 | if (sdpiItemChildren.length) break; 377 | } 378 | } 379 | 380 | if (e.selectedIndex) { 381 | idx = e.selectedIndex; 382 | } else { 383 | sdpiItemChildren.forEach((ec, i) => { 384 | if (ec.classList.contains("selected")) { 385 | selectedElements.push(ec.innerText); 386 | } 387 | if (ec === e) idx = i; 388 | }); 389 | } 390 | 391 | const returnValue = { 392 | key: e.id || sdpiItem.id, 393 | value: isList 394 | ? e.innerText 395 | : e.value 396 | ? e.type === "file" 397 | ? decodeURIComponent(e.value.replace(/^C:\\fakepath\\/, "")) 398 | : e.value 399 | : e.getAttribute("value"), 400 | group: sdpiItemGroup ? sdpiItemGroup.id : false, 401 | index: idx, 402 | selection: selectedElements, 403 | checked: e.checked, 404 | }; 405 | 406 | /** Just simulate the original file-selector: 407 | * If there's an element of class '.sdpi-file-info' 408 | * show the filename there 409 | */ 410 | if (e.type === "file") { 411 | const info = sdpiItem.querySelector(".sdpi-file-info"); 412 | if (info) { 413 | const s = returnValue.value.split("/").pop(); 414 | info.innerText = 415 | s.length > 28 416 | ? s.substr(0, 10) + "..." + s.substr(s.length - 10, s.length) 417 | : s; 418 | } 419 | } 420 | 421 | sendValueToPlugin(returnValue, "sdpi_collection"); 422 | } 423 | 424 | function updateKeyForDemoCanvas(cnv) { 425 | sendValueToPlugin( 426 | { 427 | key: "your_canvas", 428 | value: cnv.toDataURL(), 429 | }, 430 | "sdpi_collection" 431 | ); 432 | } 433 | 434 | /** Stream Deck software passes system-highlight color information 435 | * to Property Inspector. Here we 'inject' the CSS styles into the DOM 436 | * when we receive this information. */ 437 | 438 | function addDynamicStyles(clrs, fromWhere) { 439 | const node = 440 | document.getElementById("#sdpi-dynamic-styles") || 441 | document.createElement("style"); 442 | if (!clrs.mouseDownColor) 443 | clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100); 444 | const clr = clrs.highlightColor.slice(0, 7); 445 | const clr1 = fadeColor(clr, 100); 446 | const clr2 = fadeColor(clr, 60); 447 | const metersActiveColor = fadeColor(clr, -60); 448 | 449 | node.setAttribute("id", "sdpi-dynamic-styles"); 450 | node.innerHTML = ` 451 | 452 | input[type="radio"]:checked + label span, 453 | input[type="checkbox"]:checked + label span { 454 | background-color: ${clrs.highlightColor}; 455 | } 456 | 457 | input[type="radio"]:active:checked + label span, 458 | input[type="radio"]:active + label span, 459 | input[type="checkbox"]:active:checked + label span, 460 | input[type="checkbox"]:active + label span { 461 | background-color: ${clrs.mouseDownColor}; 462 | } 463 | 464 | input[type="radio"]:active + label span, 465 | input[type="checkbox"]:active + label span { 466 | background-color: ${clrs.buttonPressedBorderColor}; 467 | } 468 | 469 | td.selected, 470 | td.selected:hover, 471 | li.selected:hover, 472 | li.selected { 473 | color: white; 474 | background-color: ${clrs.highlightColor}; 475 | } 476 | 477 | .sdpi-file-label > label:active, 478 | .sdpi-file-label.file:active, 479 | label.sdpi-file-label:active, 480 | label.sdpi-file-info:active, 481 | input[type="file"]::-webkit-file-upload-button:active, 482 | button:active { 483 | background-color: ${clrs.buttonPressedBackgroundColor}; 484 | color: ${clrs.buttonPressedTextColor}; 485 | border-color: ${clrs.buttonPressedBorderColor}; 486 | } 487 | 488 | ::-webkit-progress-value, 489 | meter::-webkit-meter-optimum-value { 490 | background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2}) 491 | } 492 | 493 | ::-webkit-progress-value:active, 494 | meter::-webkit-meter-optimum-value:active { 495 | background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr}) 496 | } 497 | `; 498 | document.body.appendChild(node); 499 | } 500 | 501 | /** UTILITIES */ 502 | 503 | /** Helper function to construct a list of running apps 504 | * from a template string. 505 | * -> information about running apps is received from the plugin 506 | */ 507 | 508 | function sdpiCreateList(el, obj, cb) { 509 | if (el) { 510 | el.style.display = obj.value.length ? "block" : "none"; 511 | Array.from(document.querySelectorAll(`.${el.id}`)).forEach((subel, i) => { 512 | subel.style.display = obj.value.length ? "flex" : "none"; 513 | }); 514 | if (obj.value.length) { 515 | el.innerHTML = `
518 |
${obj.label || ""}
519 |
    521 | ${obj.value.map((e) => `
  • ${e.name}
  • `).join("")} 522 |
523 |
`; 524 | setTimeout(function () { 525 | prepareDOMElements(el); 526 | if (cb) cb(); 527 | }, 10); 528 | return; 529 | } 530 | } 531 | if (cb) cb(); 532 | } 533 | 534 | /** get a JSON property from a (dot-separated) string 535 | * Works on nested JSON, e.g.: 536 | * jsn = { 537 | * propA: 1, 538 | * propB: 2, 539 | * propC: { 540 | * subA: 3, 541 | * subB: { 542 | * testA: 5, 543 | * testB: 'Hello' 544 | * } 545 | * } 546 | * } 547 | * getPropFromString(jsn,'propC.subB.testB') will return 'Hello'; 548 | */ 549 | const getPropFromString = (jsn, str, sep = ".") => { 550 | const arr = str.split(sep); 551 | return arr.reduce( 552 | (obj, key) => (obj && obj.hasOwnProperty(key) ? obj[key] : undefined), 553 | jsn 554 | ); 555 | }; 556 | 557 | /* 558 | Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account) 559 | Usage: 560 | fadeColor('#061261', 100); // will lighten the color 561 | fadeColor('#200867'), -100); // will darken the color 562 | */ 563 | function fadeColor(col, amt) { 564 | const min = Math.min, 565 | max = Math.max; 566 | const num = parseInt(col.replace(/#/g, ""), 16); 567 | const r = min(255, max((num >> 16) + amt, 0)); 568 | const g = min(255, max((num & 0x0000ff) + amt, 0)); 569 | const b = min(255, max(((num >> 8) & 0x00ff) + amt, 0)); 570 | return "#" + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0); 571 | } 572 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/launch-hwinfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/launch-hwinfo.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "SDKVersion": 2, 3 | "Software": { 4 | "MinimumVersion": "4.1" 5 | }, 6 | "Actions": [ 7 | { 8 | "Icon": "icon", 9 | "Name": "HWiNFO", 10 | "States": [ 11 | { 12 | "Image": "defaultImage", 13 | "TitleAlignment": "top", 14 | "FontSize": "9", 15 | "TitleColor": "#b7b7b7", 16 | "ShowTitle": false 17 | } 18 | ], 19 | "SupportedInMultiActions": false, 20 | "Tooltip": "Display sensor readings from HWiNFO", 21 | "UUID": "com.exension.hwinfo.reading" 22 | } 23 | ], 24 | "Author": "shayne", 25 | "CodePathWin": "hwinfo.exe", 26 | "PropertyInspectorPath": "index_pi.html", 27 | "Description": "Display sensor readings from HWiNFO64. This plugin is not affiliated with HWiNFO64, for more information and to download HWiNFO64 visit https://www.hwinfo.com", 28 | "Name": "HWiNFO", 29 | "Icon": "pluginIcon", 30 | "URL": "https://github.com/shayne/hwinfo-streamdeck", 31 | "Version": "2.0.5", 32 | "ApplicationsToMonitor": { 33 | "windows": ["HWiNFO64.EXE", "HWiNFO64.exe"] 34 | }, 35 | "OS": [ 36 | { 37 | "Platform": "windows", 38 | "MinimumVersion": "10" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/pluginIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/pluginIcon.png -------------------------------------------------------------------------------- /com.exension.hwinfo.sdPlugin/pluginIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.sdPlugin/pluginIcon@2x.png -------------------------------------------------------------------------------- /com.exension.hwinfo.streamDeckPlugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/com.exension.hwinfo.streamDeckPlugin -------------------------------------------------------------------------------- /examples/bench/DejaVuSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/examples/bench/DejaVuSans-Bold.ttf -------------------------------------------------------------------------------- /examples/bench/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | func main() { 11 | _, filename, _, _ := runtime.Caller(0) 12 | fmt.Println("Current test filename: " + filename) 13 | os.Chdir(filepath.Dir(filename)) 14 | } 15 | -------------------------------------------------------------------------------- /examples/bench/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "testing" 10 | 11 | "github.com/shayne/hwinfo-streamdeck/pkg/graph" 12 | ) 13 | 14 | func BenchmarkFoo(b *testing.B) { 15 | _, filename, _, _ := runtime.Caller(0) 16 | fmt.Println("Current test filename: " + filename) 17 | os.Chdir(filepath.Dir(filename)) 18 | 19 | g := graph.NewGraph(72, 72, 0., 100., 20 | &color.RGBA{255, 255, 255, 255}, 21 | &color.RGBA{0, 0, 0, 255}, 22 | &color.RGBA{255, 255, 255, 255}) 23 | g.SetLabel(0, "CPU °C", 15, &color.RGBA{183, 183, 183, 255}) 24 | g.SetLabel(1, "5%", 40, &color.RGBA{255, 255, 255, 255}) 25 | 26 | data := []float64{ 27 | 0., 0., 0., 0., 0., 28 | 10., 10., 10., 10., 10., 29 | 20., 20., 20., 20., 20., 30 | 30., 30., 30., 30., 30., 31 | 40., 40., 40., 40., 40., 32 | 50., 50., 50., 50., 50., 33 | 60., 60., 60., 60., 60., 34 | 70., 70., 70., 70., 70., 35 | 80., 80., 80., 80., 80., 36 | 90., 90., 90., 90., 90., 37 | 100., 100., 100., 100., 100., 38 | // 0., 0., 0., 0., 0., 39 | // 10., 10., 10., 10., 10., 40 | // 20., 20., 20., 20., 20., 41 | // 30., 30., 30., 30., 30., 42 | // 40., 40., 40., 40., 40., 43 | // 50., 50., 50., 50., 50., 44 | // 60., 60., 60., 60., 60., 45 | // 70., 70., 70., 70., 70., 46 | // 80., 80., 80., 80., 80., 47 | // 90., 90., 90., 90., 90., 48 | // 100., 100., 100., 100., 100., 49 | } 50 | _ = data // FIXME 51 | for i := 0; i < b.N; i++ { 52 | // FIXME: updateChart does not exist 53 | // for _, v := range data { 54 | // g.UpdateChart(v) 55 | // } 56 | _, err := g.EncodePNG() 57 | if err != nil { 58 | b.Fatal("failed to encode png") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/graph/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "io/ioutil" 7 | "log" 8 | "math/rand" 9 | "time" 10 | 11 | "github.com/shayne/hwinfo-streamdeck/pkg/graph" 12 | ) 13 | 14 | const ( 15 | dev = 40 16 | ) 17 | 18 | func main() { 19 | g := graph.NewGraph(72, 72, 0., 100., 20 | &color.RGBA{255, 255, 255, 255}, 21 | &color.RGBA{0, 0, 0, 255}, 22 | &color.RGBA{255, 255, 255, 255}) 23 | g.SetLabel(0, "CPU °C", 15, &color.RGBA{183, 183, 183, 255}) 24 | g.SetLabel(1, "5%", 40, &color.RGBA{255, 255, 255, 255}) 25 | 26 | data := makeFakeData() 27 | // data := []float64{ 28 | // 0., 0., 0., 0., 0., 29 | // 10., 10., 10., 10., 10., 30 | // 20., 20., 20., 20., 20., 31 | // 30., 30., 30., 30., 30., 32 | // 40., 40., 40., 40., 40., 33 | // 50., 50., 50., 50., 50., 34 | // 60., 60., 60., 60., 60., 35 | // 70., 70., 70., 70., 70., 36 | // 80., 80., 80., 80., 80., 37 | // 90., 90., 90., 90., 90., 38 | // 100., 100., 100., 100., 100., 39 | // } 40 | for _, v := range data { 41 | g.Update(v) 42 | } 43 | lastv := data[len(data)-1] 44 | 45 | ticker := time.NewTicker(time.Second) 46 | for { 47 | select { 48 | case <-ticker.C: 49 | s := rand.NewSource(time.Now().UnixNano()) 50 | r := rand.New(s) 51 | ndev := r.Intn(dev) - (dev / 2) 52 | v := lastv + float64(ndev) 53 | if v > 100 { 54 | v = 100 55 | } else if v < 0 { 56 | v = 0 57 | } 58 | fmt.Println(v) 59 | g.Update(v) 60 | lastv = v 61 | bts, err := g.EncodePNG() 62 | if err != nil { 63 | log.Fatal("failed to encode png") 64 | } 65 | err = ioutil.WriteFile("graph.png", bts, 0644) 66 | if err != nil { 67 | log.Fatal("failed to write png") 68 | } 69 | } 70 | } 71 | } 72 | 73 | func makeFakeData() []float64 { 74 | s := rand.NewSource(time.Now().UnixNano()) 75 | r := rand.New(s) 76 | data := make([]float64, 72) 77 | v := r.Intn(100) 78 | lastv := v 79 | data[0] = float64(v) 80 | for i := 1; i < 72; i++ { 81 | ndev := r.Intn(dev) - (dev / 2) 82 | v = lastv + ndev 83 | data[i] = float64(v) 84 | } 85 | return data 86 | } 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shayne/hwinfo-streamdeck 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 7 | github.com/golang/protobuf v1.5.2 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/hashicorp/go-plugin v1.4.8 10 | github.com/shayne/go-winpeg v0.0.0-20200807055429-803ae16a07c6 11 | golang.org/x/image v0.3.0 12 | golang.org/x/sys v0.4.0 13 | golang.org/x/text v0.6.0 14 | google.golang.org/grpc v1.52.0 15 | google.golang.org/protobuf v1.28.1 16 | ) 17 | 18 | require ( 19 | github.com/fatih/color v1.13.0 // indirect 20 | github.com/hashicorp/go-hclog v1.4.0 // indirect 21 | github.com/hashicorp/yamux v0.1.1 // indirect 22 | github.com/mattn/go-colorable v0.1.13 // indirect 23 | github.com/mattn/go-isatty v0.0.17 // indirect 24 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 25 | github.com/oklog/run v1.1.0 // indirect 26 | golang.org/x/net v0.5.0 // indirect 27 | google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 5 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 8 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 9 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 10 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 11 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 12 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 13 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 14 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 15 | github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= 16 | github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 17 | github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM= 18 | github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= 19 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 20 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 21 | github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= 22 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 23 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 24 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 25 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 26 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 27 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 28 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 29 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 30 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 31 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 32 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 33 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 34 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/shayne/go-winpeg v0.0.0-20200807055429-803ae16a07c6 h1:HKw6S9JJ7+Z4jc0ygiefl253IwWgk4/ohsQ/5tFWVs0= 38 | github.com/shayne/go-winpeg v0.0.0-20200807055429-803ae16a07c6/go.mod h1:gkGydh7Q4gy2dbfmfQ5++JV6nn8jb1iRj62BWtNPQOg= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 41 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 42 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 45 | golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= 46 | golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= 47 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 48 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 49 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 50 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 51 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 52 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 53 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 56 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20200806125547-5acd03effb82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 68 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 70 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 74 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 75 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 78 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 79 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5 h1:wJT65XLOzhpSPCdAmmKfz94SlmnQzDzjm3Cj9k3fsXY= 82 | google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= 83 | google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= 84 | google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= 85 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 86 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 87 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 88 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 91 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | -------------------------------------------------------------------------------- /images/clicksettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/clicksettings.png -------------------------------------------------------------------------------- /images/configureaction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/configureaction.gif -------------------------------------------------------------------------------- /images/contextquit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/contextquit.png -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/demo.gif -------------------------------------------------------------------------------- /images/dragaction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/dragaction.gif -------------------------------------------------------------------------------- /images/recommendedsettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/recommendedsettings.png -------------------------------------------------------------------------------- /images/sensorsonly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/sensorsonly.png -------------------------------------------------------------------------------- /images/sharedmemory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/sharedmemory.png -------------------------------------------------------------------------------- /images/streamdeckactionlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/streamdeckactionlist.png -------------------------------------------------------------------------------- /images/streamdeckinstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shayne/hwinfo-streamdeck/6d7ea32a4e5487088f6158e10bffec889e665c07/images/streamdeckinstall.png -------------------------------------------------------------------------------- /install-plugin.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | CALL .\kill-streamdeck.bat 3 | xcopy com.exension.hwinfo.sdPlugin %APPDATA%\\Elgato\\StreamDeck\\Plugins\\com.exension.hwinfo.sdPlugin\\ /E /Q /Y 4 | CALL .\start-streamdeck.bat -------------------------------------------------------------------------------- /internal/app/hwinfostreamdeckplugin/action_manager.go: -------------------------------------------------------------------------------- 1 | package hwinfostreamdeckplugin 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type actionManager struct { 10 | mux sync.RWMutex 11 | actions map[string]*actionData 12 | } 13 | 14 | func newActionManager() *actionManager { 15 | return &actionManager{actions: make(map[string]*actionData)} 16 | } 17 | 18 | func (tm *actionManager) Run(updateTiles func(*actionData)) { 19 | go func() { 20 | ticker := time.NewTicker(time.Second) 21 | for range ticker.C { 22 | tm.mux.RLock() 23 | for _, data := range tm.actions { 24 | if data.settings.IsValid { 25 | updateTiles(data) 26 | } 27 | } 28 | tm.mux.RUnlock() 29 | } 30 | }() 31 | } 32 | 33 | func (tm *actionManager) SetAction(action, context string, settings *actionSettings) { 34 | tm.mux.Lock() 35 | tm.actions[context] = &actionData{action, context, settings} 36 | tm.mux.Unlock() 37 | } 38 | 39 | func (tm *actionManager) RemoveAction(context string) { 40 | tm.mux.Lock() 41 | delete(tm.actions, context) 42 | tm.mux.Unlock() 43 | } 44 | 45 | func (tm *actionManager) getSettings(context string) (actionSettings, error) { 46 | tm.mux.RLock() 47 | data, ok := tm.actions[context] 48 | tm.mux.RUnlock() 49 | if !ok { 50 | return actionSettings{}, fmt.Errorf("getSettings invalid key: %s", context) 51 | } 52 | // return full copy of settings, not reference to stored settings 53 | return *data.settings, nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/app/hwinfostreamdeckplugin/delegate.go: -------------------------------------------------------------------------------- 1 | package hwinfostreamdeckplugin 2 | 3 | import ( 4 | "encoding/json" 5 | "image/color" 6 | "log" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/shayne/hwinfo-streamdeck/pkg/graph" 10 | "github.com/shayne/hwinfo-streamdeck/pkg/streamdeck" 11 | ) 12 | 13 | const ( 14 | tileWidth = 72 15 | tileHeight = 72 16 | ) 17 | 18 | // OnConnected event 19 | func (p *Plugin) OnConnected(c *websocket.Conn) { 20 | log.Println("OnConnected") 21 | } 22 | 23 | // OnWillAppear event 24 | func (p *Plugin) OnWillAppear(event *streamdeck.EvWillAppear) { 25 | var settings actionSettings 26 | err := json.Unmarshal(*event.Payload.Settings, &settings) 27 | if err != nil { 28 | log.Println("OnWillAppear settings unmarshal", err) 29 | } 30 | tfSize := 10.5 31 | vfSize := 10.5 32 | var fgColor *color.RGBA 33 | var bgColor *color.RGBA 34 | var hlColor *color.RGBA 35 | var tColor *color.RGBA 36 | var vtColor *color.RGBA 37 | if settings.TitleFontSize != 0 { 38 | tfSize = settings.TitleFontSize 39 | } 40 | if settings.ValueFontSize != 0 { 41 | vfSize = settings.ValueFontSize 42 | } 43 | if settings.ForegroundColor == "" { 44 | fgColor = &color.RGBA{0, 81, 40, 255} 45 | } else { 46 | fgColor = hexToRGBA(settings.ForegroundColor) 47 | } 48 | if settings.BackgroundColor == "" { 49 | bgColor = &color.RGBA{0, 0, 0, 255} 50 | } else { 51 | bgColor = hexToRGBA(settings.BackgroundColor) 52 | } 53 | if settings.HighlightColor == "" { 54 | hlColor = &color.RGBA{0, 158, 0, 255} 55 | } else { 56 | hlColor = hexToRGBA(settings.HighlightColor) 57 | } 58 | if settings.TitleColor == "" { 59 | tColor = &color.RGBA{183, 183, 183, 255} 60 | } else { 61 | tColor = hexToRGBA(settings.TitleColor) 62 | } 63 | if settings.ValueTextColor == "" { 64 | vtColor = &color.RGBA{255, 255, 255, 255} 65 | } else { 66 | vtColor = hexToRGBA(settings.ValueTextColor) 67 | } 68 | g := graph.NewGraph(tileWidth, tileHeight, settings.Min, settings.Max, fgColor, bgColor, hlColor) 69 | g.SetLabel(0, "", 19, tColor) 70 | g.SetLabelFontSize(0, tfSize) 71 | g.SetLabel(1, "", 44, vtColor) 72 | g.SetLabelFontSize(1, vfSize) 73 | p.graphs[event.Context] = g 74 | p.am.SetAction(event.Action, event.Context, &settings) 75 | } 76 | 77 | // OnWillDisappear event 78 | func (p *Plugin) OnWillDisappear(event *streamdeck.EvWillDisappear) { 79 | var settings actionSettings 80 | err := json.Unmarshal(*event.Payload.Settings, &settings) 81 | if err != nil { 82 | log.Println("OnWillAppear settings unmarshal", err) 83 | } 84 | delete(p.graphs, event.Context) 85 | p.am.RemoveAction(event.Context) 86 | } 87 | 88 | // OnApplicationDidLaunch event 89 | func (p *Plugin) OnApplicationDidLaunch(event *streamdeck.EvApplication) { 90 | p.appLaunched = true 91 | } 92 | 93 | // OnApplicationDidTerminate event 94 | func (p *Plugin) OnApplicationDidTerminate(event *streamdeck.EvApplication) { 95 | p.appLaunched = false 96 | } 97 | 98 | // OnTitleParametersDidChange event 99 | func (p *Plugin) OnTitleParametersDidChange(event *streamdeck.EvTitleParametersDidChange) { 100 | var settings actionSettings 101 | err := json.Unmarshal(*event.Payload.Settings, &settings) 102 | if err != nil { 103 | log.Println("OnWillAppear settings unmarshal", err) 104 | } 105 | g, ok := p.graphs[event.Context] 106 | if !ok { 107 | log.Printf("handleSetMax no graph for context: %s\n", event.Context) 108 | return 109 | } 110 | g.SetLabelText(0, event.Payload.Title) 111 | if event.Payload.TitleParameters.TitleColor != "" { 112 | tClr := hexToRGBA(event.Payload.TitleParameters.TitleColor) 113 | g.SetLabelColor(0, tClr) 114 | } 115 | 116 | settings.Title = event.Payload.Title 117 | settings.TitleColor = event.Payload.TitleParameters.TitleColor 118 | err = p.sd.SetSettings(event.Context, &settings) 119 | if err != nil { 120 | log.Printf("handleSetTitle SetSettings: %v\n", err) 121 | return 122 | } 123 | p.am.SetAction(event.Action, event.Context, &settings) 124 | } 125 | 126 | // OnPropertyInspectorConnected event 127 | func (p *Plugin) OnPropertyInspectorConnected(event *streamdeck.EvSendToPlugin) { 128 | settings, err := p.am.getSettings(event.Context) 129 | if err != nil { 130 | log.Println("OnPropertyInspectorConnected getSettings", err) 131 | } 132 | sensors, err := p.hw.Sensors() 133 | if err != nil { 134 | log.Println("OnPropertyInspectorConnected Sensors", err) 135 | payload := evStatus{Error: true, Message: "HWiNFO Unavailable"} 136 | err := p.sd.SendToPropertyInspector(event.Action, event.Context, payload) 137 | settings.InErrorState = true 138 | err = p.sd.SetSettings(event.Context, &settings) 139 | if err != nil { 140 | log.Printf("OnPropertyInspectorConnected SetSettings: %v\n", err) 141 | return 142 | } 143 | p.am.SetAction(event.Action, event.Context, &settings) 144 | if err != nil { 145 | log.Println("updateTiles SendToPropertyInspector", err) 146 | } 147 | return 148 | } 149 | evsensors := make([]*evSendSensorsPayloadSensor, 0, len(sensors)) 150 | for _, s := range sensors { 151 | evsensors = append(evsensors, &evSendSensorsPayloadSensor{UID: s.ID(), Name: s.Name()}) 152 | } 153 | payload := evSendSensorsPayload{Sensors: evsensors, Settings: &settings} 154 | err = p.sd.SendToPropertyInspector(event.Action, event.Context, payload) 155 | if err != nil { 156 | log.Println("OnPropertyInspectorConnected SendToPropertyInspector", err) 157 | } 158 | } 159 | 160 | // OnSendToPlugin event 161 | func (p *Plugin) OnSendToPlugin(event *streamdeck.EvSendToPlugin) { 162 | var payload map[string]*json.RawMessage 163 | err := json.Unmarshal(*event.Payload, &payload) 164 | if err != nil { 165 | log.Println("OnSendToPlugin unmarshal", err) 166 | } 167 | if data, ok := payload["sdpi_collection"]; ok { 168 | sdpi := evSdpiCollection{} 169 | err = json.Unmarshal(*data, &sdpi) 170 | if err != nil { 171 | log.Println("SDPI unmarshal", err) 172 | } 173 | switch sdpi.Key { 174 | case "sensorSelect": 175 | err = p.handleSensorSelect(event, &sdpi) 176 | if err != nil { 177 | log.Println("handleSensorSelect", err) 178 | } 179 | case "readingSelect": 180 | err = p.handleReadingSelect(event, &sdpi) 181 | if err != nil { 182 | log.Println("handleReadingSelect", err) 183 | } 184 | case "min": 185 | err := p.handleSetMin(event, &sdpi) 186 | if err != nil { 187 | log.Println("handleSetMin", err) 188 | } 189 | case "max": 190 | err := p.handleSetMax(event, &sdpi) 191 | if err != nil { 192 | log.Println("handleSetMax", err) 193 | } 194 | case "format": 195 | err := p.handleSetFormat(event, &sdpi) 196 | if err != nil { 197 | log.Println("handleSetFormat", err) 198 | } 199 | case "divisor": 200 | err := p.handleDivisor(event, &sdpi) 201 | if err != nil { 202 | log.Println("handleDivisor", err) 203 | } 204 | case "foreground", "background", "highlight", "valuetext": 205 | err := p.handleColorChange(event, sdpi.Key, &sdpi) 206 | if err != nil { 207 | log.Println("handleColorChange", err) 208 | } 209 | case "titleFontSize", "valueFontSize": 210 | err := p.handleSetFontSize(event, sdpi.Key, &sdpi) 211 | if err != nil { 212 | log.Println("handleSetTitleFontSize", err) 213 | } 214 | default: 215 | log.Printf("Unknown sdpi key: %s\n", sdpi.Key) 216 | } 217 | return 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /internal/app/hwinfostreamdeckplugin/handlers.go: -------------------------------------------------------------------------------- 1 | package hwinfostreamdeckplugin 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "strconv" 7 | 8 | hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" 9 | "github.com/shayne/hwinfo-streamdeck/pkg/streamdeck" 10 | ) 11 | 12 | func (p *Plugin) handleSensorSelect(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { 13 | sensorid := sdpi.Value 14 | readings, err := p.hw.ReadingsForSensorID(sensorid) 15 | if err != nil { 16 | return fmt.Errorf("handleSensorSelect ReadingsBySensor failed: %v", err) 17 | } 18 | evreadings := []*evSendReadingsPayloadReading{} 19 | for _, r := range readings { 20 | evreadings = append(evreadings, &evSendReadingsPayloadReading{ID: r.ID(), Label: r.Label(), Prefix: r.Unit()}) 21 | } 22 | settings, err := p.am.getSettings(event.Context) 23 | if err != nil { 24 | return fmt.Errorf("handleReadingSelect getSettings: %v", err) 25 | } 26 | // only update settings if SensorUID is changing 27 | // this covers case where PI sends event when tile 28 | // selected in SD UI 29 | if settings.SensorUID != sensorid { 30 | settings.SensorUID = sensorid 31 | settings.ReadingID = 0 32 | settings.IsValid = false 33 | } 34 | payload := evSendReadingsPayload{Readings: evreadings, Settings: &settings} 35 | err = p.sd.SendToPropertyInspector(event.Action, event.Context, payload) 36 | if err != nil { 37 | return fmt.Errorf("sensorsSelect SendToPropertyInspector: %v", err) 38 | } 39 | err = p.sd.SetSettings(event.Context, &settings) 40 | if err != nil { 41 | return fmt.Errorf("handleSensorSelect SetSettings: %v", err) 42 | } 43 | p.am.SetAction(event.Action, event.Context, &settings) 44 | return nil 45 | } 46 | 47 | func getDefaultMinMaxForReading(r hwsensorsservice.Reading) (int, int) { 48 | switch r.Unit() { 49 | case "%": 50 | return 0, 100 51 | case "Yes/No": 52 | return 0, 1 53 | } 54 | min := r.ValueMin() 55 | max := r.ValueMax() 56 | min -= min * .2 57 | if min <= 0 { 58 | min = 0. 59 | } 60 | max += max * .2 61 | return int(min), int(max) 62 | } 63 | 64 | func (p *Plugin) handleReadingSelect(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { 65 | rid64, err := strconv.ParseInt(sdpi.Value, 10, 32) 66 | if err != nil { 67 | return fmt.Errorf("handleReadingSelect Atoi failed: %s, %v", sdpi.Value, err) 68 | } 69 | rid := int32(rid64) 70 | settings, err := p.am.getSettings(event.Context) 71 | if err != nil { 72 | return fmt.Errorf("handleReadingSelect getSettings: %v", err) 73 | } 74 | 75 | // no action if reading didn't change 76 | if settings.ReadingID == rid { 77 | return nil 78 | } 79 | 80 | settings.ReadingID = rid 81 | 82 | // set default min/max 83 | r, err := p.getReading(settings.SensorUID, settings.ReadingID) 84 | if err != nil { 85 | return fmt.Errorf("handleReadingSelect getReading: %v", err) 86 | } 87 | 88 | g, ok := p.graphs[event.Context] 89 | if !ok { 90 | return fmt.Errorf("handleReadingSelect no graph for context: %s", event.Context) 91 | } 92 | defaultMin, defaultMax := getDefaultMinMaxForReading(r) 93 | settings.Min = defaultMin 94 | g.SetMin(settings.Min) 95 | settings.Max = defaultMax 96 | g.SetMax(settings.Max) 97 | settings.IsValid = true // set IsValid once we choose reading 98 | 99 | err = p.sd.SetSettings(event.Context, &settings) 100 | if err != nil { 101 | return fmt.Errorf("handleReadingSelect SetSettings: %v", err) 102 | } 103 | p.am.SetAction(event.Action, event.Context, &settings) 104 | return nil 105 | } 106 | 107 | func (p *Plugin) handleSetMin(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { 108 | min, err := strconv.Atoi(sdpi.Value) 109 | if err != nil { 110 | return fmt.Errorf("handleSetMin strconv: %v", err) 111 | } 112 | g, ok := p.graphs[event.Context] 113 | if !ok { 114 | return fmt.Errorf("handleSetMax no graph for context: %s", event.Context) 115 | } 116 | g.SetMin(min) 117 | settings, err := p.am.getSettings(event.Context) 118 | if err != nil { 119 | return fmt.Errorf("handleSetMin getSettings: %v", err) 120 | } 121 | settings.Min = min 122 | err = p.sd.SetSettings(event.Context, &settings) 123 | if err != nil { 124 | return fmt.Errorf("handleSetMin SetSettings: %v", err) 125 | } 126 | p.am.SetAction(event.Action, event.Context, &settings) 127 | return nil 128 | } 129 | 130 | func (p *Plugin) handleSetMax(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { 131 | max, err := strconv.Atoi(sdpi.Value) 132 | if err != nil { 133 | return fmt.Errorf("handleSetMax strconv: %v", err) 134 | } 135 | g, ok := p.graphs[event.Context] 136 | if !ok { 137 | return fmt.Errorf("handleSetMax no graph for context: %s", event.Context) 138 | } 139 | g.SetMax(max) 140 | settings, err := p.am.getSettings(event.Context) 141 | if err != nil { 142 | return fmt.Errorf("handleSetMax getSettings: %v", err) 143 | } 144 | settings.Max = max 145 | err = p.sd.SetSettings(event.Context, &settings) 146 | if err != nil { 147 | return fmt.Errorf("handleSetMax SetSettings: %v", err) 148 | } 149 | p.am.SetAction(event.Action, event.Context, &settings) 150 | return nil 151 | } 152 | 153 | func (p *Plugin) handleSetFormat(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { 154 | format := sdpi.Value 155 | settings, err := p.am.getSettings(event.Context) 156 | if err != nil { 157 | return fmt.Errorf("handleSetFormat getSettings: %v", err) 158 | } 159 | settings.Format = format 160 | err = p.sd.SetSettings(event.Context, &settings) 161 | if err != nil { 162 | return fmt.Errorf("handleSetFormat SetSettings: %v", err) 163 | } 164 | p.am.SetAction(event.Action, event.Context, &settings) 165 | return nil 166 | } 167 | 168 | func (p *Plugin) handleDivisor(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { 169 | divisor := sdpi.Value 170 | settings, err := p.am.getSettings(event.Context) 171 | if err != nil { 172 | return fmt.Errorf("handleDivisor getSettings: %v", err) 173 | } 174 | settings.Divisor = divisor 175 | err = p.sd.SetSettings(event.Context, &settings) 176 | if err != nil { 177 | return fmt.Errorf("handleDivisor SetSettings: %v", err) 178 | } 179 | p.am.SetAction(event.Action, event.Context, &settings) 180 | return nil 181 | } 182 | 183 | const ( 184 | hexFormat = "#%02x%02x%02x" 185 | hexShortFormat = "#%1x%1x%1x" 186 | hexToRGBFactor = 17 187 | ) 188 | 189 | func hexToRGBA(hex string) *color.RGBA { 190 | var r, g, b uint8 191 | 192 | if len(hex) == 4 { 193 | fmt.Sscanf(hex, hexShortFormat, &r, &g, &b) 194 | r *= hexToRGBFactor 195 | g *= hexToRGBFactor 196 | b *= hexToRGBFactor 197 | } else { 198 | fmt.Sscanf(hex, hexFormat, &r, &g, &b) 199 | } 200 | 201 | return &color.RGBA{R: r, G: g, B: b, A: 255} 202 | } 203 | 204 | func (p *Plugin) handleColorChange(event *streamdeck.EvSendToPlugin, key string, sdpi *evSdpiCollection) error { 205 | hex := sdpi.Value 206 | settings, err := p.am.getSettings(event.Context) 207 | if err != nil { 208 | return fmt.Errorf("handleDivisor getSettings: %v", err) 209 | } 210 | g, ok := p.graphs[event.Context] 211 | if !ok { 212 | return fmt.Errorf("handleSetMax no graph for context: %s", event.Context) 213 | } 214 | clr := hexToRGBA(hex) 215 | switch key { 216 | case "foreground": 217 | settings.ForegroundColor = hex 218 | g.SetForegroundColor(clr) 219 | case "background": 220 | settings.BackgroundColor = hex 221 | g.SetBackgroundColor(clr) 222 | case "highlight": 223 | settings.HighlightColor = hex 224 | g.SetHighlightColor(clr) 225 | case "valuetext": 226 | settings.ValueTextColor = hex 227 | g.SetLabelColor(1, clr) 228 | } 229 | err = p.sd.SetSettings(event.Context, &settings) 230 | if err != nil { 231 | return fmt.Errorf("handleColorChange SetSettings: %v", err) 232 | } 233 | p.am.SetAction(event.Action, event.Context, &settings) 234 | return nil 235 | } 236 | 237 | func (p *Plugin) handleSetFontSize(event *streamdeck.EvSendToPlugin, key string, sdpi *evSdpiCollection) error { 238 | sv := sdpi.Value 239 | size, err := strconv.ParseFloat(sv, 64) 240 | if err != nil { 241 | return fmt.Errorf("failed to convert value to float: %w", err) 242 | } 243 | 244 | settings, err := p.am.getSettings(event.Context) 245 | if err != nil { 246 | return fmt.Errorf("getSettings failed: %w", err) 247 | } 248 | 249 | g, ok := p.graphs[event.Context] 250 | if !ok { 251 | return fmt.Errorf("no graph for context: %s", event.Context) 252 | } 253 | 254 | switch key { 255 | case "titleFontSize": 256 | settings.TitleFontSize = size 257 | g.SetLabelFontSize(0, size) 258 | case "valueFontSize": 259 | settings.ValueFontSize = size 260 | g.SetLabelFontSize(1, size) 261 | default: 262 | return fmt.Errorf("invalid key: %s", sdpi.Key) 263 | } 264 | 265 | err = p.sd.SetSettings(event.Context, &settings) 266 | if err != nil { 267 | return fmt.Errorf("SetSettings failed: %w", err) 268 | } 269 | 270 | p.am.SetAction(event.Action, event.Context, &settings) 271 | return nil 272 | } 273 | -------------------------------------------------------------------------------- /internal/app/hwinfostreamdeckplugin/plugin.go: -------------------------------------------------------------------------------- 1 | package hwinfostreamdeckplugin 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os/exec" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/hashicorp/go-plugin" 12 | "github.com/shayne/go-winpeg" 13 | "github.com/shayne/hwinfo-streamdeck/pkg/graph" 14 | hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" 15 | "github.com/shayne/hwinfo-streamdeck/pkg/streamdeck" 16 | ) 17 | 18 | // Plugin handles information between HWiNFO and Stream Deck 19 | type Plugin struct { 20 | c *plugin.Client 21 | peg winpeg.ProcessExitGroup 22 | hw hwsensorsservice.HardwareService 23 | sd *streamdeck.StreamDeck 24 | am *actionManager 25 | graphs map[string]*graph.Graph 26 | 27 | appLaunched bool 28 | } 29 | 30 | func (p *Plugin) startClient() error { 31 | cmd := exec.Command("./hwinfo-plugin.exe") 32 | 33 | // We're a host. Start by launching the plugin process. 34 | client := plugin.NewClient(&plugin.ClientConfig{ 35 | HandshakeConfig: hwsensorsservice.Handshake, 36 | Plugins: hwsensorsservice.PluginMap, 37 | Cmd: cmd, 38 | AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 39 | AutoMTLS: true, 40 | }) 41 | 42 | // Connect via RPC 43 | rpcClient, err := client.Client() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | g, err := winpeg.NewProcessExitGroup() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if err := g.AddProcess(cmd.Process); err != nil { 54 | return err 55 | } 56 | 57 | // Request the plugin 58 | raw, err := rpcClient.Dispense("hwinfoplugin") 59 | if err != nil { 60 | return err 61 | } 62 | 63 | p.c = client 64 | p.peg = g 65 | p.hw = raw.(hwsensorsservice.HardwareService) 66 | 67 | return nil 68 | } 69 | 70 | // NewPlugin creates an instance and initializes the plugin 71 | func NewPlugin(port, uuid, event, info string) (*Plugin, error) { 72 | // We don't want to see the plugin logs. 73 | // log.SetOutput(ioutil.Discard) 74 | p := &Plugin{ 75 | am: newActionManager(), 76 | graphs: make(map[string]*graph.Graph), 77 | } 78 | p.startClient() 79 | p.sd = streamdeck.NewStreamDeck(port, uuid, event, info) 80 | return p, nil 81 | } 82 | 83 | // RunForever starts the plugin and waits for events, indefinitely 84 | func (p *Plugin) RunForever() error { 85 | defer func() { 86 | p.c.Kill() 87 | p.peg.Dispose() 88 | }() 89 | 90 | p.sd.SetDelegate(p) 91 | p.am.Run(p.updateTiles) 92 | 93 | go func() { 94 | for { 95 | if p.c.Exited() { 96 | p.startClient() 97 | } 98 | time.Sleep(1 * time.Second) 99 | } 100 | }() 101 | 102 | err := p.sd.Connect() 103 | if err != nil { 104 | return fmt.Errorf("StreamDeck Connect: %v", err) 105 | } 106 | defer p.sd.Close() 107 | p.sd.ListenAndWait() 108 | return nil 109 | } 110 | 111 | func (p *Plugin) getReading(suid string, rid int32) (hwsensorsservice.Reading, error) { 112 | rbs, err := p.hw.ReadingsForSensorID(suid) 113 | if err != nil { 114 | return nil, fmt.Errorf("getReading ReadingsBySensor failed: %v", err) 115 | } 116 | for _, r := range rbs { 117 | if r.ID() == rid { 118 | return r, nil 119 | } 120 | } 121 | return nil, fmt.Errorf("ReadingID does not exist: %s", suid) 122 | } 123 | 124 | func (p *Plugin) applyDefaultFormat(v float64, t hwsensorsservice.ReadingType, u string) string { 125 | switch t { 126 | case hwsensorsservice.ReadingTypeNone: 127 | return fmt.Sprintf("%0.f %s", v, u) 128 | case hwsensorsservice.ReadingTypeTemp: 129 | return fmt.Sprintf("%.0f %s", v, u) 130 | case hwsensorsservice.ReadingTypeVolt: 131 | return fmt.Sprintf("%.0f %s", v, u) 132 | case hwsensorsservice.ReadingTypeFan: 133 | return fmt.Sprintf("%.0f %s", v, u) 134 | case hwsensorsservice.ReadingTypeCurrent: 135 | return fmt.Sprintf("%.0f %s", v, u) 136 | case hwsensorsservice.ReadingTypePower: 137 | return fmt.Sprintf("%0.f %s", v, u) 138 | case hwsensorsservice.ReadingTypeClock: 139 | return fmt.Sprintf("%.0f %s", v, u) 140 | case hwsensorsservice.ReadingTypeUsage: 141 | return fmt.Sprintf("%.0f%s", v, u) 142 | case hwsensorsservice.ReadingTypeOther: 143 | return fmt.Sprintf("%.0f %s", v, u) 144 | } 145 | return "Bad Format" 146 | } 147 | 148 | func (p *Plugin) updateTiles(data *actionData) { 149 | if data.action != "com.exension.hwinfo.reading" { 150 | log.Printf("Unknown action updateTiles: %s\n", data.action) 151 | return 152 | } 153 | 154 | g, ok := p.graphs[data.context] 155 | if !ok { 156 | log.Printf("Graph not found for context: %s\n", data.context) 157 | return 158 | } 159 | 160 | if !p.appLaunched { 161 | if !data.settings.InErrorState { 162 | payload := evStatus{Error: true, Message: "HWiNFO Unavailable"} 163 | err := p.sd.SendToPropertyInspector("com.exension.hwinfo.reading", data.context, payload) 164 | if err != nil { 165 | log.Println("updateTiles SendToPropertyInspector", err) 166 | } 167 | data.settings.InErrorState = true 168 | p.sd.SetSettings(data.context, &data.settings) 169 | } 170 | bts, err := ioutil.ReadFile("./launch-hwinfo.png") 171 | if err != nil { 172 | log.Printf("Failed to read launch-hwinfo.png: %v\n", err) 173 | return 174 | } 175 | err = p.sd.SetImage(data.context, bts) 176 | if err != nil { 177 | log.Printf("Failed to setImage: %v\n", err) 178 | return 179 | } 180 | return 181 | } 182 | 183 | // show ui on property inspector if in error state 184 | if data.settings.InErrorState { 185 | payload := evStatus{Error: false, Message: "show_ui"} 186 | err := p.sd.SendToPropertyInspector("com.exension.hwinfo.reading", data.context, payload) 187 | if err != nil { 188 | log.Println("updateTiles SendToPropertyInspector", err) 189 | } 190 | data.settings.InErrorState = false 191 | p.sd.SetSettings(data.context, &data.settings) 192 | } 193 | 194 | s := data.settings 195 | r, err := p.getReading(s.SensorUID, s.ReadingID) 196 | if err != nil { 197 | log.Printf("getReading failed: %v\n", err) 198 | return 199 | } 200 | 201 | v := r.Value() 202 | if s.Divisor != "" { 203 | fdiv := 1. 204 | fdiv, err := strconv.ParseFloat(s.Divisor, 64) 205 | if err != nil { 206 | log.Printf("Failed to parse float: %s\n", s.Divisor) 207 | return 208 | } 209 | v = r.Value() / fdiv 210 | } 211 | g.Update(v) 212 | var text string 213 | if f := s.Format; f != "" { 214 | text = fmt.Sprintf(f, v) 215 | } else { 216 | text = p.applyDefaultFormat(v, hwsensorsservice.ReadingType(r.TypeI()), r.Unit()) 217 | } 218 | g.SetLabelText(1, text) 219 | 220 | b, err := g.EncodePNG() 221 | if err != nil { 222 | log.Printf("Failed to encode graph: %v\n", err) 223 | return 224 | } 225 | 226 | err = p.sd.SetImage(data.context, b) 227 | if err != nil { 228 | log.Printf("Failed to setImage: %v\n", err) 229 | return 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /internal/app/hwinfostreamdeckplugin/types.go: -------------------------------------------------------------------------------- 1 | package hwinfostreamdeckplugin 2 | 3 | type actionSettings struct { 4 | SensorUID string `json:"sensorUid"` 5 | ReadingID int32 `json:"readingId,string"` 6 | Title string `json:"title"` 7 | TitleFontSize float64 `json:"titleFontSize"` 8 | ValueFontSize float64 `json:"valueFontSize"` 9 | Min int `json:"min"` 10 | Max int `json:"max"` 11 | Format string `json:"format"` 12 | Divisor string `json:"divisor"` 13 | IsValid bool `json:"isValid"` 14 | TitleColor string `json:"titleColor"` 15 | ForegroundColor string `json:"foregroundColor"` 16 | BackgroundColor string `json:"backgroundColor"` 17 | HighlightColor string `json:"highlightColor"` 18 | ValueTextColor string `json:"valueTextColor"` 19 | InErrorState bool `json:"inErrorState"` 20 | } 21 | 22 | type actionData struct { 23 | action string 24 | context string 25 | settings *actionSettings 26 | } 27 | 28 | type evStatus struct { 29 | Error bool `json:"error"` 30 | Message string `json:"message"` 31 | } 32 | 33 | type evSendSensorsPayloadSensor struct { 34 | UID string `json:"uid"` 35 | Name string `json:"name"` 36 | } 37 | 38 | type evSendSensorsPayload struct { 39 | Sensors []*evSendSensorsPayloadSensor `json:"sensors"` 40 | Settings *actionSettings `json:"settings"` 41 | } 42 | 43 | type evSendReadingsPayloadReading struct { 44 | ID int32 `json:"id,string"` 45 | Label string `json:"label"` 46 | Prefix string `json:"prefix"` 47 | } 48 | 49 | type evSendReadingsPayload struct { 50 | Readings []*evSendReadingsPayloadReading `json:"readings"` 51 | Settings *actionSettings `json:"settings"` 52 | } 53 | 54 | type evSdpiCollection struct { 55 | Group bool `json:"group"` 56 | Index int `json:"index"` 57 | Key string `json:"key"` 58 | Selection []string `json:"selection"` 59 | Value string `json:"value"` 60 | } 61 | -------------------------------------------------------------------------------- /internal/hwinfo/hwinfo.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #include 5 | #include "hwisenssm2.h" 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "time" 13 | "unsafe" 14 | 15 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/shmem" 16 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" 17 | ) 18 | 19 | // SharedMemory provides access to the HWiNFO shared memory 20 | type SharedMemory struct { 21 | data []byte 22 | shmem C.PHWiNFO_SENSORS_SHARED_MEM2 23 | } 24 | 25 | // ReadSharedMem reads data from HWiNFO shared memory 26 | // creating a copy of the data 27 | func ReadSharedMem() (*SharedMemory, error) { 28 | data, err := shmem.ReadBytes() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &SharedMemory{ 34 | data: append([]byte(nil), data...), 35 | shmem: C.PHWiNFO_SENSORS_SHARED_MEM2(unsafe.Pointer(&data[0])), 36 | }, nil 37 | } 38 | 39 | // Result for streamed shared memory updates 40 | type Result struct { 41 | Shmem *SharedMemory 42 | Err error 43 | } 44 | 45 | func readAndSend(ch chan<- Result) { 46 | shmem, err := ReadSharedMem() 47 | ch <- Result{Shmem: shmem, Err: err} 48 | } 49 | 50 | // StreamSharedMem delivers shared memory hardware sensors updates 51 | // over a channel 52 | func StreamSharedMem() <-chan Result { 53 | ch := make(chan Result) 54 | go func() { 55 | readAndSend(ch) 56 | // TODO: don't use time.Tick, cancellable? 57 | for range time.Tick(1 * time.Second) { 58 | readAndSend(ch) 59 | } 60 | }() 61 | return ch 62 | } 63 | 64 | // Signature "HWiS" if active, 'DEAD' when inactive 65 | func (s *SharedMemory) Signature() string { 66 | return util.DecodeCharPtr(unsafe.Pointer(&s.shmem.dwSignature), C.sizeof_DWORD) 67 | } 68 | 69 | // Version v1 is latest 70 | func (s *SharedMemory) Version() int { 71 | return int(s.shmem.dwVersion) 72 | } 73 | 74 | // Revision revision of version 75 | func (s *SharedMemory) Revision() int { 76 | return int(s.shmem.dwRevision) 77 | } 78 | 79 | // PollTime last polling time 80 | func (s *SharedMemory) PollTime() uint64 { 81 | addr := unsafe.Pointer(uintptr(unsafe.Pointer(&s.shmem.dwRevision)) + C.sizeof_DWORD) 82 | return uint64(*(*C.__time64_t)(addr)) 83 | } 84 | 85 | // OffsetOfSensorSection offset of the Sensor section from beginning of HWiNFO_SENSORS_SHARED_MEM2 86 | func (s *SharedMemory) OffsetOfSensorSection() int { 87 | return int(s.shmem.dwOffsetOfSensorSection) 88 | } 89 | 90 | // SizeOfSensorElement size of each sensor element = sizeof( HWiNFO_SENSORS_SENSOR_ELEMENT ) 91 | func (s *SharedMemory) SizeOfSensorElement() int { 92 | return int(s.shmem.dwSizeOfSensorElement) 93 | } 94 | 95 | // NumSensorElements number of sensor elements 96 | func (s *SharedMemory) NumSensorElements() int { 97 | return int(s.shmem.dwNumSensorElements) 98 | } 99 | 100 | // OffsetOfReadingSection offset of the Reading section from beginning of HWiNFO_SENSORS_SHARED_MEM2 101 | func (s *SharedMemory) OffsetOfReadingSection() int { 102 | return int(s.shmem.dwOffsetOfReadingSection) 103 | } 104 | 105 | // SizeOfReadingElement size of each Reading element = sizeof( HWiNFO_SENSORS_READING_ELEMENT ) 106 | func (s *SharedMemory) SizeOfReadingElement() int { 107 | return int(s.shmem.dwSizeOfReadingElement) 108 | } 109 | 110 | // NumReadingElements number of Reading elements 111 | func (s *SharedMemory) NumReadingElements() int { 112 | return int(s.shmem.dwNumReadingElements) 113 | } 114 | 115 | func (s *SharedMemory) dataForSensor(pos int) ([]byte, error) { 116 | if pos >= s.NumSensorElements() { 117 | return nil, fmt.Errorf("dataForSensor pos out of range, %d for size %d", pos, s.NumSensorElements()) 118 | } 119 | start := s.OffsetOfSensorSection() + (pos * s.SizeOfSensorElement()) 120 | end := start + s.SizeOfSensorElement() 121 | return s.data[start:end], nil 122 | } 123 | 124 | // IterSensors iterate over each sensor 125 | func (s *SharedMemory) IterSensors() <-chan Sensor { 126 | ch := make(chan Sensor) 127 | go func() { 128 | for i := 0; i < s.NumSensorElements(); i++ { 129 | data, err := s.dataForSensor(i) 130 | if err != nil { 131 | log.Fatalf("TODO: failed to read dataForSensor: %v", err) 132 | } 133 | ch <- NewSensor(data) 134 | } 135 | close(ch) 136 | }() 137 | return ch 138 | } 139 | 140 | func (s *SharedMemory) dataForReading(pos int) ([]byte, error) { 141 | if pos >= s.NumReadingElements() { 142 | return nil, fmt.Errorf("dataForReading pos out of range, %d for size %d", pos, s.NumSensorElements()) 143 | } 144 | start := s.OffsetOfReadingSection() + (pos * s.SizeOfReadingElement()) 145 | end := start + s.SizeOfReadingElement() 146 | return s.data[start:end], nil 147 | } 148 | 149 | // IterReadings iterate over each sensor 150 | func (s *SharedMemory) IterReadings() <-chan Reading { 151 | ch := make(chan Reading) 152 | go func() { 153 | for i := 0; i < s.NumReadingElements(); i++ { 154 | data, err := s.dataForReading(i) 155 | if err != nil { 156 | log.Fatalf("TODO: failed to read dataForReading: %v", err) 157 | } 158 | ch <- NewReading(data) 159 | } 160 | close(ch) 161 | }() 162 | return ch 163 | } 164 | -------------------------------------------------------------------------------- /internal/hwinfo/hwisenssm2.h: -------------------------------------------------------------------------------- 1 | #ifndef _HWISENSSM2_H_INCLUDED_ 2 | #define _HWISENSSM2_H_INCLUDED_ 3 | 4 | // Name of the file mapping object that needs to be opened using OpenFileMapping Function: 5 | #define HWiNFO_SENSORS_MAP_FILE_NAME2 "Global\\HWiNFO_SENS_SM2" 6 | 7 | // Name of the global mutex which is acquired when accessing the Shared Memory space. Release as quick as possible ! 8 | #define HWiNFO_SENSORS_SM2_MUTEX "Global\\HWiNFO_SM2_MUTEX" 9 | 10 | #define HWiNFO_SENSORS_STRING_LEN2 128 11 | #define HWiNFO_UNIT_STRING_LEN 16 12 | 13 | enum SENSOR_READING_TYPE 14 | { 15 | SENSOR_TYPE_NONE = 0, 16 | SENSOR_TYPE_TEMP, 17 | SENSOR_TYPE_VOLT, 18 | SENSOR_TYPE_FAN, 19 | SENSOR_TYPE_CURRENT, 20 | SENSOR_TYPE_POWER, 21 | SENSOR_TYPE_CLOCK, 22 | SENSOR_TYPE_USAGE, 23 | SENSOR_TYPE_OTHER 24 | }; 25 | typedef enum SENSOR_READING_TYPE SENSOR_READING_TYPE; 26 | 27 | // No alignment of structure members 28 | #pragma pack(1) 29 | 30 | typedef struct _HWiNFO_SENSORS_READING_ELEMENT 31 | { 32 | 33 | SENSOR_READING_TYPE tReading; // Type of sensor reading 34 | DWORD dwSensorIndex; // This is the index of sensor in the Sensors[] array to which this reading belongs to 35 | DWORD dwReadingID; // A unique ID of the reading within a particular sensor 36 | char szLabelOrig[HWiNFO_SENSORS_STRING_LEN2]; // Original label (e.g. "Chassis2 Fan") 37 | char szLabelUser[HWiNFO_SENSORS_STRING_LEN2]; // Label displayed, which might have been renamed by user 38 | char szUnit[HWiNFO_UNIT_STRING_LEN]; // e.g. "RPM" 39 | double Value; 40 | double ValueMin; 41 | double ValueMax; 42 | double ValueAvg; 43 | 44 | } HWiNFO_SENSORS_READING_ELEMENT, *PHWiNFO_SENSORS_READING_ELEMENT; 45 | 46 | typedef struct _HWiNFO_SENSORS_SENSOR_ELEMENT 47 | { 48 | 49 | DWORD dwSensorID; // A unique Sensor ID 50 | DWORD dwSensorInst; // The instance of the sensor (together with dwSensorID forms a unique ID) 51 | char szSensorNameOrig[HWiNFO_SENSORS_STRING_LEN2]; // Original sensor name 52 | char szSensorNameUser[HWiNFO_SENSORS_STRING_LEN2]; // Sensor name displayed, which might have been renamed by user 53 | 54 | } HWiNFO_SENSORS_SENSOR_ELEMENT, *PHWiNFO_SENSORS_SENSOR_ELEMENT; 55 | 56 | typedef struct _HWiNFO_SENSORS_SHARED_MEM2 57 | { 58 | 59 | DWORD dwSignature; // "HWiS" if active, 'DEAD' when inactive 60 | DWORD dwVersion; // v1 is latest 61 | DWORD dwRevision; // 62 | __time64_t poll_time; // last polling time 63 | 64 | // descriptors for the Sensors section 65 | DWORD dwOffsetOfSensorSection; // Offset of the Sensor section from beginning of HWiNFO_SENSORS_SHARED_MEM2 66 | DWORD dwSizeOfSensorElement; // Size of each sensor element = sizeof( HWiNFO_SENSORS_SENSOR_ELEMENT ) 67 | DWORD dwNumSensorElements; // Number of sensor elements 68 | 69 | // descriptors for the Readings section 70 | DWORD dwOffsetOfReadingSection; // Offset of the Reading section from beginning of HWiNFO_SENSORS_SHARED_MEM2 71 | DWORD dwSizeOfReadingElement; // Size of each Reading element = sizeof( HWiNFO_SENSORS_READING_ELEMENT ) 72 | DWORD dwNumReadingElements; // Number of Reading elements 73 | 74 | } HWiNFO_SENSORS_SHARED_MEM2, *PHWiNFO_SENSORS_SHARED_MEM2; 75 | 76 | #pragma pack() 77 | 78 | #endif 79 | 80 | // *************************************************************************************************************** 81 | // HWiNFO Shared Memory Footprint 82 | // *************************************************************************************************************** 83 | // 84 | // |-----------------------------|-----------------------------------|-----------------------------------| 85 | // Content | HWiNFO_SENSORS_SHARED_MEM2 | HWiNFO_SENSORS_SENSOR_ELEMENT[] | HWiNFO_SENSORS_READING_ELEMENT[] | 86 | // |-----------------------------|-----------------------------------|-----------------------------------| 87 | // Pointer |<--0 |<--dwOffsetOfSensorSection |<--dwOffsetOfReadingSection | 88 | // |-----------------------------|-----------------------------------|-----------------------------------| 89 | // Size | dwOffsetOfSensorSection | dwSizeOfSensorElement | dwSizeOfReadingElement | 90 | // | | * dwNumSensorElement | * dwNumReadingElement | 91 | // |-----------------------------|-----------------------------------|-----------------------------------| 92 | // 93 | // *************************************************************************************************************** 94 | // Code Example 95 | // *************************************************************************************************************** 96 | /* 97 | 98 | HANDLE hHWiNFOMemory = OpenFileMapping( FILE_MAP_READ, FALSE, HWiNFO_SENSORS_MAP_FILE_NAME2 ); 99 | if (hHWiNFOMemory) 100 | PHWiNFO_SENSORS_SHARED_MEM2 pHWiNFOMemory = 101 | (PHWiNFO_SENSORS_SHARED_MEM2) MapViewOfFile( hHWiNFOMemory, FILE_MAP_READ, 0, 0, 0 ); 102 | 103 | // TODO: process signature, version, revision and poll time 104 | 105 | // loop through all available sensors 106 | for (DWORD dwSensor = 0; dwSensor < pHWiNFOMemory->dwNumSensorElements; dwSensor++) 107 | { 108 | PHWiNFO_SENSORS_SENSOR_ELEMENT sensor = (PHWiNFO_SENSORS_SENSOR_ELEMENT) ((BYTE*)pHWiNFOMemory + 109 | pHWiNFOMemory->dwOffsetOfSensorSection + 110 | (pHWiNFOMemory->dwSizeOfSensorElement * dwSensor)); 111 | 112 | // TODO: process sensor 113 | } 114 | 115 | // loop through all available readings 116 | for (DWORD dwReading = 0; dwReading < pHWiNFOMemory->dwNumReadingElements; dwReading++) 117 | { 118 | PHWiNFO_SENSORS_READING_ELEMENT reading = (PHWiNFO_SENSORS_READING_ELEMENT) ((BYTE*)pHWiNFOMemory + 119 | pHWiNFOMemory->dwOffsetOfReadingSection + 120 | (pHWiNFOMemory->dwSizeOfReadingElement * dwReading)); 121 | 122 | // TODO: process reading 123 | } 124 | } 125 | 126 | */ -------------------------------------------------------------------------------- /internal/hwinfo/mutex/mutex.go: -------------------------------------------------------------------------------- 1 | package mutex 2 | 3 | /* 4 | #include 5 | #include "../hwisenssm2.h" 6 | */ 7 | import "C" 8 | import ( 9 | "fmt" 10 | "sync" 11 | "unsafe" 12 | 13 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" 14 | ) 15 | 16 | var ghnd C.HANDLE 17 | var imut = sync.Mutex{} 18 | 19 | // Lock the global mutex 20 | func Lock() error { 21 | imut.Lock() 22 | lpName := C.CString(C.HWiNFO_SENSORS_SM2_MUTEX) 23 | defer C.free(unsafe.Pointer(lpName)) 24 | 25 | ghnd = C.OpenMutex(C.READ_CONTROL, C.FALSE, lpName) 26 | if ghnd == C.HANDLE(C.NULL) { 27 | errstr := util.HandleLastError(uint64(C.GetLastError())) 28 | return fmt.Errorf("failed to lock global mutex: %w", errstr) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // Unlock the global mutex 35 | func Unlock() { 36 | defer imut.Unlock() 37 | C.CloseHandle(ghnd) 38 | } 39 | -------------------------------------------------------------------------------- /internal/hwinfo/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo" 5 | hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" 6 | ) 7 | 8 | // Plugin implementation 9 | type Plugin struct { 10 | Service *Service 11 | } 12 | 13 | // PollTime implementation for plugin 14 | func (p *Plugin) PollTime() (uint64, error) { 15 | shmem, err := p.Service.Shmem() 16 | if err != nil { 17 | return 0, err 18 | } 19 | return shmem.PollTime(), nil 20 | } 21 | 22 | // Sensors implementation for plugin 23 | func (p *Plugin) Sensors() ([]hwsensorsservice.Sensor, error) { 24 | shmem, err := p.Service.Shmem() 25 | if err != nil { 26 | return nil, err 27 | } 28 | var sensors []hwsensorsservice.Sensor 29 | for s := range shmem.IterSensors() { 30 | sensors = append(sensors, &sensor{s}) 31 | } 32 | return sensors, nil 33 | } 34 | 35 | // ReadingsForSensorID implementation for plugin 36 | func (p *Plugin) ReadingsForSensorID(id string) ([]hwsensorsservice.Reading, error) { 37 | res, err := p.Service.ReadingsBySensorID(id) 38 | if err != nil { 39 | return nil, err 40 | } 41 | var readings []hwsensorsservice.Reading 42 | for _, r := range res { 43 | readings = append(readings, &reading{r}) 44 | } 45 | return readings, nil 46 | } 47 | 48 | type sensor struct { 49 | hwinfo.Sensor 50 | } 51 | 52 | func (s sensor) Name() string { 53 | return s.NameOrig() 54 | } 55 | 56 | type reading struct { 57 | hwinfo.Reading 58 | } 59 | 60 | func (r reading) Label() string { 61 | return r.LabelOrig() 62 | } 63 | 64 | func (r reading) Type() string { 65 | return r.Reading.Type().String() 66 | } 67 | 68 | func (r reading) TypeI() int32 { 69 | return int32(r.Reading.Type()) 70 | } 71 | -------------------------------------------------------------------------------- /internal/hwinfo/plugin/service.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo" 8 | ) 9 | 10 | // Service wraps hwinfo shared mem streaming 11 | // and provides convenient methods for data access 12 | type Service struct { 13 | streamch <-chan hwinfo.Result 14 | mu sync.RWMutex 15 | sensorIDByIdx []string 16 | readingsBySensorID map[string][]hwinfo.Reading 17 | shmem *hwinfo.SharedMemory 18 | readingsBuilt bool 19 | } 20 | 21 | // Start starts the service providing updating hardware info 22 | func StartService() *Service { 23 | return &Service{ 24 | streamch: hwinfo.StreamSharedMem(), 25 | } 26 | } 27 | 28 | func (s *Service) recvShmem(shmem *hwinfo.SharedMemory) error { 29 | if shmem == nil { 30 | return fmt.Errorf("shmem nil") 31 | } 32 | s.mu.Lock() 33 | defer s.mu.Unlock() 34 | 35 | s.shmem = shmem 36 | 37 | s.sensorIDByIdx = s.sensorIDByIdx[:0] 38 | for k, v := range s.readingsBySensorID { 39 | s.readingsBySensorID[k] = v[:0] 40 | } 41 | s.readingsBuilt = false 42 | 43 | return nil 44 | } 45 | 46 | // Recv receives new hardware sensor updates 47 | func (s *Service) Recv() error { 48 | select { 49 | case r := <-s.streamch: 50 | if r.Err != nil { 51 | return r.Err 52 | } 53 | return s.recvShmem(r.Shmem) 54 | } 55 | } 56 | 57 | // Shmem provides access to underlying hwinfo shared memory 58 | func (s *Service) Shmem() (*hwinfo.SharedMemory, error) { 59 | s.mu.RLock() 60 | defer s.mu.RUnlock() 61 | 62 | if s.shmem != nil { 63 | return s.shmem, nil 64 | } 65 | return nil, fmt.Errorf("shmem nil") 66 | } 67 | 68 | // SensorIDByIdx returns ordered slice of sensor IDs 69 | func (s *Service) SensorIDByIdx() ([]string, error) { 70 | s.mu.RLock() 71 | if len(s.sensorIDByIdx) > 0 { 72 | defer s.mu.RUnlock() 73 | return s.sensorIDByIdx, nil 74 | } 75 | s.mu.RUnlock() 76 | 77 | s.mu.Lock() 78 | defer s.mu.Unlock() 79 | 80 | for sens := range s.shmem.IterSensors() { 81 | s.sensorIDByIdx = append(s.sensorIDByIdx, sens.ID()) 82 | } 83 | 84 | return s.sensorIDByIdx, nil 85 | } 86 | 87 | // ReadingsBySensorID returns slice of hwinfoReading for a given sensor ID 88 | func (s *Service) ReadingsBySensorID(id string) ([]hwinfo.Reading, error) { 89 | s.mu.RLock() 90 | if s.readingsBySensorID != nil && s.readingsBuilt { 91 | defer s.mu.RUnlock() 92 | readings, ok := s.readingsBySensorID[id] 93 | if !ok { 94 | return nil, fmt.Errorf("readings for sensor id %s do not exist", id) 95 | } 96 | return readings, nil 97 | } 98 | s.mu.RUnlock() 99 | 100 | sids, err := s.SensorIDByIdx() 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | s.mu.Lock() 106 | defer s.mu.Unlock() 107 | 108 | if s.readingsBySensorID == nil { 109 | s.readingsBySensorID = make(map[string][]hwinfo.Reading) 110 | } 111 | 112 | for r := range s.shmem.IterReadings() { 113 | sidx := int(r.SensorIndex()) 114 | if sidx < len(sids) { 115 | sid := sids[sidx] 116 | s.readingsBySensorID[sid] = append(s.readingsBySensorID[sid], r) 117 | } else { 118 | return nil, fmt.Errorf("sensor at index %d out of range ", sidx) 119 | } 120 | } 121 | s.readingsBuilt = true 122 | 123 | readings, ok := s.readingsBySensorID[id] 124 | if !ok { 125 | return nil, fmt.Errorf("readings for sensor id %s do not exist", id) 126 | } 127 | return readings, nil 128 | } 129 | -------------------------------------------------------------------------------- /internal/hwinfo/reading.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #include 5 | #include "hwisenssm2.h" 6 | */ 7 | import "C" 8 | import ( 9 | "unsafe" 10 | 11 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" 12 | ) 13 | 14 | // ReadingType enum of value/unit type for reading 15 | type ReadingType int 16 | 17 | const ( 18 | // ReadingTypeNone no type 19 | ReadingTypeNone ReadingType = iota 20 | // ReadingTypeTemp temperature in celsius 21 | ReadingTypeTemp 22 | // ReadingTypeVolt voltage 23 | ReadingTypeVolt 24 | // ReadingTypeFan RPM 25 | ReadingTypeFan 26 | // ReadingTypeCurrent amps 27 | ReadingTypeCurrent 28 | // ReadingTypePower watts 29 | ReadingTypePower 30 | // ReadingTypeClock Mhz 31 | ReadingTypeClock 32 | // ReadingTypeUsage e.g. MBs 33 | ReadingTypeUsage 34 | // ReadingTypeOther other 35 | ReadingTypeOther 36 | ) 37 | 38 | func (t ReadingType) String() string { 39 | return [...]string{"None", "Temp", "Volt", "Fan", "Current", "Power", "Clock", "Usage", "Other"}[t] 40 | } 41 | 42 | // Reading element (e.g. usage, power, mhz...) 43 | type Reading struct { 44 | cr C.PHWiNFO_SENSORS_READING_ELEMENT 45 | } 46 | 47 | // NewReading contructs a Reading 48 | func NewReading(data []byte) Reading { 49 | return Reading{ 50 | cr: C.PHWiNFO_SENSORS_READING_ELEMENT(unsafe.Pointer(&data[0])), 51 | } 52 | } 53 | 54 | // ID unique ID of the reading within a particular sensor 55 | func (r *Reading) ID() int32 { 56 | return int32(r.cr.dwReadingID) 57 | } 58 | 59 | // Type of sensor reading 60 | func (r *Reading) Type() ReadingType { 61 | return ReadingType(r.cr.tReading) 62 | } 63 | 64 | // SensorIndex this is the index of sensor in the Sensors[] array to 65 | // which this reading belongs to 66 | func (r *Reading) SensorIndex() uint64 { 67 | return uint64(r.cr.dwSensorIndex) 68 | } 69 | 70 | // ReadingID a unique ID of the reading within a particular sensor 71 | func (r *Reading) ReadingID() uint64 { 72 | return uint64(r.cr.dwReadingID) 73 | } 74 | 75 | // LabelOrig original label (e.g. "Chassis2 Fan") 76 | func (r *Reading) LabelOrig() string { 77 | return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szLabelOrig), C.HWiNFO_SENSORS_STRING_LEN2) 78 | } 79 | 80 | // LabelUser label displayed, which might have been renamed by user 81 | func (r *Reading) LabelUser() string { 82 | return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szLabelUser), C.HWiNFO_SENSORS_STRING_LEN2) 83 | } 84 | 85 | // Unit e.g. "RPM" 86 | func (r *Reading) Unit() string { 87 | return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szUnit), C.HWiNFO_UNIT_STRING_LEN) 88 | } 89 | 90 | func (r *Reading) valuePtr() unsafe.Pointer { 91 | return unsafe.Pointer(uintptr(unsafe.Pointer(&r.cr.szUnit)) + C.HWiNFO_UNIT_STRING_LEN) 92 | } 93 | 94 | // Value current value 95 | func (r *Reading) Value() float64 { 96 | return float64(*(*C.double)(r.valuePtr())) 97 | } 98 | 99 | func (r *Reading) valueMinPtr() unsafe.Pointer { 100 | return unsafe.Pointer(uintptr(r.valuePtr()) + C.sizeof_double) 101 | } 102 | 103 | // ValueMin current value 104 | func (r *Reading) ValueMin() float64 { 105 | return float64(*(*C.double)(r.valueMinPtr())) 106 | } 107 | 108 | func (r *Reading) valueMaxPtr() unsafe.Pointer { 109 | return unsafe.Pointer(uintptr(r.valueMinPtr()) + C.sizeof_double) 110 | } 111 | 112 | // ValueMax current value 113 | func (r *Reading) ValueMax() float64 { 114 | return float64(*(*C.double)(r.valueMaxPtr())) 115 | } 116 | 117 | func (r *Reading) valueAvgPtr() unsafe.Pointer { 118 | return unsafe.Pointer(uintptr(r.valueMaxPtr()) + C.sizeof_double) 119 | } 120 | 121 | // ValueAvg current value 122 | func (r *Reading) ValueAvg() float64 { 123 | return float64(*(*C.double)(r.valueAvgPtr())) 124 | } 125 | -------------------------------------------------------------------------------- /internal/hwinfo/sensor.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #include 5 | #include "hwisenssm2.h" 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "strconv" 11 | "unsafe" 12 | 13 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" 14 | ) 15 | 16 | // Sensor element (e.g. motherboard, cpu, gpu...) 17 | type Sensor struct { 18 | cs C.PHWiNFO_SENSORS_SENSOR_ELEMENT 19 | } 20 | 21 | // NewSensor constructs a Sensor 22 | func NewSensor(data []byte) Sensor { 23 | return Sensor{ 24 | cs: C.PHWiNFO_SENSORS_SENSOR_ELEMENT(unsafe.Pointer(&data[0])), 25 | } 26 | } 27 | 28 | // SensorID a unique Sensor ID 29 | func (s *Sensor) SensorID() uint64 { 30 | return uint64(s.cs.dwSensorID) 31 | } 32 | 33 | // SensorInst the instance of the sensor (together with SensorID forms a unique ID) 34 | func (s *Sensor) SensorInst() uint64 { 35 | return uint64(s.cs.dwSensorInst) 36 | } 37 | 38 | // ID a unique ID combining SensorID and SensorInst 39 | func (s *Sensor) ID() string { 40 | // keeping old method used in legacy steam deck plugin 41 | return strconv.FormatUint(s.SensorID()*100+s.SensorInst(), 10) 42 | } 43 | 44 | // NameOrig original name of sensor 45 | func (s *Sensor) NameOrig() string { 46 | return util.DecodeCharPtr(unsafe.Pointer(&s.cs.szSensorNameOrig), C.HWiNFO_SENSORS_STRING_LEN2) 47 | } 48 | 49 | // NameUser sensor name displayed, which might have been renamed by user 50 | func (s *Sensor) NameUser() string { 51 | return util.DecodeCharPtr(unsafe.Pointer(&s.cs.szSensorNameUser), C.HWiNFO_SENSORS_STRING_LEN2) 52 | } 53 | -------------------------------------------------------------------------------- /internal/hwinfo/shmem/shmem.go: -------------------------------------------------------------------------------- 1 | package shmem 2 | 3 | /* 4 | #include 5 | #include "../hwisenssm2.h" 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "fmt" 11 | "reflect" 12 | "syscall" 13 | "unsafe" 14 | 15 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/mutex" 16 | "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" 17 | "golang.org/x/sys/windows" 18 | ) 19 | 20 | var buf = make([]byte, 200000) 21 | 22 | func copyBytes(addr uintptr) []byte { 23 | headerLen := C.sizeof_HWiNFO_SENSORS_SHARED_MEM2 24 | 25 | var d []byte 26 | dh := (*reflect.SliceHeader)(unsafe.Pointer(&d)) 27 | 28 | dh.Data = addr 29 | dh.Len, dh.Cap = headerLen, headerLen 30 | 31 | cheader := C.PHWiNFO_SENSORS_SHARED_MEM2(unsafe.Pointer(&d[0])) 32 | fullLen := int(cheader.dwOffsetOfReadingSection + (cheader.dwSizeOfReadingElement * cheader.dwNumReadingElements)) 33 | 34 | if fullLen > cap(buf) { 35 | buf = append(buf, make([]byte, fullLen-cap(buf))...) 36 | } 37 | 38 | dh.Len, dh.Cap = fullLen, fullLen 39 | 40 | copy(buf, d) 41 | 42 | return buf[:fullLen] 43 | } 44 | 45 | // ReadBytes copies bytes from global shared memory 46 | func ReadBytes() ([]byte, error) { 47 | err := mutex.Lock() 48 | defer mutex.Unlock() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | hnd, err := openFileMapping() 54 | if err != nil { 55 | return nil, err 56 | } 57 | addr, err := mapViewOfFile(hnd) 58 | if err != nil { 59 | return nil, err 60 | } 61 | defer unmapViewOfFile(addr) 62 | defer windows.CloseHandle(windows.Handle(unsafe.Pointer(hnd))) 63 | 64 | return copyBytes(addr), nil 65 | } 66 | 67 | func openFileMapping() (C.HANDLE, error) { 68 | lpName := C.CString(C.HWiNFO_SENSORS_MAP_FILE_NAME2) 69 | defer C.free(unsafe.Pointer(lpName)) 70 | 71 | hnd := C.OpenFileMapping(syscall.FILE_MAP_READ, 0, lpName) 72 | if hnd == C.HANDLE(C.NULL) { 73 | errstr := util.HandleLastError(uint64(C.GetLastError())) 74 | return nil, fmt.Errorf("OpenFileMapping: %w", errstr) 75 | } 76 | 77 | return hnd, nil 78 | } 79 | 80 | func mapViewOfFile(hnd C.HANDLE) (uintptr, error) { 81 | addr, err := windows.MapViewOfFile(windows.Handle(unsafe.Pointer(hnd)), C.FILE_MAP_READ, 0, 0, 0) 82 | if err != nil { 83 | return 0, fmt.Errorf("MapViewOfFile: %w", err) 84 | } 85 | 86 | return addr, nil 87 | } 88 | 89 | func unmapViewOfFile(ptr uintptr) error { 90 | err := windows.UnmapViewOfFile(ptr) 91 | if err != nil { 92 | return fmt.Errorf("UnmapViewOfFile: %w", err) 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/hwinfo/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "C" 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "log" 9 | "strings" 10 | "unsafe" 11 | 12 | "golang.org/x/text/encoding/charmap" 13 | ) 14 | 15 | // ErrFileNotFound Windows error 16 | var ErrFileNotFound = errors.New("file not found") 17 | 18 | // ErrInvalidHandle Windows error 19 | var ErrInvalidHandle = errors.New("invalid handle") 20 | 21 | // UnknownError unhandled Windows error 22 | type UnknownError struct { 23 | Code uint64 24 | } 25 | 26 | func (e UnknownError) Error() string { 27 | return fmt.Sprintf("unknown error code: %d", e.Code) 28 | } 29 | 30 | // HandleLastError converts C.GetLastError() to golang error 31 | func HandleLastError(code uint64) error { 32 | switch code { 33 | case 2: // ERROR_FILE_NOT_FOUND 34 | return ErrFileNotFound 35 | case 6: // ERROR_INVALID_HANDLE 36 | return ErrInvalidHandle 37 | default: 38 | return UnknownError{Code: code} 39 | } 40 | } 41 | 42 | func goStringFromPtr(ptr unsafe.Pointer, len int) string { 43 | s := C.GoStringN((*C.char)(ptr), C.int(len)) 44 | return s[:strings.IndexByte(s, 0)] 45 | } 46 | 47 | // DecodeCharPtr decodes ISO8859_1 string to UTF-8 48 | func DecodeCharPtr(ptr unsafe.Pointer, len int) string { 49 | s := goStringFromPtr(ptr, len) 50 | ds, err := decodeISO8859_1(s) 51 | if err != nil { 52 | log.Fatalf("TODO: failed to decode: %v", err) 53 | } 54 | return ds 55 | } 56 | 57 | var isodecoder = charmap.ISO8859_1.NewDecoder() 58 | 59 | func decodeISO8859_1(in string) (string, error) { 60 | return isodecoder.String(in) 61 | } 62 | -------------------------------------------------------------------------------- /kill-streamdeck.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | taskkill /F /IM StreamDeck.exe /T -------------------------------------------------------------------------------- /make-release.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | del build\com.exension.hwinfo.streamDeckPlugin 3 | DistributionTool.exe com.exension.hwinfo.sdPlugin build 4 | -------------------------------------------------------------------------------- /pkg/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "math" 9 | "regexp" 10 | 11 | "github.com/golang/freetype/truetype" 12 | "golang.org/x/image/font" 13 | "golang.org/x/image/math/fixed" 14 | 15 | "image" 16 | "image/color" 17 | "image/png" 18 | "sync" 19 | ) 20 | 21 | // Label struct contains text, position and color information 22 | type Label struct { 23 | text string 24 | y uint 25 | fontSize float64 26 | clr *color.RGBA 27 | } 28 | 29 | // Graph is used to display a histogram of data passed to Update 30 | type Graph struct { 31 | img *image.RGBA 32 | 33 | lvay int 34 | width int 35 | height int 36 | min int 37 | max int 38 | 39 | yvals []uint8 40 | 41 | fgColor *color.RGBA 42 | bgColor *color.RGBA 43 | hlColor *color.RGBA 44 | 45 | labels map[int]*Label 46 | drawn bool 47 | redraw bool 48 | } 49 | 50 | // FontFaceManager builds and caches fonts based on size 51 | type FontFaceManager struct { 52 | mux sync.Mutex 53 | fontCache map[float64]font.Face 54 | } 55 | 56 | // NewFontFaceManager constructs new manager 57 | func NewFontFaceManager() *FontFaceManager { 58 | return &FontFaceManager{fontCache: make(map[float64]font.Face)} 59 | } 60 | 61 | func (f *FontFaceManager) newFace(size float64) font.Face { 62 | b, err := ioutil.ReadFile("DejaVuSans-Bold.ttf") 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | tt, err := truetype.Parse(b) 67 | if err != nil { 68 | log.Fatal("failed to parse font") 69 | } 70 | face := truetype.NewFace(tt, &truetype.Options{Size: size, DPI: 72}) 71 | return face 72 | } 73 | 74 | // GetFaceOfSize returns font face for given size 75 | func (f *FontFaceManager) GetFaceOfSize(size float64) font.Face { 76 | f.mux.Lock() 77 | defer f.mux.Unlock() 78 | if f, ok := f.fontCache[size]; ok { 79 | return f 80 | } 81 | nf := f.newFace(size) 82 | f.fontCache[size] = nf 83 | return nf 84 | } 85 | 86 | type singleshared struct { 87 | fontFaceManager *FontFaceManager 88 | pngEnc *png.Encoder 89 | pngBuf *bytes.Buffer 90 | } 91 | 92 | var sharedinstance *singleshared 93 | var once sync.Once 94 | 95 | func shared() *singleshared { 96 | once.Do(func() { 97 | sharedinstance = &singleshared{ 98 | pngEnc: &png.Encoder{ 99 | CompressionLevel: png.NoCompression, 100 | }, 101 | pngBuf: bytes.NewBuffer(make([]byte, 0, 15697)), 102 | } 103 | sharedinstance.fontFaceManager = NewFontFaceManager() 104 | }) 105 | return sharedinstance 106 | } 107 | 108 | // NewGraph initializes a new Graph for rendering 109 | func NewGraph(width, height, min, max int, fgColor, bgColor, hlColor *color.RGBA) *Graph { 110 | img := image.NewRGBA(image.Rect(0, 0, int(width), int(height))) 111 | labels := make(map[int]*Label) 112 | 113 | return &Graph{ 114 | img: img, 115 | lvay: -1, 116 | width: width, 117 | height: height, 118 | min: min, 119 | max: max, 120 | labels: labels, 121 | 122 | yvals: make([]uint8, 0, width), 123 | 124 | fgColor: fgColor, 125 | bgColor: bgColor, 126 | hlColor: hlColor, 127 | } 128 | } 129 | 130 | // SetForegroundColor sets the foreground color of the graph 131 | func (g *Graph) SetForegroundColor(clr *color.RGBA) { 132 | g.fgColor = clr 133 | g.redraw = true 134 | } 135 | 136 | // SetBackgroundColor sets the background color of the graph 137 | func (g *Graph) SetBackgroundColor(clr *color.RGBA) { 138 | g.bgColor = clr 139 | g.redraw = true 140 | } 141 | 142 | // SetHighlightColor sets the highlight color of the graph 143 | func (g *Graph) SetHighlightColor(clr *color.RGBA) { 144 | g.hlColor = clr 145 | g.redraw = true 146 | } 147 | 148 | // SetMin sets the min value for the graph scale 149 | func (g *Graph) SetMin(min int) { 150 | g.min = min 151 | } 152 | 153 | // SetMax sets the max value for the graph scale 154 | func (g *Graph) SetMax(max int) { 155 | g.max = max 156 | } 157 | 158 | // SetLabel given a key, set the initial text, position and color 159 | func (g *Graph) SetLabel(key int, text string, y uint, clr *color.RGBA) { 160 | l := &Label{text: text, y: y, clr: clr} 161 | g.labels[key] = l 162 | } 163 | 164 | // SetLabelText given a key, update the text for a pre-set label 165 | func (g *Graph) SetLabelText(key int, text string) error { 166 | l, ok := g.labels[key] 167 | if !ok { 168 | return fmt.Errorf("Label with key (%d) does not exist", key) 169 | } 170 | l.text = text 171 | return nil 172 | } 173 | 174 | // SetLabelFontSize given a key, update the text for a pre-set label 175 | func (g *Graph) SetLabelFontSize(key int, size float64) error { 176 | l, ok := g.labels[key] 177 | if !ok { 178 | return fmt.Errorf("Label with key (%d) does not exist", key) 179 | } 180 | l.fontSize = size 181 | return nil 182 | } 183 | 184 | // SetLabelColor given a key and color, sets the color of the text 185 | func (g *Graph) SetLabelColor(key int, clr *color.RGBA) error { 186 | l, ok := g.labels[key] 187 | if !ok { 188 | return fmt.Errorf("Label with key (%d) does not exist", key) 189 | } 190 | l.clr = clr 191 | return nil 192 | } 193 | 194 | func (g *Graph) drawGraph(x, vay, maxx int) { 195 | var clr *color.RGBA 196 | for ; x <= maxx; x++ { 197 | for y := 0; y < g.height; y++ { 198 | if y == vay { 199 | clr = g.hlColor 200 | } else if g.lvay != -1 && vay > g.lvay && vay >= y && y >= g.lvay { 201 | clr = g.hlColor 202 | } else if g.lvay != -1 && vay < g.lvay && vay <= y && y <= g.lvay { 203 | clr = g.hlColor 204 | } else if vay > y { 205 | clr = g.fgColor 206 | } else { 207 | clr = g.bgColor 208 | } 209 | i := g.img.PixOffset(x, g.width-1-y) 210 | g.img.Pix[i+0] = clr.R 211 | g.img.Pix[i+1] = clr.G 212 | g.img.Pix[i+2] = clr.B 213 | g.img.Pix[i+3] = clr.A 214 | } 215 | g.lvay = vay 216 | } 217 | } 218 | 219 | // Update given a value draws the graph, shifting contents left. Call EncodePNG to get a rendered PNG 220 | func (g *Graph) Update(value float64) { 221 | vay := vAsY(g.height-1, value, g.min, g.max) 222 | 223 | if len(g.yvals) >= g.width { 224 | _, a := g.yvals[0], g.yvals[1:] 225 | g.yvals = a 226 | } 227 | g.yvals = append(g.yvals, uint8(vay)) 228 | 229 | if g.redraw { 230 | g.lvay = -1 231 | lyvals := len(g.yvals) 232 | for idx := lyvals - 1; idx >= 0; idx-- { 233 | x := g.width - lyvals + idx 234 | maxx := x 235 | if idx == 0 { 236 | x = 0 237 | } 238 | v := int(g.yvals[idx]) 239 | g.drawGraph(x, v, maxx) 240 | } 241 | g.lvay = int(g.yvals[lyvals-1]) 242 | g.redraw = false 243 | } else if g.drawn { 244 | // shift the graph left 1px 245 | for y := 0; y < g.height; y++ { 246 | idx := g.img.PixOffset(0, y) 247 | p1 := g.img.Pix[:idx] 248 | p2 := g.img.Pix[idx+4 : idx+(g.width*4)] 249 | p3 := g.img.Pix[idx+(g.width*4):] 250 | g.img.Pix = append(p1, append(append(p2, []uint8{0, 0, 0, 0}...), p3...)...) 251 | } 252 | g.drawGraph(int(g.width)-1, int(vay), g.width-1) 253 | } else { 254 | g.drawGraph(0, vay, g.width-1) 255 | g.drawn = true 256 | } 257 | } 258 | 259 | // EncodePNG renders the current state of the graph 260 | func (g *Graph) EncodePNG() ([]byte, error) { 261 | bak := append(g.img.Pix[:0:0], g.img.Pix...) 262 | for _, l := range g.labels { 263 | g.drawLabel(l) 264 | } 265 | shared := shared() 266 | err := shared.pngEnc.Encode(shared.pngBuf, g.img) 267 | if err != nil { 268 | return nil, err 269 | } 270 | g.img.Pix = bak 271 | bts := shared.pngBuf.Bytes() 272 | shared.pngBuf.Reset() 273 | return bts, nil 274 | } 275 | 276 | func vAsY(maxY int, v float64, minV, maxV int) int { 277 | r := maxV - minV 278 | v1 := v - float64(minV) 279 | yf := v1 / float64(r) * float64(maxY) 280 | yi := int(math.Round(yf)) 281 | return yi 282 | } 283 | 284 | func unfix(x fixed.Int26_6) float64 { 285 | const shift, mask = 6, 1<<6 - 1 286 | if x >= 0 { 287 | return float64(x>>shift) + float64(x&mask)/64 288 | } 289 | x = -x 290 | if x >= 0 { 291 | return -(float64(x>>shift) + float64(x&mask)/64) 292 | } 293 | return 0 294 | } 295 | 296 | var newlineRegex = regexp.MustCompile("(\n|\\\\n)+") 297 | 298 | func (g *Graph) drawLabel(l *Label) { 299 | shared := shared() 300 | lines := newlineRegex.Split(l.text, -1) 301 | face := shared.fontFaceManager.GetFaceOfSize(l.fontSize) 302 | curY := l.y - uint(10.5-float64(face.Metrics().Height.Round())) 303 | 304 | for _, line := range lines { 305 | var lwidth float64 306 | for _, x := range line { 307 | awidth, ok := face.GlyphAdvance(rune(x)) 308 | if ok != true { 309 | log.Println("drawLabel: Failed to GlyphAdvance") 310 | return 311 | } 312 | lwidth += unfix(awidth) 313 | } 314 | 315 | lx := (float64(g.width) / 2.) - (lwidth / 2.) 316 | point := fixed.Point26_6{X: fixed.Int26_6(lx * 64), Y: fixed.Int26_6(curY * 64)} 317 | 318 | d := &font.Drawer{ 319 | Dst: g.img, 320 | Src: image.NewUniform(l.clr), 321 | Face: face, 322 | Dot: point, 323 | } 324 | d.DrawString(line) 325 | curY += 12 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /pkg/service/grpc.go: -------------------------------------------------------------------------------- 1 | package hwsensorsservice 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | 8 | "github.com/golang/protobuf/ptypes/empty" 9 | "github.com/shayne/hwinfo-streamdeck/pkg/service/proto" 10 | ) 11 | 12 | // GRPCClient is an implementation of KV that talks over RPC. 13 | type GRPCClient struct { 14 | Client proto.HWServiceClient 15 | } 16 | 17 | // PollTime rpc call 18 | func (c *GRPCClient) PollTime() (uint64, error) { 19 | resp, err := c.Client.PollTime(context.Background(), &empty.Empty{}) 20 | if err != nil { 21 | return 0, err 22 | } 23 | return resp.GetPollTime(), nil 24 | } 25 | 26 | // Sensors implementation 27 | func (c *GRPCClient) Sensors() ([]Sensor, error) { 28 | stream, err := c.Client.Sensors(context.Background(), &empty.Empty{}) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | var sensors []Sensor 34 | for { 35 | s, err := stream.Recv() 36 | if errors.Is(err, io.EOF) { 37 | break 38 | } 39 | if err != nil { 40 | return nil, err 41 | } 42 | sensors = append(sensors, &sensor{s}) 43 | } 44 | 45 | return sensors, nil 46 | } 47 | 48 | // ReadingsForSensorID implementation 49 | func (c *GRPCClient) ReadingsForSensorID(id string) ([]Reading, error) { 50 | stream, err := c.Client.ReadingsForSensorID(context.Background(), &proto.SensorIDRequest{Id: id}) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | var readings []Reading 56 | for { 57 | r, err := stream.Recv() 58 | if errors.Is(err, io.EOF) { 59 | break 60 | } 61 | if err != nil { 62 | return nil, err 63 | } 64 | readings = append(readings, &reading{r}) 65 | } 66 | 67 | return readings, nil 68 | } 69 | 70 | // GRPCServer is the gRPC server that GRPCClient talks to. 71 | type GRPCServer struct { 72 | // This is the real implementation 73 | Impl HardwareService 74 | proto.UnimplementedHWServiceServer 75 | } 76 | 77 | // PollTime gRPC wrapper 78 | func (s *GRPCServer) PollTime(ctx context.Context, _ *empty.Empty) (*proto.PollTimeReply, error) { 79 | v, err := s.Impl.PollTime() 80 | return &proto.PollTimeReply{PollTime: v}, err 81 | } 82 | 83 | // Sensors gRPC wrapper 84 | func (s *GRPCServer) Sensors(_ *empty.Empty, stream proto.HWService_SensorsServer) error { 85 | sensors, err := s.Impl.Sensors() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | for _, sensor := range sensors { 91 | if err := stream.Send(&proto.Sensor{ 92 | ID: sensor.ID(), 93 | Name: sensor.Name(), 94 | }); err != nil { 95 | return err 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // ReadingsForSensorID gRPC wrapper 103 | func (s *GRPCServer) ReadingsForSensorID(req *proto.SensorIDRequest, stream proto.HWService_ReadingsForSensorIDServer) error { 104 | readings, err := s.Impl.ReadingsForSensorID(req.GetId()) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | for _, reading := range readings { 110 | if err := stream.Send(&proto.Reading{ 111 | ID: reading.ID(), 112 | TypeI: reading.TypeI(), 113 | Type: reading.Type(), 114 | Label: reading.Label(), 115 | Unit: reading.Unit(), 116 | Value: reading.Value(), 117 | ValueMin: reading.ValueMin(), 118 | ValueMax: reading.ValueMax(), 119 | ValueAvg: reading.ValueAvg(), 120 | }); err != nil { 121 | return err 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /pkg/service/interface.go: -------------------------------------------------------------------------------- 1 | package hwsensorsservice 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | 8 | "github.com/hashicorp/go-plugin" 9 | "github.com/shayne/hwinfo-streamdeck/pkg/service/proto" 10 | ) 11 | 12 | // Handshake is a common handshake that is shared by plugin and host. 13 | var Handshake = plugin.HandshakeConfig{ 14 | // This isn't required when using VersionedPlugins 15 | ProtocolVersion: 1, 16 | MagicCookieKey: "BASIC_PLUGIN", 17 | MagicCookieValue: "hello", 18 | } 19 | 20 | // PluginMap is the map of plugins we can dispense. 21 | var PluginMap = map[string]plugin.Plugin{ 22 | "hwinfoplugin": &HardwareServicePlugin{}, 23 | } 24 | 25 | // HardwareService is the interface that we're exposing as a plugin. 26 | type HardwareService interface { 27 | PollTime() (uint64, error) 28 | Sensors() ([]Sensor, error) 29 | ReadingsForSensorID(id string) ([]Reading, error) 30 | } 31 | 32 | // HardwareServicePlugin is the implementation of plugin.GRPCPlugin so we can serve/consume this. 33 | type HardwareServicePlugin struct { 34 | // GRPCPlugin must still implement the Plugin interface 35 | plugin.Plugin 36 | // Concrete implementation, written in Go. This is only used for plugins 37 | // that are written in Go. 38 | Impl HardwareService 39 | } 40 | 41 | // GRPCServer constructor 42 | func (p *HardwareServicePlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 43 | proto.RegisterHWServiceServer(s, &GRPCServer{Impl: p.Impl}) 44 | return nil 45 | } 46 | 47 | // GRPCClient constructor 48 | func (p *HardwareServicePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 49 | return &GRPCClient{Client: proto.NewHWServiceClient(c)}, nil 50 | } 51 | 52 | // Sensor is the common hardware interface for a sensor 53 | type Sensor interface { 54 | ID() string 55 | Name() string 56 | } 57 | 58 | // ReadingType enum of value/unit type for reading 59 | type ReadingType int 60 | 61 | const ( 62 | // ReadingTypeNone no type 63 | ReadingTypeNone ReadingType = iota 64 | // ReadingTypeTemp temperature in celsius 65 | ReadingTypeTemp 66 | // ReadingTypeVolt voltage 67 | ReadingTypeVolt 68 | // ReadingTypeFan RPM 69 | ReadingTypeFan 70 | // ReadingTypeCurrent amps 71 | ReadingTypeCurrent 72 | // ReadingTypePower watts 73 | ReadingTypePower 74 | // ReadingTypeClock Mhz 75 | ReadingTypeClock 76 | // ReadingTypeUsage e.g. MBs 77 | ReadingTypeUsage 78 | // ReadingTypeOther other 79 | ReadingTypeOther 80 | ) 81 | 82 | func (t ReadingType) String() string { 83 | return [...]string{"None", "Temp", "Volt", "Fan", "Current", "Power", "Clock", "Usage", "Other"}[t] 84 | } 85 | 86 | // Reading is the common hardware interface for a sensor's reading 87 | type Reading interface { 88 | ID() int32 89 | TypeI() int32 90 | Type() string 91 | Label() string 92 | Unit() string 93 | Value() float64 94 | ValueMin() float64 95 | ValueMax() float64 96 | ValueAvg() float64 97 | } 98 | 99 | type sensor struct { 100 | *proto.Sensor 101 | } 102 | 103 | func (s sensor) ID() string { 104 | return s.Sensor.GetID() 105 | } 106 | 107 | func (s sensor) Name() string { 108 | return s.Sensor.GetName() 109 | } 110 | 111 | type reading struct { 112 | *proto.Reading 113 | } 114 | 115 | func (r reading) ID() int32 { 116 | return r.Reading.GetID() 117 | } 118 | 119 | func (r reading) Label() string { 120 | return r.Reading.GetLabel() 121 | } 122 | 123 | func (r reading) Type() string { 124 | return r.Reading.GetType() 125 | } 126 | 127 | func (r reading) TypeI() int32 { 128 | return r.Reading.GetTypeI() 129 | } 130 | 131 | func (r reading) Unit() string { 132 | return r.Reading.GetUnit() 133 | } 134 | 135 | func (r reading) Value() float64 { 136 | return r.Reading.GetValue() 137 | } 138 | 139 | func (r reading) ValueMin() float64 { 140 | return r.Reading.GetValueMin() 141 | } 142 | 143 | func (r reading) ValueMax() float64 { 144 | return r.Reading.GetValueMax() 145 | } 146 | 147 | func (r reading) ValueAvg() float64 { 148 | return r.Reading.GetValueAvg() 149 | } 150 | -------------------------------------------------------------------------------- /pkg/service/proto/hwservice.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v3.21.12 5 | // source: pkg/service/proto/hwservice.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | emptypb "google.golang.org/protobuf/types/known/emptypb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type PollTimeReply struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | PollTime uint64 `protobuf:"varint,1,opt,name=pollTime,proto3" json:"pollTime,omitempty"` 30 | } 31 | 32 | func (x *PollTimeReply) Reset() { 33 | *x = PollTimeReply{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *PollTimeReply) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*PollTimeReply) ProtoMessage() {} 46 | 47 | func (x *PollTimeReply) ProtoReflect() protoreflect.Message { 48 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use PollTimeReply.ProtoReflect.Descriptor instead. 60 | func (*PollTimeReply) Descriptor() ([]byte, []int) { 61 | return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *PollTimeReply) GetPollTime() uint64 { 65 | if x != nil { 66 | return x.PollTime 67 | } 68 | return 0 69 | } 70 | 71 | type Sensor struct { 72 | state protoimpl.MessageState 73 | sizeCache protoimpl.SizeCache 74 | unknownFields protoimpl.UnknownFields 75 | 76 | ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` 77 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 78 | } 79 | 80 | func (x *Sensor) Reset() { 81 | *x = Sensor{} 82 | if protoimpl.UnsafeEnabled { 83 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[1] 84 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 85 | ms.StoreMessageInfo(mi) 86 | } 87 | } 88 | 89 | func (x *Sensor) String() string { 90 | return protoimpl.X.MessageStringOf(x) 91 | } 92 | 93 | func (*Sensor) ProtoMessage() {} 94 | 95 | func (x *Sensor) ProtoReflect() protoreflect.Message { 96 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[1] 97 | if protoimpl.UnsafeEnabled && x != nil { 98 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 99 | if ms.LoadMessageInfo() == nil { 100 | ms.StoreMessageInfo(mi) 101 | } 102 | return ms 103 | } 104 | return mi.MessageOf(x) 105 | } 106 | 107 | // Deprecated: Use Sensor.ProtoReflect.Descriptor instead. 108 | func (*Sensor) Descriptor() ([]byte, []int) { 109 | return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{1} 110 | } 111 | 112 | func (x *Sensor) GetID() string { 113 | if x != nil { 114 | return x.ID 115 | } 116 | return "" 117 | } 118 | 119 | func (x *Sensor) GetName() string { 120 | if x != nil { 121 | return x.Name 122 | } 123 | return "" 124 | } 125 | 126 | type SensorIDRequest struct { 127 | state protoimpl.MessageState 128 | sizeCache protoimpl.SizeCache 129 | unknownFields protoimpl.UnknownFields 130 | 131 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 132 | } 133 | 134 | func (x *SensorIDRequest) Reset() { 135 | *x = SensorIDRequest{} 136 | if protoimpl.UnsafeEnabled { 137 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[2] 138 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 139 | ms.StoreMessageInfo(mi) 140 | } 141 | } 142 | 143 | func (x *SensorIDRequest) String() string { 144 | return protoimpl.X.MessageStringOf(x) 145 | } 146 | 147 | func (*SensorIDRequest) ProtoMessage() {} 148 | 149 | func (x *SensorIDRequest) ProtoReflect() protoreflect.Message { 150 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[2] 151 | if protoimpl.UnsafeEnabled && x != nil { 152 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 153 | if ms.LoadMessageInfo() == nil { 154 | ms.StoreMessageInfo(mi) 155 | } 156 | return ms 157 | } 158 | return mi.MessageOf(x) 159 | } 160 | 161 | // Deprecated: Use SensorIDRequest.ProtoReflect.Descriptor instead. 162 | func (*SensorIDRequest) Descriptor() ([]byte, []int) { 163 | return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{2} 164 | } 165 | 166 | func (x *SensorIDRequest) GetId() string { 167 | if x != nil { 168 | return x.Id 169 | } 170 | return "" 171 | } 172 | 173 | type Reading struct { 174 | state protoimpl.MessageState 175 | sizeCache protoimpl.SizeCache 176 | unknownFields protoimpl.UnknownFields 177 | 178 | ID int32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` 179 | TypeI int32 `protobuf:"varint,2,opt,name=typeI,proto3" json:"typeI,omitempty"` 180 | Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` 181 | Label string `protobuf:"bytes,4,opt,name=label,proto3" json:"label,omitempty"` 182 | Unit string `protobuf:"bytes,5,opt,name=unit,proto3" json:"unit,omitempty"` 183 | Value float64 `protobuf:"fixed64,6,opt,name=value,proto3" json:"value,omitempty"` 184 | ValueMin float64 `protobuf:"fixed64,7,opt,name=valueMin,proto3" json:"valueMin,omitempty"` 185 | ValueMax float64 `protobuf:"fixed64,8,opt,name=valueMax,proto3" json:"valueMax,omitempty"` 186 | ValueAvg float64 `protobuf:"fixed64,9,opt,name=valueAvg,proto3" json:"valueAvg,omitempty"` 187 | } 188 | 189 | func (x *Reading) Reset() { 190 | *x = Reading{} 191 | if protoimpl.UnsafeEnabled { 192 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[3] 193 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 194 | ms.StoreMessageInfo(mi) 195 | } 196 | } 197 | 198 | func (x *Reading) String() string { 199 | return protoimpl.X.MessageStringOf(x) 200 | } 201 | 202 | func (*Reading) ProtoMessage() {} 203 | 204 | func (x *Reading) ProtoReflect() protoreflect.Message { 205 | mi := &file_pkg_service_proto_hwservice_proto_msgTypes[3] 206 | if protoimpl.UnsafeEnabled && x != nil { 207 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 208 | if ms.LoadMessageInfo() == nil { 209 | ms.StoreMessageInfo(mi) 210 | } 211 | return ms 212 | } 213 | return mi.MessageOf(x) 214 | } 215 | 216 | // Deprecated: Use Reading.ProtoReflect.Descriptor instead. 217 | func (*Reading) Descriptor() ([]byte, []int) { 218 | return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{3} 219 | } 220 | 221 | func (x *Reading) GetID() int32 { 222 | if x != nil { 223 | return x.ID 224 | } 225 | return 0 226 | } 227 | 228 | func (x *Reading) GetTypeI() int32 { 229 | if x != nil { 230 | return x.TypeI 231 | } 232 | return 0 233 | } 234 | 235 | func (x *Reading) GetType() string { 236 | if x != nil { 237 | return x.Type 238 | } 239 | return "" 240 | } 241 | 242 | func (x *Reading) GetLabel() string { 243 | if x != nil { 244 | return x.Label 245 | } 246 | return "" 247 | } 248 | 249 | func (x *Reading) GetUnit() string { 250 | if x != nil { 251 | return x.Unit 252 | } 253 | return "" 254 | } 255 | 256 | func (x *Reading) GetValue() float64 { 257 | if x != nil { 258 | return x.Value 259 | } 260 | return 0 261 | } 262 | 263 | func (x *Reading) GetValueMin() float64 { 264 | if x != nil { 265 | return x.ValueMin 266 | } 267 | return 0 268 | } 269 | 270 | func (x *Reading) GetValueMax() float64 { 271 | if x != nil { 272 | return x.ValueMax 273 | } 274 | return 0 275 | } 276 | 277 | func (x *Reading) GetValueAvg() float64 { 278 | if x != nil { 279 | return x.ValueAvg 280 | } 281 | return 0 282 | } 283 | 284 | var File_pkg_service_proto_hwservice_proto protoreflect.FileDescriptor 285 | 286 | var file_pkg_service_proto_hwservice_proto_rawDesc = []byte{ 287 | 0x0a, 0x21, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x72, 288 | 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x77, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 289 | 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 290 | 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 291 | 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2b, 0x0a, 0x0d, 0x50, 0x6f, 0x6c, 0x6c, 0x54, 292 | 0x69, 0x6d, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6c, 0x6c, 293 | 0x54, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x6c, 294 | 0x54, 0x69, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x06, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x12, 0x0e, 295 | 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 296 | 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 297 | 0x6d, 0x65, 0x22, 0x21, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x49, 0x44, 0x52, 0x65, 298 | 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 299 | 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xd7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 300 | 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x49, 301 | 0x44, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x49, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 302 | 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x49, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 303 | 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 304 | 0x61, 0x62, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 305 | 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 306 | 0x04, 0x75, 0x6e, 0x69, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 307 | 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 308 | 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x76, 309 | 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 310 | 0x4d, 0x61, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 311 | 0x4d, 0x61, 0x78, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x76, 0x67, 0x18, 312 | 0x09, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x76, 0x67, 0x32, 313 | 0xc0, 0x01, 0x0a, 0x09, 0x48, 0x57, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, 0x0a, 314 | 0x08, 0x50, 0x6f, 0x6c, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 315 | 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 316 | 0x79, 0x1a, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6f, 0x6c, 0x6c, 0x54, 0x69, 317 | 0x6d, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x07, 0x53, 0x65, 0x6e, 318 | 0x73, 0x6f, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 319 | 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x70, 320 | 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x22, 0x00, 0x30, 0x01, 0x12, 321 | 0x41, 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x46, 0x6f, 0x72, 0x53, 0x65, 322 | 0x6e, 0x73, 0x6f, 0x72, 0x49, 0x44, 0x12, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 323 | 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 324 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x00, 325 | 0x30, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 326 | 0x2f, 0x73, 0x68, 0x61, 0x79, 0x6e, 0x65, 0x2f, 0x68, 0x77, 0x69, 0x6e, 0x66, 0x6f, 0x2d, 0x73, 327 | 0x74, 0x72, 0x65, 0x61, 0x6d, 0x64, 0x65, 0x63, 0x6b, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 328 | 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 329 | 0x74, 0x6f, 0x33, 330 | } 331 | 332 | var ( 333 | file_pkg_service_proto_hwservice_proto_rawDescOnce sync.Once 334 | file_pkg_service_proto_hwservice_proto_rawDescData = file_pkg_service_proto_hwservice_proto_rawDesc 335 | ) 336 | 337 | func file_pkg_service_proto_hwservice_proto_rawDescGZIP() []byte { 338 | file_pkg_service_proto_hwservice_proto_rawDescOnce.Do(func() { 339 | file_pkg_service_proto_hwservice_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_service_proto_hwservice_proto_rawDescData) 340 | }) 341 | return file_pkg_service_proto_hwservice_proto_rawDescData 342 | } 343 | 344 | var file_pkg_service_proto_hwservice_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 345 | var file_pkg_service_proto_hwservice_proto_goTypes = []interface{}{ 346 | (*PollTimeReply)(nil), // 0: proto.PollTimeReply 347 | (*Sensor)(nil), // 1: proto.Sensor 348 | (*SensorIDRequest)(nil), // 2: proto.SensorIDRequest 349 | (*Reading)(nil), // 3: proto.Reading 350 | (*emptypb.Empty)(nil), // 4: google.protobuf.Empty 351 | } 352 | var file_pkg_service_proto_hwservice_proto_depIdxs = []int32{ 353 | 4, // 0: proto.HWService.PollTime:input_type -> google.protobuf.Empty 354 | 4, // 1: proto.HWService.Sensors:input_type -> google.protobuf.Empty 355 | 2, // 2: proto.HWService.ReadingsForSensorID:input_type -> proto.SensorIDRequest 356 | 0, // 3: proto.HWService.PollTime:output_type -> proto.PollTimeReply 357 | 1, // 4: proto.HWService.Sensors:output_type -> proto.Sensor 358 | 3, // 5: proto.HWService.ReadingsForSensorID:output_type -> proto.Reading 359 | 3, // [3:6] is the sub-list for method output_type 360 | 0, // [0:3] is the sub-list for method input_type 361 | 0, // [0:0] is the sub-list for extension type_name 362 | 0, // [0:0] is the sub-list for extension extendee 363 | 0, // [0:0] is the sub-list for field type_name 364 | } 365 | 366 | func init() { file_pkg_service_proto_hwservice_proto_init() } 367 | func file_pkg_service_proto_hwservice_proto_init() { 368 | if File_pkg_service_proto_hwservice_proto != nil { 369 | return 370 | } 371 | if !protoimpl.UnsafeEnabled { 372 | file_pkg_service_proto_hwservice_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 373 | switch v := v.(*PollTimeReply); i { 374 | case 0: 375 | return &v.state 376 | case 1: 377 | return &v.sizeCache 378 | case 2: 379 | return &v.unknownFields 380 | default: 381 | return nil 382 | } 383 | } 384 | file_pkg_service_proto_hwservice_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 385 | switch v := v.(*Sensor); i { 386 | case 0: 387 | return &v.state 388 | case 1: 389 | return &v.sizeCache 390 | case 2: 391 | return &v.unknownFields 392 | default: 393 | return nil 394 | } 395 | } 396 | file_pkg_service_proto_hwservice_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 397 | switch v := v.(*SensorIDRequest); i { 398 | case 0: 399 | return &v.state 400 | case 1: 401 | return &v.sizeCache 402 | case 2: 403 | return &v.unknownFields 404 | default: 405 | return nil 406 | } 407 | } 408 | file_pkg_service_proto_hwservice_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 409 | switch v := v.(*Reading); i { 410 | case 0: 411 | return &v.state 412 | case 1: 413 | return &v.sizeCache 414 | case 2: 415 | return &v.unknownFields 416 | default: 417 | return nil 418 | } 419 | } 420 | } 421 | type x struct{} 422 | out := protoimpl.TypeBuilder{ 423 | File: protoimpl.DescBuilder{ 424 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 425 | RawDescriptor: file_pkg_service_proto_hwservice_proto_rawDesc, 426 | NumEnums: 0, 427 | NumMessages: 4, 428 | NumExtensions: 0, 429 | NumServices: 1, 430 | }, 431 | GoTypes: file_pkg_service_proto_hwservice_proto_goTypes, 432 | DependencyIndexes: file_pkg_service_proto_hwservice_proto_depIdxs, 433 | MessageInfos: file_pkg_service_proto_hwservice_proto_msgTypes, 434 | }.Build() 435 | File_pkg_service_proto_hwservice_proto = out.File 436 | file_pkg_service_proto_hwservice_proto_rawDesc = nil 437 | file_pkg_service_proto_hwservice_proto_goTypes = nil 438 | file_pkg_service_proto_hwservice_proto_depIdxs = nil 439 | } 440 | -------------------------------------------------------------------------------- /pkg/service/proto/hwservice.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/shayne/hwinfo-streamdeck/pkg/service/proto"; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | package proto; 8 | 9 | service HWService { 10 | rpc PollTime(google.protobuf.Empty) returns (PollTimeReply) {} 11 | rpc Sensors(google.protobuf.Empty) returns (stream Sensor) {} 12 | rpc ReadingsForSensorID(SensorIDRequest) returns (stream Reading) {} 13 | } 14 | 15 | message PollTimeReply { uint64 pollTime = 1; } 16 | 17 | message Sensor { 18 | string ID = 1; 19 | string name = 2; 20 | } 21 | 22 | message SensorIDRequest { string id = 1; } 23 | 24 | message Reading { 25 | int32 ID = 1; 26 | int32 typeI = 2; 27 | string type = 3; 28 | string label = 4; 29 | string unit = 5; 30 | double value = 6; 31 | double valueMin = 7; 32 | double valueMax = 8; 33 | double valueAvg = 9; 34 | } 35 | -------------------------------------------------------------------------------- /pkg/service/proto/hwservice_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v3.21.12 5 | // source: pkg/service/proto/hwservice.proto 6 | 7 | package proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.32.0 or later. 20 | const _ = grpc.SupportPackageIsVersion7 21 | 22 | // HWServiceClient is the client API for HWService service. 23 | // 24 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 25 | type HWServiceClient interface { 26 | PollTime(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PollTimeReply, error) 27 | Sensors(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (HWService_SensorsClient, error) 28 | ReadingsForSensorID(ctx context.Context, in *SensorIDRequest, opts ...grpc.CallOption) (HWService_ReadingsForSensorIDClient, error) 29 | } 30 | 31 | type hWServiceClient struct { 32 | cc grpc.ClientConnInterface 33 | } 34 | 35 | func NewHWServiceClient(cc grpc.ClientConnInterface) HWServiceClient { 36 | return &hWServiceClient{cc} 37 | } 38 | 39 | func (c *hWServiceClient) PollTime(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PollTimeReply, error) { 40 | out := new(PollTimeReply) 41 | err := c.cc.Invoke(ctx, "/proto.HWService/PollTime", in, out, opts...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return out, nil 46 | } 47 | 48 | func (c *hWServiceClient) Sensors(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (HWService_SensorsClient, error) { 49 | stream, err := c.cc.NewStream(ctx, &HWService_ServiceDesc.Streams[0], "/proto.HWService/Sensors", opts...) 50 | if err != nil { 51 | return nil, err 52 | } 53 | x := &hWServiceSensorsClient{stream} 54 | if err := x.ClientStream.SendMsg(in); err != nil { 55 | return nil, err 56 | } 57 | if err := x.ClientStream.CloseSend(); err != nil { 58 | return nil, err 59 | } 60 | return x, nil 61 | } 62 | 63 | type HWService_SensorsClient interface { 64 | Recv() (*Sensor, error) 65 | grpc.ClientStream 66 | } 67 | 68 | type hWServiceSensorsClient struct { 69 | grpc.ClientStream 70 | } 71 | 72 | func (x *hWServiceSensorsClient) Recv() (*Sensor, error) { 73 | m := new(Sensor) 74 | if err := x.ClientStream.RecvMsg(m); err != nil { 75 | return nil, err 76 | } 77 | return m, nil 78 | } 79 | 80 | func (c *hWServiceClient) ReadingsForSensorID(ctx context.Context, in *SensorIDRequest, opts ...grpc.CallOption) (HWService_ReadingsForSensorIDClient, error) { 81 | stream, err := c.cc.NewStream(ctx, &HWService_ServiceDesc.Streams[1], "/proto.HWService/ReadingsForSensorID", opts...) 82 | if err != nil { 83 | return nil, err 84 | } 85 | x := &hWServiceReadingsForSensorIDClient{stream} 86 | if err := x.ClientStream.SendMsg(in); err != nil { 87 | return nil, err 88 | } 89 | if err := x.ClientStream.CloseSend(); err != nil { 90 | return nil, err 91 | } 92 | return x, nil 93 | } 94 | 95 | type HWService_ReadingsForSensorIDClient interface { 96 | Recv() (*Reading, error) 97 | grpc.ClientStream 98 | } 99 | 100 | type hWServiceReadingsForSensorIDClient struct { 101 | grpc.ClientStream 102 | } 103 | 104 | func (x *hWServiceReadingsForSensorIDClient) Recv() (*Reading, error) { 105 | m := new(Reading) 106 | if err := x.ClientStream.RecvMsg(m); err != nil { 107 | return nil, err 108 | } 109 | return m, nil 110 | } 111 | 112 | // HWServiceServer is the server API for HWService service. 113 | // All implementations must embed UnimplementedHWServiceServer 114 | // for forward compatibility 115 | type HWServiceServer interface { 116 | PollTime(context.Context, *emptypb.Empty) (*PollTimeReply, error) 117 | Sensors(*emptypb.Empty, HWService_SensorsServer) error 118 | ReadingsForSensorID(*SensorIDRequest, HWService_ReadingsForSensorIDServer) error 119 | mustEmbedUnimplementedHWServiceServer() 120 | } 121 | 122 | // UnimplementedHWServiceServer must be embedded to have forward compatible implementations. 123 | type UnimplementedHWServiceServer struct { 124 | } 125 | 126 | func (UnimplementedHWServiceServer) PollTime(context.Context, *emptypb.Empty) (*PollTimeReply, error) { 127 | return nil, status.Errorf(codes.Unimplemented, "method PollTime not implemented") 128 | } 129 | func (UnimplementedHWServiceServer) Sensors(*emptypb.Empty, HWService_SensorsServer) error { 130 | return status.Errorf(codes.Unimplemented, "method Sensors not implemented") 131 | } 132 | func (UnimplementedHWServiceServer) ReadingsForSensorID(*SensorIDRequest, HWService_ReadingsForSensorIDServer) error { 133 | return status.Errorf(codes.Unimplemented, "method ReadingsForSensorID not implemented") 134 | } 135 | func (UnimplementedHWServiceServer) mustEmbedUnimplementedHWServiceServer() {} 136 | 137 | // UnsafeHWServiceServer may be embedded to opt out of forward compatibility for this service. 138 | // Use of this interface is not recommended, as added methods to HWServiceServer will 139 | // result in compilation errors. 140 | type UnsafeHWServiceServer interface { 141 | mustEmbedUnimplementedHWServiceServer() 142 | } 143 | 144 | func RegisterHWServiceServer(s grpc.ServiceRegistrar, srv HWServiceServer) { 145 | s.RegisterService(&HWService_ServiceDesc, srv) 146 | } 147 | 148 | func _HWService_PollTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 149 | in := new(emptypb.Empty) 150 | if err := dec(in); err != nil { 151 | return nil, err 152 | } 153 | if interceptor == nil { 154 | return srv.(HWServiceServer).PollTime(ctx, in) 155 | } 156 | info := &grpc.UnaryServerInfo{ 157 | Server: srv, 158 | FullMethod: "/proto.HWService/PollTime", 159 | } 160 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 161 | return srv.(HWServiceServer).PollTime(ctx, req.(*emptypb.Empty)) 162 | } 163 | return interceptor(ctx, in, info, handler) 164 | } 165 | 166 | func _HWService_Sensors_Handler(srv interface{}, stream grpc.ServerStream) error { 167 | m := new(emptypb.Empty) 168 | if err := stream.RecvMsg(m); err != nil { 169 | return err 170 | } 171 | return srv.(HWServiceServer).Sensors(m, &hWServiceSensorsServer{stream}) 172 | } 173 | 174 | type HWService_SensorsServer interface { 175 | Send(*Sensor) error 176 | grpc.ServerStream 177 | } 178 | 179 | type hWServiceSensorsServer struct { 180 | grpc.ServerStream 181 | } 182 | 183 | func (x *hWServiceSensorsServer) Send(m *Sensor) error { 184 | return x.ServerStream.SendMsg(m) 185 | } 186 | 187 | func _HWService_ReadingsForSensorID_Handler(srv interface{}, stream grpc.ServerStream) error { 188 | m := new(SensorIDRequest) 189 | if err := stream.RecvMsg(m); err != nil { 190 | return err 191 | } 192 | return srv.(HWServiceServer).ReadingsForSensorID(m, &hWServiceReadingsForSensorIDServer{stream}) 193 | } 194 | 195 | type HWService_ReadingsForSensorIDServer interface { 196 | Send(*Reading) error 197 | grpc.ServerStream 198 | } 199 | 200 | type hWServiceReadingsForSensorIDServer struct { 201 | grpc.ServerStream 202 | } 203 | 204 | func (x *hWServiceReadingsForSensorIDServer) Send(m *Reading) error { 205 | return x.ServerStream.SendMsg(m) 206 | } 207 | 208 | // HWService_ServiceDesc is the grpc.ServiceDesc for HWService service. 209 | // It's only intended for direct use with grpc.RegisterService, 210 | // and not to be introspected or modified (even as a copy) 211 | var HWService_ServiceDesc = grpc.ServiceDesc{ 212 | ServiceName: "proto.HWService", 213 | HandlerType: (*HWServiceServer)(nil), 214 | Methods: []grpc.MethodDesc{ 215 | { 216 | MethodName: "PollTime", 217 | Handler: _HWService_PollTime_Handler, 218 | }, 219 | }, 220 | Streams: []grpc.StreamDesc{ 221 | { 222 | StreamName: "Sensors", 223 | Handler: _HWService_Sensors_Handler, 224 | ServerStreams: true, 225 | }, 226 | { 227 | StreamName: "ReadingsForSensorID", 228 | Handler: _HWService_ReadingsForSensorID_Handler, 229 | ServerStreams: true, 230 | }, 231 | }, 232 | Metadata: "pkg/service/proto/hwservice.proto", 233 | } 234 | -------------------------------------------------------------------------------- /pkg/streamdeck/streamdeck.go: -------------------------------------------------------------------------------- 1 | package streamdeck 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/url" 9 | "os" 10 | "os/signal" 11 | "time" 12 | 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | // EventDelegate receives callbacks for Stream Deck SDK events 17 | type EventDelegate interface { 18 | OnConnected(*websocket.Conn) 19 | OnWillAppear(*EvWillAppear) 20 | OnTitleParametersDidChange(*EvTitleParametersDidChange) 21 | OnPropertyInspectorConnected(*EvSendToPlugin) 22 | OnSendToPlugin(*EvSendToPlugin) 23 | OnApplicationDidLaunch(*EvApplication) 24 | OnApplicationDidTerminate(*EvApplication) 25 | } 26 | 27 | // StreamDeck SDK APIs 28 | type StreamDeck struct { 29 | Port string 30 | PluginUUID string 31 | RegisterEvent string 32 | Info string 33 | delegate EventDelegate 34 | conn *websocket.Conn 35 | done chan struct{} 36 | } 37 | 38 | // NewStreamDeck prepares StreamDeck struct 39 | func NewStreamDeck(port, pluginUUID, registerEvent, info string) *StreamDeck { 40 | return &StreamDeck{ 41 | Port: port, 42 | PluginUUID: pluginUUID, 43 | RegisterEvent: registerEvent, 44 | Info: info, 45 | done: make(chan struct{}), 46 | } 47 | } 48 | 49 | // SetDelegate sets the delegate for receiving Stream Deck SDK event callbacks 50 | func (sd *StreamDeck) SetDelegate(ed EventDelegate) { 51 | sd.delegate = ed 52 | } 53 | 54 | func (sd *StreamDeck) register() error { 55 | reg := evRegister{Event: sd.RegisterEvent, UUID: sd.PluginUUID} 56 | data, err := json.Marshal(reg) 57 | log.Println(string(data)) 58 | if err != nil { 59 | return err 60 | } 61 | err = sd.conn.WriteMessage(websocket.TextMessage, data) 62 | if err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // Connect establishes WebSocket connection to StreamDeck software 69 | func (sd *StreamDeck) Connect() error { 70 | u := url.URL{Scheme: "ws", Host: fmt.Sprintf("127.0.0.1:%s", sd.Port)} 71 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | sd.conn = c 77 | 78 | err = sd.register() 79 | if err != nil { 80 | return fmt.Errorf("failed register: %v", err) 81 | } 82 | 83 | if sd.delegate != nil { 84 | sd.delegate.OnConnected(sd.conn) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // Close closes the websocket connection, defer after Connect 91 | func (sd *StreamDeck) Close() { 92 | sd.conn.Close() 93 | } 94 | 95 | func (sd *StreamDeck) onPropertyInspectorMessage(value string, ev *EvSendToPlugin) error { 96 | switch value { 97 | case "propertyInspectorConnected": 98 | if sd.delegate != nil { 99 | sd.delegate.OnPropertyInspectorConnected(ev) 100 | } 101 | default: 102 | log.Printf("Unknown property_inspector value: %s\n", value) 103 | } 104 | return nil 105 | } 106 | 107 | func (sd *StreamDeck) onSendToPlugin(ev *EvSendToPlugin) error { 108 | payload := make(map[string]*json.RawMessage) 109 | err := json.Unmarshal(*ev.Payload, &payload) 110 | if err != nil { 111 | return fmt.Errorf("onSendToPlugin payload unmarshal: %v", err) 112 | } 113 | if raw, ok := payload["property_inspector"]; ok { 114 | var value string 115 | err := json.Unmarshal(*raw, &value) 116 | if err != nil { 117 | return fmt.Errorf("onSendToPlugin unmarshal property_inspector value: %v", err) 118 | } 119 | err = sd.onPropertyInspectorMessage(value, ev) 120 | if err != nil { 121 | return fmt.Errorf("onPropertyInspectorMessage: %v", err) 122 | } 123 | return nil 124 | } 125 | if sd.delegate != nil { 126 | sd.delegate.OnSendToPlugin(ev) 127 | } 128 | return nil 129 | } 130 | 131 | func (sd *StreamDeck) spawnMessageReader() { 132 | defer close(sd.done) 133 | for { 134 | _, message, err := sd.conn.ReadMessage() 135 | if err != nil { 136 | log.Println("read:", err) 137 | return 138 | } 139 | log.Printf("recv: %s", message) 140 | 141 | var objmap map[string]*json.RawMessage 142 | err = json.Unmarshal(message, &objmap) 143 | if err != nil { 144 | log.Fatal("message unmarshal", err) 145 | } 146 | var event string 147 | err = json.Unmarshal(*objmap["event"], &event) 148 | if err != nil { 149 | log.Fatal("event unmarshal", err) 150 | } 151 | switch event { 152 | case "willAppear": 153 | var ev EvWillAppear 154 | err := json.Unmarshal(message, &ev) 155 | if err != nil { 156 | log.Fatal("willAppear unmarshal", err) 157 | } 158 | if sd.delegate != nil { 159 | sd.delegate.OnWillAppear(&ev) 160 | } 161 | case "titleParametersDidChange": 162 | var ev EvTitleParametersDidChange 163 | err := json.Unmarshal(message, &ev) 164 | if err != nil { 165 | log.Fatal("titleParametersDidChange unmarshal", err) 166 | } 167 | if sd.delegate != nil { 168 | sd.delegate.OnTitleParametersDidChange(&ev) 169 | } 170 | case "sendToPlugin": 171 | var ev EvSendToPlugin 172 | err := json.Unmarshal(message, &ev) 173 | if err != nil { 174 | log.Fatal("onSendToPlugin event unmarshal", err) 175 | } 176 | err = sd.onSendToPlugin(&ev) 177 | if err != nil { 178 | log.Fatal("onSendToPlugin", err) 179 | } 180 | case "applicationDidLaunch": 181 | var ev EvApplication 182 | err := json.Unmarshal(message, &ev) 183 | if err != nil { 184 | log.Fatal("applicationDidLaunch unmarshal", err) 185 | } 186 | if sd.delegate != nil { 187 | sd.delegate.OnApplicationDidLaunch(&ev) 188 | } 189 | case "applicationDidTerminate": 190 | var ev EvApplication 191 | err := json.Unmarshal(message, &ev) 192 | if err != nil { 193 | log.Fatal("applicationDidTerminate unmarshal", err) 194 | } 195 | if sd.delegate != nil { 196 | sd.delegate.OnApplicationDidTerminate(&ev) 197 | } 198 | default: 199 | log.Printf("Unknown event: %s\n", event) 200 | } 201 | } 202 | } 203 | 204 | // ListenAndWait processes messages and waits until closed 205 | func (sd *StreamDeck) ListenAndWait() { 206 | go sd.spawnMessageReader() 207 | 208 | interrupt := make(chan os.Signal, 1) 209 | signal.Notify(interrupt, os.Interrupt) 210 | 211 | for { 212 | select { 213 | case <-sd.done: 214 | return 215 | case <-interrupt: 216 | log.Println("interrupt") 217 | 218 | // Cleanly close the connection by sending a close message and then 219 | // waiting (with timeout) for the server to close the connection. 220 | err := sd.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 221 | if err != nil { 222 | log.Println("write close:", err) 223 | return 224 | } 225 | select { 226 | case <-sd.done: 227 | case <-time.After(time.Second): 228 | } 229 | } 230 | } 231 | } 232 | 233 | // SendToPropertyInspector sends a payload to the Property Inspector 234 | func (sd *StreamDeck) SendToPropertyInspector(action, context string, payload interface{}) error { 235 | event := evSendToPropertyInspector{Action: action, Event: "sendToPropertyInspector", 236 | Context: context, Payload: payload} 237 | data, err := json.Marshal(event) 238 | if err != nil { 239 | return fmt.Errorf("sendToPropertyInspector: %v", err) 240 | } 241 | err = sd.conn.WriteMessage(websocket.TextMessage, data) 242 | if err != nil { 243 | return fmt.Errorf("setTitle write: %v", err) 244 | } 245 | return nil 246 | } 247 | 248 | // SetTitle dynamically changes the title displayed by an instance of an action 249 | func (sd *StreamDeck) SetTitle(context, title string) error { 250 | event := evSetTitle{Event: "setTitle", Context: context, Payload: evSetTitlePayload{ 251 | Title: title, 252 | Target: 0, 253 | }} 254 | data, err := json.Marshal(event) 255 | if err != nil { 256 | return fmt.Errorf("setTitle: %v", err) 257 | } 258 | err = sd.conn.WriteMessage(websocket.TextMessage, data) 259 | if err != nil { 260 | return fmt.Errorf("setTitle write: %v", err) 261 | } 262 | return nil 263 | } 264 | 265 | // SetSettings saves persistent data for the action's instance 266 | func (sd *StreamDeck) SetSettings(context string, payload interface{}) error { 267 | event := evSetSettings{Event: "setSettings", Context: context, Payload: payload} 268 | data, err := json.Marshal(event) 269 | if err != nil { 270 | return fmt.Errorf("setSettings: %v", err) 271 | } 272 | err = sd.conn.WriteMessage(websocket.TextMessage, data) 273 | if err != nil { 274 | return fmt.Errorf("setSettings write: %v", err) 275 | } 276 | return nil 277 | } 278 | 279 | // SetImage dynamically changes the image displayed by an instance of an action 280 | func (sd *StreamDeck) SetImage(context string, bts []byte) error { 281 | b64 := base64.StdEncoding.EncodeToString(bts) 282 | event := evSetImage{Event: "setImage", Context: context, Payload: evSetImagePayload{ 283 | Image: fmt.Sprintf("data:image/png;base64, %s", b64), 284 | Target: 0, 285 | }} 286 | data, err := json.Marshal(event) 287 | if err != nil { 288 | return fmt.Errorf("setImage: %v", err) 289 | } 290 | err = sd.conn.WriteMessage(websocket.TextMessage, data) 291 | if err != nil { 292 | return fmt.Errorf("setImage write: %v", err) 293 | } 294 | return nil 295 | } 296 | -------------------------------------------------------------------------------- /pkg/streamdeck/types.go: -------------------------------------------------------------------------------- 1 | package streamdeck 2 | 3 | import "encoding/json" 4 | 5 | type evRegister struct { 6 | Event string `json:"event"` 7 | UUID string `json:"uuid"` 8 | } 9 | 10 | // EvCoordinates is the coordinates structure from events 11 | type EvCoordinates struct { 12 | Column int `json:"column"` 13 | Row int `json:"row"` 14 | } 15 | 16 | // EvWillAppearPayload is the Payload structure from the willAppear event 17 | type EvWillAppearPayload struct { 18 | Settings *json.RawMessage `json:"settings"` 19 | Coordinates EvCoordinates `json:"coordinates"` 20 | Device string `json:"device"` 21 | State int `json:"state"` 22 | IsInMultiAction bool `json:"isInMultiAction"` 23 | } 24 | 25 | // EvWillAppear is the payload from the willAppear event 26 | type EvWillAppear struct { 27 | Action string `json:"action"` 28 | Event string `json:"event"` 29 | Context string `json:"context"` 30 | Device string `json:"device"` 31 | Payload EvWillAppearPayload `json:"payload"` 32 | } 33 | 34 | // EvWillDisappearPayload is the Payload structure from willDisappear event 35 | type EvWillDisappearPayload struct { 36 | EvWillAppearPayload 37 | } 38 | 39 | // EvWillDisappear is the payload from the willDisappear event 40 | type EvWillDisappear struct { 41 | EvWillAppear 42 | } 43 | 44 | // EvApplicationPayload is the sub-strcture from the EvApplication struct 45 | type EvApplicationPayload struct { 46 | Application string `json:"application"` 47 | } 48 | 49 | // EvApplication is the payload from the applicatioDidLaunch/Terminate events 50 | type EvApplication struct { 51 | Payload EvApplicationPayload `json:"payload"` 52 | } 53 | 54 | // EvTitleParameters is sub-structure from EvTitleParametersDidChangePayload 55 | type EvTitleParameters struct { 56 | FontFamily string `json:"fontFamily"` 57 | FontSize int `json:"fontSize"` 58 | FontStyle string `json:"fontStyle"` 59 | FontUnderline bool `json:"fontUnderline"` 60 | ShowTitle bool `json:"showTitle"` 61 | TitleAlignment string `json:"titleAlignment"` 62 | TitleColor string `json:"titleColor"` 63 | } 64 | 65 | // EvTitleParametersDidChangePayload is the payload structure of EvTitleParametersDidChange 66 | type EvTitleParametersDidChangePayload struct { 67 | Coordinates EvCoordinates `json:"coordinates"` 68 | Settings *json.RawMessage `json:"settings"` 69 | TitleParameters EvTitleParameters `json:"titleParameters"` 70 | Title string `json:"title"` 71 | State int `json:"state"` 72 | } 73 | 74 | // EvTitleParametersDidChange is the payload from the titleParametersDidChange event 75 | type EvTitleParametersDidChange struct { 76 | Action string `json:"action"` 77 | Event string `json:"event"` 78 | Context string `json:"context"` 79 | Device string `json:"device"` 80 | Payload EvTitleParametersDidChangePayload `json:"payload"` 81 | } 82 | 83 | // EvSendToPlugin is received from the Property Inspector 84 | type EvSendToPlugin struct { 85 | Action string `json:"action"` 86 | Event string `json:"event"` 87 | Context string `json:"context"` 88 | Payload *json.RawMessage `json:"payload"` 89 | } 90 | 91 | type evSendToPropertyInspector struct { 92 | Action string `json:"action"` 93 | Event string `json:"event"` 94 | Context string `json:"context"` 95 | Payload interface{} `json:"payload"` 96 | } 97 | 98 | type evSetTitlePayload struct { 99 | Title string `json:"title"` 100 | Target int `json:"target"` 101 | } 102 | 103 | type evSetTitle struct { 104 | Event string `json:"event"` 105 | Context string `json:"context"` 106 | Payload evSetTitlePayload `json:"payload"` 107 | } 108 | 109 | type evSetSettings struct { 110 | Event string `json:"event"` 111 | Context string `json:"context"` 112 | Payload interface{} `json:"payload"` 113 | } 114 | 115 | type evSetImagePayload struct { 116 | Image string `json:"image"` 117 | Target int `json:"target"` 118 | } 119 | 120 | type evSetImage struct { 121 | Event string `json:"event"` 122 | Context string `json:"context"` 123 | Payload evSetImagePayload `json:"payload"` 124 | } 125 | -------------------------------------------------------------------------------- /start-streamdeck.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | start "" /B "C:\Program Files\Elgato\StreamDeck\StreamDeck.exe" --------------------------------------------------------------------------------