├── .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 | 
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 | 
32 |
33 | 3. Click "Settings"
34 |
35 | 
36 |
37 | 4. Ensure "Shared Memory Support" is checked
38 |
39 | 
40 |
41 | 5. (Optional) Recommended launch settings
42 |
43 | 
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 | > 
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 | 
67 |
68 | 4. Locate "HWiNFO" under "Custom" in the action list
69 |
70 | 
71 |
72 | 5. Drag the "HWiNFO" action from the list to a tile in the canvas area
73 |
74 | 
75 |
76 | 6. Configure the action to display the sensor reading you wish
77 |
78 | 
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 |
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 |
4 |
--------------------------------------------------------------------------------
/com.exension.hwinfo.sdPlugin/css/elg_calendar.svg:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv_13.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/com.exension.hwinfo.sdPlugin/css/pi_required_ok.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/com.exension.hwinfo.sdPlugin/css/rcheck.svg:
--------------------------------------------------------------------------------
1 |
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 |
113 |
114 |
115 | Advanced
116 |
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"
--------------------------------------------------------------------------------