Added support for remote0
This commit is contained in:
54
internal/app/hwinfostreamdeckplugin/action_manager.go
Normal file
54
internal/app/hwinfostreamdeckplugin/action_manager.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package hwinfostreamdeckplugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type actionManager struct {
|
||||
mux sync.RWMutex
|
||||
actions map[string]*actionData
|
||||
}
|
||||
|
||||
func newActionManager() *actionManager {
|
||||
return &actionManager{actions: make(map[string]*actionData)}
|
||||
}
|
||||
|
||||
func (tm *actionManager) Run(updateTiles func(*actionData)) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
for range ticker.C {
|
||||
tm.mux.RLock()
|
||||
for _, data := range tm.actions {
|
||||
if data.settings.IsValid {
|
||||
updateTiles(data)
|
||||
}
|
||||
}
|
||||
tm.mux.RUnlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (tm *actionManager) SetAction(action, context string, settings *actionSettings) {
|
||||
tm.mux.Lock()
|
||||
tm.actions[context] = &actionData{action, context, settings}
|
||||
tm.mux.Unlock()
|
||||
}
|
||||
|
||||
func (tm *actionManager) RemoveAction(context string) {
|
||||
tm.mux.Lock()
|
||||
delete(tm.actions, context)
|
||||
tm.mux.Unlock()
|
||||
}
|
||||
|
||||
func (tm *actionManager) getSettings(context string) (actionSettings, error) {
|
||||
tm.mux.RLock()
|
||||
data, ok := tm.actions[context]
|
||||
tm.mux.RUnlock()
|
||||
if !ok {
|
||||
return actionSettings{}, fmt.Errorf("getSettings invalid key: %s", context)
|
||||
}
|
||||
// return full copy of settings, not reference to stored settings
|
||||
return *data.settings, nil
|
||||
}
|
||||
219
internal/app/hwinfostreamdeckplugin/delegate.go
Normal file
219
internal/app/hwinfostreamdeckplugin/delegate.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package hwinfostreamdeckplugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"image/color"
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/shayne/hwinfo-streamdeck/pkg/graph"
|
||||
"github.com/shayne/hwinfo-streamdeck/pkg/streamdeck"
|
||||
)
|
||||
|
||||
const (
|
||||
tileWidth = 72
|
||||
tileHeight = 72
|
||||
)
|
||||
|
||||
// OnConnected event
|
||||
func (p *Plugin) OnConnected(c *websocket.Conn) {
|
||||
log.Println("OnConnected")
|
||||
}
|
||||
|
||||
// OnWillAppear event
|
||||
func (p *Plugin) OnWillAppear(event *streamdeck.EvWillAppear) {
|
||||
var settings actionSettings
|
||||
err := json.Unmarshal(*event.Payload.Settings, &settings)
|
||||
if err != nil {
|
||||
log.Println("OnWillAppear settings unmarshal", err)
|
||||
}
|
||||
tfSize := 10.5
|
||||
vfSize := 10.5
|
||||
var fgColor *color.RGBA
|
||||
var bgColor *color.RGBA
|
||||
var hlColor *color.RGBA
|
||||
var tColor *color.RGBA
|
||||
var vtColor *color.RGBA
|
||||
if settings.TitleFontSize != 0 {
|
||||
tfSize = settings.TitleFontSize
|
||||
}
|
||||
if settings.ValueFontSize != 0 {
|
||||
vfSize = settings.ValueFontSize
|
||||
}
|
||||
if settings.ForegroundColor == "" {
|
||||
fgColor = &color.RGBA{0, 81, 40, 255}
|
||||
} else {
|
||||
fgColor = hexToRGBA(settings.ForegroundColor)
|
||||
}
|
||||
if settings.BackgroundColor == "" {
|
||||
bgColor = &color.RGBA{0, 0, 0, 255}
|
||||
} else {
|
||||
bgColor = hexToRGBA(settings.BackgroundColor)
|
||||
}
|
||||
if settings.HighlightColor == "" {
|
||||
hlColor = &color.RGBA{0, 158, 0, 255}
|
||||
} else {
|
||||
hlColor = hexToRGBA(settings.HighlightColor)
|
||||
}
|
||||
if settings.TitleColor == "" {
|
||||
tColor = &color.RGBA{183, 183, 183, 255}
|
||||
} else {
|
||||
tColor = hexToRGBA(settings.TitleColor)
|
||||
}
|
||||
if settings.ValueTextColor == "" {
|
||||
vtColor = &color.RGBA{255, 255, 255, 255}
|
||||
} else {
|
||||
vtColor = hexToRGBA(settings.ValueTextColor)
|
||||
}
|
||||
g := graph.NewGraph(tileWidth, tileHeight, settings.Min, settings.Max, fgColor, bgColor, hlColor)
|
||||
g.SetLabel(0, "", 19, tColor)
|
||||
g.SetLabelFontSize(0, tfSize)
|
||||
g.SetLabel(1, "", 44, vtColor)
|
||||
g.SetLabelFontSize(1, vfSize)
|
||||
p.graphs[event.Context] = g
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
}
|
||||
|
||||
// OnWillDisappear event
|
||||
func (p *Plugin) OnWillDisappear(event *streamdeck.EvWillDisappear) {
|
||||
var settings actionSettings
|
||||
err := json.Unmarshal(*event.Payload.Settings, &settings)
|
||||
if err != nil {
|
||||
log.Println("OnWillAppear settings unmarshal", err)
|
||||
}
|
||||
delete(p.graphs, event.Context)
|
||||
p.am.RemoveAction(event.Context)
|
||||
}
|
||||
|
||||
// OnApplicationDidLaunch event
|
||||
func (p *Plugin) OnApplicationDidLaunch(event *streamdeck.EvApplication) {
|
||||
p.appLaunched = true
|
||||
}
|
||||
|
||||
// OnApplicationDidTerminate event
|
||||
func (p *Plugin) OnApplicationDidTerminate(event *streamdeck.EvApplication) {
|
||||
p.appLaunched = false
|
||||
}
|
||||
|
||||
// OnTitleParametersDidChange event
|
||||
func (p *Plugin) OnTitleParametersDidChange(event *streamdeck.EvTitleParametersDidChange) {
|
||||
var settings actionSettings
|
||||
err := json.Unmarshal(*event.Payload.Settings, &settings)
|
||||
if err != nil {
|
||||
log.Println("OnWillAppear settings unmarshal", err)
|
||||
}
|
||||
g, ok := p.graphs[event.Context]
|
||||
if !ok {
|
||||
log.Printf("handleSetMax no graph for context: %s\n", event.Context)
|
||||
return
|
||||
}
|
||||
g.SetLabelText(0, event.Payload.Title)
|
||||
if event.Payload.TitleParameters.TitleColor != "" {
|
||||
tClr := hexToRGBA(event.Payload.TitleParameters.TitleColor)
|
||||
g.SetLabelColor(0, tClr)
|
||||
}
|
||||
|
||||
settings.Title = event.Payload.Title
|
||||
settings.TitleColor = event.Payload.TitleParameters.TitleColor
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
log.Printf("handleSetTitle SetSettings: %v\n", err)
|
||||
return
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
}
|
||||
|
||||
// OnPropertyInspectorConnected event
|
||||
func (p *Plugin) OnPropertyInspectorConnected(event *streamdeck.EvSendToPlugin) {
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
log.Println("OnPropertyInspectorConnected getSettings", err)
|
||||
}
|
||||
sensors, err := p.hw.Sensors()
|
||||
if err != nil {
|
||||
log.Println("OnPropertyInspectorConnected Sensors", err)
|
||||
payload := evStatus{Error: true, Message: "HWiNFO Unavailable"}
|
||||
err := p.sd.SendToPropertyInspector(event.Action, event.Context, payload)
|
||||
settings.InErrorState = true
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
log.Printf("OnPropertyInspectorConnected SetSettings: %v\n", err)
|
||||
return
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
if err != nil {
|
||||
log.Println("updateTiles SendToPropertyInspector", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
evsensors := make([]*evSendSensorsPayloadSensor, 0, len(sensors))
|
||||
for _, s := range sensors {
|
||||
evsensors = append(evsensors, &evSendSensorsPayloadSensor{UID: s.ID(), Name: s.Name()})
|
||||
}
|
||||
payload := evSendSensorsPayload{Sensors: evsensors, Settings: &settings}
|
||||
err = p.sd.SendToPropertyInspector(event.Action, event.Context, payload)
|
||||
if err != nil {
|
||||
log.Println("OnPropertyInspectorConnected SendToPropertyInspector", err)
|
||||
}
|
||||
}
|
||||
|
||||
// OnSendToPlugin event
|
||||
func (p *Plugin) OnSendToPlugin(event *streamdeck.EvSendToPlugin) {
|
||||
var payload map[string]*json.RawMessage
|
||||
err := json.Unmarshal(*event.Payload, &payload)
|
||||
if err != nil {
|
||||
log.Println("OnSendToPlugin unmarshal", err)
|
||||
}
|
||||
if data, ok := payload["sdpi_collection"]; ok {
|
||||
sdpi := evSdpiCollection{}
|
||||
err = json.Unmarshal(*data, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("SDPI unmarshal", err)
|
||||
}
|
||||
switch sdpi.Key {
|
||||
case "sensorSelect":
|
||||
err = p.handleSensorSelect(event, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleSensorSelect", err)
|
||||
}
|
||||
case "readingSelect":
|
||||
err = p.handleReadingSelect(event, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleReadingSelect", err)
|
||||
}
|
||||
case "min":
|
||||
err := p.handleSetMin(event, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleSetMin", err)
|
||||
}
|
||||
case "max":
|
||||
err := p.handleSetMax(event, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleSetMax", err)
|
||||
}
|
||||
case "format":
|
||||
err := p.handleSetFormat(event, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleSetFormat", err)
|
||||
}
|
||||
case "divisor":
|
||||
err := p.handleDivisor(event, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleDivisor", err)
|
||||
}
|
||||
case "foreground", "background", "highlight", "valuetext":
|
||||
err := p.handleColorChange(event, sdpi.Key, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleColorChange", err)
|
||||
}
|
||||
case "titleFontSize", "valueFontSize":
|
||||
err := p.handleSetFontSize(event, sdpi.Key, &sdpi)
|
||||
if err != nil {
|
||||
log.Println("handleSetTitleFontSize", err)
|
||||
}
|
||||
default:
|
||||
log.Printf("Unknown sdpi key: %s\n", sdpi.Key)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
272
internal/app/hwinfostreamdeckplugin/handlers.go
Normal file
272
internal/app/hwinfostreamdeckplugin/handlers.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package hwinfostreamdeckplugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strconv"
|
||||
|
||||
hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service"
|
||||
"github.com/shayne/hwinfo-streamdeck/pkg/streamdeck"
|
||||
)
|
||||
|
||||
func (p *Plugin) handleSensorSelect(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error {
|
||||
sensorid := sdpi.Value
|
||||
readings, err := p.hw.ReadingsForSensorID(sensorid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSensorSelect ReadingsBySensor failed: %v", err)
|
||||
}
|
||||
evreadings := []*evSendReadingsPayloadReading{}
|
||||
for _, r := range readings {
|
||||
evreadings = append(evreadings, &evSendReadingsPayloadReading{ID: r.ID(), Label: r.Label(), Prefix: r.Unit()})
|
||||
}
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleReadingSelect getSettings: %v", err)
|
||||
}
|
||||
// only update settings if SensorUID is changing
|
||||
// this covers case where PI sends event when tile
|
||||
// selected in SD UI
|
||||
if settings.SensorUID != sensorid {
|
||||
settings.SensorUID = sensorid
|
||||
settings.ReadingID = 0
|
||||
settings.IsValid = false
|
||||
}
|
||||
payload := evSendReadingsPayload{Readings: evreadings, Settings: &settings}
|
||||
err = p.sd.SendToPropertyInspector(event.Action, event.Context, payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sensorsSelect SendToPropertyInspector: %v", err)
|
||||
}
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSensorSelect SetSettings: %v", err)
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDefaultMinMaxForReading(r hwsensorsservice.Reading) (int, int) {
|
||||
switch r.Unit() {
|
||||
case "%":
|
||||
return 0, 100
|
||||
case "Yes/No":
|
||||
return 0, 1
|
||||
}
|
||||
min := r.ValueMin()
|
||||
max := r.ValueMax()
|
||||
min -= min * .2
|
||||
if min <= 0 {
|
||||
min = 0.
|
||||
}
|
||||
max += max * .2
|
||||
return int(min), int(max)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleReadingSelect(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error {
|
||||
rid64, err := strconv.ParseInt(sdpi.Value, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleReadingSelect Atoi failed: %s, %v", sdpi.Value, err)
|
||||
}
|
||||
rid := int32(rid64)
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleReadingSelect getSettings: %v", err)
|
||||
}
|
||||
|
||||
// no action if reading didn't change
|
||||
if settings.ReadingID == rid {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings.ReadingID = rid
|
||||
|
||||
// set default min/max
|
||||
r, err := p.getReading(settings.SensorUID, settings.ReadingID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleReadingSelect getReading: %v", err)
|
||||
}
|
||||
|
||||
g, ok := p.graphs[event.Context]
|
||||
if !ok {
|
||||
return fmt.Errorf("handleReadingSelect no graph for context: %s", event.Context)
|
||||
}
|
||||
defaultMin, defaultMax := getDefaultMinMaxForReading(r)
|
||||
settings.Min = defaultMin
|
||||
g.SetMin(settings.Min)
|
||||
settings.Max = defaultMax
|
||||
g.SetMax(settings.Max)
|
||||
settings.IsValid = true // set IsValid once we choose reading
|
||||
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleReadingSelect SetSettings: %v", err)
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleSetMin(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error {
|
||||
min, err := strconv.Atoi(sdpi.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetMin strconv: %v", err)
|
||||
}
|
||||
g, ok := p.graphs[event.Context]
|
||||
if !ok {
|
||||
return fmt.Errorf("handleSetMax no graph for context: %s", event.Context)
|
||||
}
|
||||
g.SetMin(min)
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetMin getSettings: %v", err)
|
||||
}
|
||||
settings.Min = min
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetMin SetSettings: %v", err)
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleSetMax(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error {
|
||||
max, err := strconv.Atoi(sdpi.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetMax strconv: %v", err)
|
||||
}
|
||||
g, ok := p.graphs[event.Context]
|
||||
if !ok {
|
||||
return fmt.Errorf("handleSetMax no graph for context: %s", event.Context)
|
||||
}
|
||||
g.SetMax(max)
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetMax getSettings: %v", err)
|
||||
}
|
||||
settings.Max = max
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetMax SetSettings: %v", err)
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleSetFormat(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error {
|
||||
format := sdpi.Value
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetFormat getSettings: %v", err)
|
||||
}
|
||||
settings.Format = format
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleSetFormat SetSettings: %v", err)
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleDivisor(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error {
|
||||
divisor := sdpi.Value
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleDivisor getSettings: %v", err)
|
||||
}
|
||||
settings.Divisor = divisor
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleDivisor SetSettings: %v", err)
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
hexFormat = "#%02x%02x%02x"
|
||||
hexShortFormat = "#%1x%1x%1x"
|
||||
hexToRGBFactor = 17
|
||||
)
|
||||
|
||||
func hexToRGBA(hex string) *color.RGBA {
|
||||
var r, g, b uint8
|
||||
|
||||
if len(hex) == 4 {
|
||||
fmt.Sscanf(hex, hexShortFormat, &r, &g, &b)
|
||||
r *= hexToRGBFactor
|
||||
g *= hexToRGBFactor
|
||||
b *= hexToRGBFactor
|
||||
} else {
|
||||
fmt.Sscanf(hex, hexFormat, &r, &g, &b)
|
||||
}
|
||||
|
||||
return &color.RGBA{R: r, G: g, B: b, A: 255}
|
||||
}
|
||||
|
||||
func (p *Plugin) handleColorChange(event *streamdeck.EvSendToPlugin, key string, sdpi *evSdpiCollection) error {
|
||||
hex := sdpi.Value
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleDivisor getSettings: %v", err)
|
||||
}
|
||||
g, ok := p.graphs[event.Context]
|
||||
if !ok {
|
||||
return fmt.Errorf("handleSetMax no graph for context: %s", event.Context)
|
||||
}
|
||||
clr := hexToRGBA(hex)
|
||||
switch key {
|
||||
case "foreground":
|
||||
settings.ForegroundColor = hex
|
||||
g.SetForegroundColor(clr)
|
||||
case "background":
|
||||
settings.BackgroundColor = hex
|
||||
g.SetBackgroundColor(clr)
|
||||
case "highlight":
|
||||
settings.HighlightColor = hex
|
||||
g.SetHighlightColor(clr)
|
||||
case "valuetext":
|
||||
settings.ValueTextColor = hex
|
||||
g.SetLabelColor(1, clr)
|
||||
}
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleColorChange SetSettings: %v", err)
|
||||
}
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleSetFontSize(event *streamdeck.EvSendToPlugin, key string, sdpi *evSdpiCollection) error {
|
||||
sv := sdpi.Value
|
||||
size, err := strconv.ParseFloat(sv, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert value to float: %w", err)
|
||||
}
|
||||
|
||||
settings, err := p.am.getSettings(event.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getSettings failed: %w", err)
|
||||
}
|
||||
|
||||
g, ok := p.graphs[event.Context]
|
||||
if !ok {
|
||||
return fmt.Errorf("no graph for context: %s", event.Context)
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "titleFontSize":
|
||||
settings.TitleFontSize = size
|
||||
g.SetLabelFontSize(0, size)
|
||||
case "valueFontSize":
|
||||
settings.ValueFontSize = size
|
||||
g.SetLabelFontSize(1, size)
|
||||
default:
|
||||
return fmt.Errorf("invalid key: %s", sdpi.Key)
|
||||
}
|
||||
|
||||
err = p.sd.SetSettings(event.Context, &settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetSettings failed: %w", err)
|
||||
}
|
||||
|
||||
p.am.SetAction(event.Action, event.Context, &settings)
|
||||
return nil
|
||||
}
|
||||
231
internal/app/hwinfostreamdeckplugin/plugin.go
Normal file
231
internal/app/hwinfostreamdeckplugin/plugin.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package hwinfostreamdeckplugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/shayne/go-winpeg"
|
||||
"github.com/shayne/hwinfo-streamdeck/pkg/graph"
|
||||
hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service"
|
||||
"github.com/shayne/hwinfo-streamdeck/pkg/streamdeck"
|
||||
)
|
||||
|
||||
// Plugin handles information between HWiNFO and Stream Deck
|
||||
type Plugin struct {
|
||||
c *plugin.Client
|
||||
peg winpeg.ProcessExitGroup
|
||||
hw hwsensorsservice.HardwareService
|
||||
sd *streamdeck.StreamDeck
|
||||
am *actionManager
|
||||
graphs map[string]*graph.Graph
|
||||
|
||||
appLaunched bool
|
||||
}
|
||||
|
||||
func (p *Plugin) startClient() error {
|
||||
cmd := exec.Command("./hwinfo-plugin.exe")
|
||||
|
||||
// We're a host. Start by launching the plugin process.
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: hwsensorsservice.Handshake,
|
||||
Plugins: hwsensorsservice.PluginMap,
|
||||
Cmd: cmd,
|
||||
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
|
||||
AutoMTLS: true,
|
||||
})
|
||||
|
||||
// Connect via RPC
|
||||
rpcClient, err := client.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g, err := winpeg.NewProcessExitGroup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := g.AddProcess(cmd.Process); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Request the plugin
|
||||
raw, err := rpcClient.Dispense("hwinfoplugin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.c = client
|
||||
p.peg = g
|
||||
p.hw = raw.(hwsensorsservice.HardwareService)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPlugin creates an instance and initializes the plugin
|
||||
func NewPlugin(port, uuid, event, info string) (*Plugin, error) {
|
||||
// We don't want to see the plugin logs.
|
||||
// log.SetOutput(ioutil.Discard)
|
||||
p := &Plugin{
|
||||
am: newActionManager(),
|
||||
graphs: make(map[string]*graph.Graph),
|
||||
}
|
||||
p.startClient()
|
||||
p.sd = streamdeck.NewStreamDeck(port, uuid, event, info)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// RunForever starts the plugin and waits for events, indefinitely
|
||||
func (p *Plugin) RunForever() error {
|
||||
defer func() {
|
||||
p.c.Kill()
|
||||
p.peg.Dispose()
|
||||
}()
|
||||
|
||||
p.sd.SetDelegate(p)
|
||||
p.am.Run(p.updateTiles)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if p.c.Exited() {
|
||||
p.startClient()
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
err := p.sd.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("StreamDeck Connect: %v", err)
|
||||
}
|
||||
defer p.sd.Close()
|
||||
p.sd.ListenAndWait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) getReading(suid string, rid int32) (hwsensorsservice.Reading, error) {
|
||||
rbs, err := p.hw.ReadingsForSensorID(suid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getReading ReadingsBySensor failed: %v", err)
|
||||
}
|
||||
for _, r := range rbs {
|
||||
if r.ID() == rid {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("ReadingID does not exist: %s", suid)
|
||||
}
|
||||
|
||||
func (p *Plugin) applyDefaultFormat(v float64, t hwsensorsservice.ReadingType, u string) string {
|
||||
switch t {
|
||||
case hwsensorsservice.ReadingTypeNone:
|
||||
return fmt.Sprintf("%0.f %s", v, u)
|
||||
case hwsensorsservice.ReadingTypeTemp:
|
||||
return fmt.Sprintf("%.0f %s", v, u)
|
||||
case hwsensorsservice.ReadingTypeVolt:
|
||||
return fmt.Sprintf("%.0f %s", v, u)
|
||||
case hwsensorsservice.ReadingTypeFan:
|
||||
return fmt.Sprintf("%.0f %s", v, u)
|
||||
case hwsensorsservice.ReadingTypeCurrent:
|
||||
return fmt.Sprintf("%.0f %s", v, u)
|
||||
case hwsensorsservice.ReadingTypePower:
|
||||
return fmt.Sprintf("%0.f %s", v, u)
|
||||
case hwsensorsservice.ReadingTypeClock:
|
||||
return fmt.Sprintf("%.0f %s", v, u)
|
||||
case hwsensorsservice.ReadingTypeUsage:
|
||||
return fmt.Sprintf("%.0f%s", v, u)
|
||||
case hwsensorsservice.ReadingTypeOther:
|
||||
return fmt.Sprintf("%.0f %s", v, u)
|
||||
}
|
||||
return "Bad Format"
|
||||
}
|
||||
|
||||
func (p *Plugin) updateTiles(data *actionData) {
|
||||
if data.action != "com.exension.hwinfor0.reading" {
|
||||
log.Printf("Unknown action updateTiles: %s\n", data.action)
|
||||
return
|
||||
}
|
||||
|
||||
g, ok := p.graphs[data.context]
|
||||
if !ok {
|
||||
log.Printf("Graph not found for context: %s\n", data.context)
|
||||
return
|
||||
}
|
||||
|
||||
if !p.appLaunched {
|
||||
if !data.settings.InErrorState {
|
||||
payload := evStatus{Error: true, Message: "HWiNFO Unavailable"}
|
||||
err := p.sd.SendToPropertyInspector("com.exension.hwinfor0.reading", data.context, payload)
|
||||
if err != nil {
|
||||
log.Println("updateTiles SendToPropertyInspector", err)
|
||||
}
|
||||
data.settings.InErrorState = true
|
||||
p.sd.SetSettings(data.context, &data.settings)
|
||||
}
|
||||
bts, err := ioutil.ReadFile("./launch-hwinfo.png")
|
||||
if err != nil {
|
||||
log.Printf("Failed to read launch-hwinfo.png: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = p.sd.SetImage(data.context, bts)
|
||||
if err != nil {
|
||||
log.Printf("Failed to setImage: %v\n", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// show ui on property inspector if in error state
|
||||
if data.settings.InErrorState {
|
||||
payload := evStatus{Error: false, Message: "show_ui"}
|
||||
err := p.sd.SendToPropertyInspector("com.exension.hwinfor0.reading", data.context, payload)
|
||||
if err != nil {
|
||||
log.Println("updateTiles SendToPropertyInspector", err)
|
||||
}
|
||||
data.settings.InErrorState = false
|
||||
p.sd.SetSettings(data.context, &data.settings)
|
||||
}
|
||||
|
||||
s := data.settings
|
||||
r, err := p.getReading(s.SensorUID, s.ReadingID)
|
||||
if err != nil {
|
||||
log.Printf("getReading failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
v := r.Value()
|
||||
if s.Divisor != "" {
|
||||
fdiv := 1.
|
||||
fdiv, err := strconv.ParseFloat(s.Divisor, 64)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse float: %s\n", s.Divisor)
|
||||
return
|
||||
}
|
||||
v = r.Value() / fdiv
|
||||
}
|
||||
g.Update(v)
|
||||
var text string
|
||||
if f := s.Format; f != "" {
|
||||
text = fmt.Sprintf(f, v)
|
||||
} else {
|
||||
text = p.applyDefaultFormat(v, hwsensorsservice.ReadingType(r.TypeI()), r.Unit())
|
||||
}
|
||||
g.SetLabelText(1, text)
|
||||
|
||||
b, err := g.EncodePNG()
|
||||
if err != nil {
|
||||
log.Printf("Failed to encode graph: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = p.sd.SetImage(data.context, b)
|
||||
if err != nil {
|
||||
log.Printf("Failed to setImage: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
60
internal/app/hwinfostreamdeckplugin/types.go
Normal file
60
internal/app/hwinfostreamdeckplugin/types.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package hwinfostreamdeckplugin
|
||||
|
||||
type actionSettings struct {
|
||||
SensorUID string `json:"sensorUid"`
|
||||
ReadingID int32 `json:"readingId,string"`
|
||||
Title string `json:"title"`
|
||||
TitleFontSize float64 `json:"titleFontSize"`
|
||||
ValueFontSize float64 `json:"valueFontSize"`
|
||||
Min int `json:"min"`
|
||||
Max int `json:"max"`
|
||||
Format string `json:"format"`
|
||||
Divisor string `json:"divisor"`
|
||||
IsValid bool `json:"isValid"`
|
||||
TitleColor string `json:"titleColor"`
|
||||
ForegroundColor string `json:"foregroundColor"`
|
||||
BackgroundColor string `json:"backgroundColor"`
|
||||
HighlightColor string `json:"highlightColor"`
|
||||
ValueTextColor string `json:"valueTextColor"`
|
||||
InErrorState bool `json:"inErrorState"`
|
||||
}
|
||||
|
||||
type actionData struct {
|
||||
action string
|
||||
context string
|
||||
settings *actionSettings
|
||||
}
|
||||
|
||||
type evStatus struct {
|
||||
Error bool `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type evSendSensorsPayloadSensor struct {
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type evSendSensorsPayload struct {
|
||||
Sensors []*evSendSensorsPayloadSensor `json:"sensors"`
|
||||
Settings *actionSettings `json:"settings"`
|
||||
}
|
||||
|
||||
type evSendReadingsPayloadReading struct {
|
||||
ID int32 `json:"id,string"`
|
||||
Label string `json:"label"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
type evSendReadingsPayload struct {
|
||||
Readings []*evSendReadingsPayloadReading `json:"readings"`
|
||||
Settings *actionSettings `json:"settings"`
|
||||
}
|
||||
|
||||
type evSdpiCollection struct {
|
||||
Group bool `json:"group"`
|
||||
Index int `json:"index"`
|
||||
Key string `json:"key"`
|
||||
Selection []string `json:"selection"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
163
internal/hwinfo/hwinfo.go
Normal file
163
internal/hwinfo/hwinfo.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package hwinfo
|
||||
|
||||
/*
|
||||
#include <windows.h>
|
||||
#include "hwisenssm2.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo/shmem"
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util"
|
||||
)
|
||||
|
||||
// SharedMemory provides access to the HWiNFO shared memory
|
||||
type SharedMemory struct {
|
||||
data []byte
|
||||
shmem C.PHWiNFO_SENSORS_SHARED_MEM2
|
||||
}
|
||||
|
||||
// ReadSharedMem reads data from HWiNFO shared memory
|
||||
// creating a copy of the data
|
||||
func ReadSharedMem() (*SharedMemory, error) {
|
||||
data, err := shmem.ReadBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SharedMemory{
|
||||
data: append([]byte(nil), data...),
|
||||
shmem: C.PHWiNFO_SENSORS_SHARED_MEM2(unsafe.Pointer(&data[0])),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Result for streamed shared memory updates
|
||||
type Result struct {
|
||||
Shmem *SharedMemory
|
||||
Err error
|
||||
}
|
||||
|
||||
func readAndSend(ch chan<- Result) {
|
||||
shmem, err := ReadSharedMem()
|
||||
ch <- Result{Shmem: shmem, Err: err}
|
||||
}
|
||||
|
||||
// StreamSharedMem delivers shared memory hardware sensors updates
|
||||
// over a channel
|
||||
func StreamSharedMem() <-chan Result {
|
||||
ch := make(chan Result)
|
||||
go func() {
|
||||
readAndSend(ch)
|
||||
// TODO: don't use time.Tick, cancellable?
|
||||
for range time.Tick(1 * time.Second) {
|
||||
readAndSend(ch)
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Signature "HWiS" if active, 'DEAD' when inactive
|
||||
func (s *SharedMemory) Signature() string {
|
||||
return util.DecodeCharPtr(unsafe.Pointer(&s.shmem.dwSignature), C.sizeof_DWORD)
|
||||
}
|
||||
|
||||
// Version v1 is latest
|
||||
func (s *SharedMemory) Version() int {
|
||||
return int(s.shmem.dwVersion)
|
||||
}
|
||||
|
||||
// Revision revision of version
|
||||
func (s *SharedMemory) Revision() int {
|
||||
return int(s.shmem.dwRevision)
|
||||
}
|
||||
|
||||
// PollTime last polling time
|
||||
func (s *SharedMemory) PollTime() uint64 {
|
||||
addr := unsafe.Pointer(uintptr(unsafe.Pointer(&s.shmem.dwRevision)) + C.sizeof_DWORD)
|
||||
return uint64(*(*C.__time64_t)(addr))
|
||||
}
|
||||
|
||||
// OffsetOfSensorSection offset of the Sensor section from beginning of HWiNFO_SENSORS_SHARED_MEM2
|
||||
func (s *SharedMemory) OffsetOfSensorSection() int {
|
||||
return int(s.shmem.dwOffsetOfSensorSection)
|
||||
}
|
||||
|
||||
// SizeOfSensorElement size of each sensor element = sizeof( HWiNFO_SENSORS_SENSOR_ELEMENT )
|
||||
func (s *SharedMemory) SizeOfSensorElement() int {
|
||||
return int(s.shmem.dwSizeOfSensorElement)
|
||||
}
|
||||
|
||||
// NumSensorElements number of sensor elements
|
||||
func (s *SharedMemory) NumSensorElements() int {
|
||||
return int(s.shmem.dwNumSensorElements)
|
||||
}
|
||||
|
||||
// OffsetOfReadingSection offset of the Reading section from beginning of HWiNFO_SENSORS_SHARED_MEM2
|
||||
func (s *SharedMemory) OffsetOfReadingSection() int {
|
||||
return int(s.shmem.dwOffsetOfReadingSection)
|
||||
}
|
||||
|
||||
// SizeOfReadingElement size of each Reading element = sizeof( HWiNFO_SENSORS_READING_ELEMENT )
|
||||
func (s *SharedMemory) SizeOfReadingElement() int {
|
||||
return int(s.shmem.dwSizeOfReadingElement)
|
||||
}
|
||||
|
||||
// NumReadingElements number of Reading elements
|
||||
func (s *SharedMemory) NumReadingElements() int {
|
||||
return int(s.shmem.dwNumReadingElements)
|
||||
}
|
||||
|
||||
func (s *SharedMemory) dataForSensor(pos int) ([]byte, error) {
|
||||
if pos >= s.NumSensorElements() {
|
||||
return nil, fmt.Errorf("dataForSensor pos out of range, %d for size %d", pos, s.NumSensorElements())
|
||||
}
|
||||
start := s.OffsetOfSensorSection() + (pos * s.SizeOfSensorElement())
|
||||
end := start + s.SizeOfSensorElement()
|
||||
return s.data[start:end], nil
|
||||
}
|
||||
|
||||
// IterSensors iterate over each sensor
|
||||
func (s *SharedMemory) IterSensors() <-chan Sensor {
|
||||
ch := make(chan Sensor)
|
||||
go func() {
|
||||
for i := 0; i < s.NumSensorElements(); i++ {
|
||||
data, err := s.dataForSensor(i)
|
||||
if err != nil {
|
||||
log.Fatalf("TODO: failed to read dataForSensor: %v", err)
|
||||
}
|
||||
ch <- NewSensor(data)
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s *SharedMemory) dataForReading(pos int) ([]byte, error) {
|
||||
if pos >= s.NumReadingElements() {
|
||||
return nil, fmt.Errorf("dataForReading pos out of range, %d for size %d", pos, s.NumSensorElements())
|
||||
}
|
||||
start := s.OffsetOfReadingSection() + (pos * s.SizeOfReadingElement())
|
||||
end := start + s.SizeOfReadingElement()
|
||||
return s.data[start:end], nil
|
||||
}
|
||||
|
||||
// IterReadings iterate over each sensor
|
||||
func (s *SharedMemory) IterReadings() <-chan Reading {
|
||||
ch := make(chan Reading)
|
||||
go func() {
|
||||
for i := 0; i < s.NumReadingElements(); i++ {
|
||||
data, err := s.dataForReading(i)
|
||||
if err != nil {
|
||||
log.Fatalf("TODO: failed to read dataForReading: %v", err)
|
||||
}
|
||||
ch <- NewReading(data)
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
126
internal/hwinfo/hwisenssm2.h
Normal file
126
internal/hwinfo/hwisenssm2.h
Normal file
@@ -0,0 +1,126 @@
|
||||
#ifndef _HWISENSSM2_H_INCLUDED_
|
||||
#define _HWISENSSM2_H_INCLUDED_
|
||||
|
||||
// Name of the file mapping object that needs to be opened using OpenFileMapping Function:
|
||||
#define HWiNFO_SENSORS_MAP_FILE_NAME2 "Global\\HWiNFO_SENS_SM2"
|
||||
|
||||
// Name of the global mutex which is acquired when accessing the Shared Memory space. Release as quick as possible !
|
||||
#define HWiNFO_SENSORS_SM2_MUTEX "Global\\HWiNFO_SM2_MUTEX"
|
||||
|
||||
#define HWiNFO_SENSORS_STRING_LEN2 128
|
||||
#define HWiNFO_UNIT_STRING_LEN 16
|
||||
|
||||
enum SENSOR_READING_TYPE
|
||||
{
|
||||
SENSOR_TYPE_NONE = 0,
|
||||
SENSOR_TYPE_TEMP,
|
||||
SENSOR_TYPE_VOLT,
|
||||
SENSOR_TYPE_FAN,
|
||||
SENSOR_TYPE_CURRENT,
|
||||
SENSOR_TYPE_POWER,
|
||||
SENSOR_TYPE_CLOCK,
|
||||
SENSOR_TYPE_USAGE,
|
||||
SENSOR_TYPE_OTHER
|
||||
};
|
||||
typedef enum SENSOR_READING_TYPE SENSOR_READING_TYPE;
|
||||
|
||||
// No alignment of structure members
|
||||
#pragma pack(1)
|
||||
|
||||
typedef struct _HWiNFO_SENSORS_READING_ELEMENT
|
||||
{
|
||||
|
||||
SENSOR_READING_TYPE tReading; // Type of sensor reading
|
||||
DWORD dwSensorIndex; // This is the index of sensor in the Sensors[] array to which this reading belongs to
|
||||
DWORD dwReadingID; // A unique ID of the reading within a particular sensor
|
||||
char szLabelOrig[HWiNFO_SENSORS_STRING_LEN2]; // Original label (e.g. "Chassis2 Fan")
|
||||
char szLabelUser[HWiNFO_SENSORS_STRING_LEN2]; // Label displayed, which might have been renamed by user
|
||||
char szUnit[HWiNFO_UNIT_STRING_LEN]; // e.g. "RPM"
|
||||
double Value;
|
||||
double ValueMin;
|
||||
double ValueMax;
|
||||
double ValueAvg;
|
||||
|
||||
} HWiNFO_SENSORS_READING_ELEMENT, *PHWiNFO_SENSORS_READING_ELEMENT;
|
||||
|
||||
typedef struct _HWiNFO_SENSORS_SENSOR_ELEMENT
|
||||
{
|
||||
|
||||
DWORD dwSensorID; // A unique Sensor ID
|
||||
DWORD dwSensorInst; // The instance of the sensor (together with dwSensorID forms a unique ID)
|
||||
char szSensorNameOrig[HWiNFO_SENSORS_STRING_LEN2]; // Original sensor name
|
||||
char szSensorNameUser[HWiNFO_SENSORS_STRING_LEN2]; // Sensor name displayed, which might have been renamed by user
|
||||
|
||||
} HWiNFO_SENSORS_SENSOR_ELEMENT, *PHWiNFO_SENSORS_SENSOR_ELEMENT;
|
||||
|
||||
typedef struct _HWiNFO_SENSORS_SHARED_MEM2
|
||||
{
|
||||
|
||||
DWORD dwSignature; // "HWiS" if active, 'DEAD' when inactive
|
||||
DWORD dwVersion; // v1 is latest
|
||||
DWORD dwRevision; //
|
||||
__time64_t poll_time; // last polling time
|
||||
|
||||
// descriptors for the Sensors section
|
||||
DWORD dwOffsetOfSensorSection; // Offset of the Sensor section from beginning of HWiNFO_SENSORS_SHARED_MEM2
|
||||
DWORD dwSizeOfSensorElement; // Size of each sensor element = sizeof( HWiNFO_SENSORS_SENSOR_ELEMENT )
|
||||
DWORD dwNumSensorElements; // Number of sensor elements
|
||||
|
||||
// descriptors for the Readings section
|
||||
DWORD dwOffsetOfReadingSection; // Offset of the Reading section from beginning of HWiNFO_SENSORS_SHARED_MEM2
|
||||
DWORD dwSizeOfReadingElement; // Size of each Reading element = sizeof( HWiNFO_SENSORS_READING_ELEMENT )
|
||||
DWORD dwNumReadingElements; // Number of Reading elements
|
||||
|
||||
} HWiNFO_SENSORS_SHARED_MEM2, *PHWiNFO_SENSORS_SHARED_MEM2;
|
||||
|
||||
#pragma pack()
|
||||
|
||||
#endif
|
||||
|
||||
// ***************************************************************************************************************
|
||||
// HWiNFO Shared Memory Footprint
|
||||
// ***************************************************************************************************************
|
||||
//
|
||||
// |-----------------------------|-----------------------------------|-----------------------------------|
|
||||
// Content | HWiNFO_SENSORS_SHARED_MEM2 | HWiNFO_SENSORS_SENSOR_ELEMENT[] | HWiNFO_SENSORS_READING_ELEMENT[] |
|
||||
// |-----------------------------|-----------------------------------|-----------------------------------|
|
||||
// Pointer |<--0 |<--dwOffsetOfSensorSection |<--dwOffsetOfReadingSection |
|
||||
// |-----------------------------|-----------------------------------|-----------------------------------|
|
||||
// Size | dwOffsetOfSensorSection | dwSizeOfSensorElement | dwSizeOfReadingElement |
|
||||
// | | * dwNumSensorElement | * dwNumReadingElement |
|
||||
// |-----------------------------|-----------------------------------|-----------------------------------|
|
||||
//
|
||||
// ***************************************************************************************************************
|
||||
// Code Example
|
||||
// ***************************************************************************************************************
|
||||
/*
|
||||
|
||||
HANDLE hHWiNFOMemory = OpenFileMapping( FILE_MAP_READ, FALSE, HWiNFO_SENSORS_MAP_FILE_NAME2 );
|
||||
if (hHWiNFOMemory)
|
||||
PHWiNFO_SENSORS_SHARED_MEM2 pHWiNFOMemory =
|
||||
(PHWiNFO_SENSORS_SHARED_MEM2) MapViewOfFile( hHWiNFOMemory, FILE_MAP_READ, 0, 0, 0 );
|
||||
|
||||
// TODO: process signature, version, revision and poll time
|
||||
|
||||
// loop through all available sensors
|
||||
for (DWORD dwSensor = 0; dwSensor < pHWiNFOMemory->dwNumSensorElements; dwSensor++)
|
||||
{
|
||||
PHWiNFO_SENSORS_SENSOR_ELEMENT sensor = (PHWiNFO_SENSORS_SENSOR_ELEMENT) ((BYTE*)pHWiNFOMemory +
|
||||
pHWiNFOMemory->dwOffsetOfSensorSection +
|
||||
(pHWiNFOMemory->dwSizeOfSensorElement * dwSensor));
|
||||
|
||||
// TODO: process sensor
|
||||
}
|
||||
|
||||
// loop through all available readings
|
||||
for (DWORD dwReading = 0; dwReading < pHWiNFOMemory->dwNumReadingElements; dwReading++)
|
||||
{
|
||||
PHWiNFO_SENSORS_READING_ELEMENT reading = (PHWiNFO_SENSORS_READING_ELEMENT) ((BYTE*)pHWiNFOMemory +
|
||||
pHWiNFOMemory->dwOffsetOfReadingSection +
|
||||
(pHWiNFOMemory->dwSizeOfReadingElement * dwReading));
|
||||
|
||||
// TODO: process reading
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
38
internal/hwinfo/mutex/mutex.go
Normal file
38
internal/hwinfo/mutex/mutex.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package mutex
|
||||
|
||||
/*
|
||||
#include <windows.h>
|
||||
#include "../hwisenssm2.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util"
|
||||
)
|
||||
|
||||
var ghnd C.HANDLE
|
||||
var imut = sync.Mutex{}
|
||||
|
||||
// Lock the global mutex
|
||||
func Lock() error {
|
||||
imut.Lock()
|
||||
lpName := C.CString(C.HWiNFO_SENSORS_SM2_MUTEX)
|
||||
defer C.free(unsafe.Pointer(lpName))
|
||||
|
||||
ghnd = C.OpenMutex(C.READ_CONTROL, C.FALSE, lpName)
|
||||
if ghnd == C.HANDLE(C.NULL) {
|
||||
errstr := util.HandleLastError(uint64(C.GetLastError()))
|
||||
return fmt.Errorf("failed to lock global mutex: %w", errstr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock the global mutex
|
||||
func Unlock() {
|
||||
defer imut.Unlock()
|
||||
C.CloseHandle(ghnd)
|
||||
}
|
||||
70
internal/hwinfo/plugin/plugin.go
Normal file
70
internal/hwinfo/plugin/plugin.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo"
|
||||
hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service"
|
||||
)
|
||||
|
||||
// Plugin implementation
|
||||
type Plugin struct {
|
||||
Service *Service
|
||||
}
|
||||
|
||||
// PollTime implementation for plugin
|
||||
func (p *Plugin) PollTime() (uint64, error) {
|
||||
shmem, err := p.Service.Shmem()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return shmem.PollTime(), nil
|
||||
}
|
||||
|
||||
// Sensors implementation for plugin
|
||||
func (p *Plugin) Sensors() ([]hwsensorsservice.Sensor, error) {
|
||||
shmem, err := p.Service.Shmem()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sensors []hwsensorsservice.Sensor
|
||||
for s := range shmem.IterSensors() {
|
||||
sensors = append(sensors, &sensor{s})
|
||||
}
|
||||
return sensors, nil
|
||||
}
|
||||
|
||||
// ReadingsForSensorID implementation for plugin
|
||||
func (p *Plugin) ReadingsForSensorID(id string) ([]hwsensorsservice.Reading, error) {
|
||||
res, err := p.Service.ReadingsBySensorID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var readings []hwsensorsservice.Reading
|
||||
for _, r := range res {
|
||||
readings = append(readings, &reading{r})
|
||||
}
|
||||
return readings, nil
|
||||
}
|
||||
|
||||
type sensor struct {
|
||||
hwinfo.Sensor
|
||||
}
|
||||
|
||||
func (s sensor) Name() string {
|
||||
return s.NameOrig()
|
||||
}
|
||||
|
||||
type reading struct {
|
||||
hwinfo.Reading
|
||||
}
|
||||
|
||||
func (r reading) Label() string {
|
||||
return r.LabelOrig()
|
||||
}
|
||||
|
||||
func (r reading) Type() string {
|
||||
return r.Reading.Type().String()
|
||||
}
|
||||
|
||||
func (r reading) TypeI() int32 {
|
||||
return int32(r.Reading.Type())
|
||||
}
|
||||
128
internal/hwinfo/plugin/service.go
Normal file
128
internal/hwinfo/plugin/service.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo"
|
||||
)
|
||||
|
||||
// Service wraps hwinfo shared mem streaming
|
||||
// and provides convenient methods for data access
|
||||
type Service struct {
|
||||
streamch <-chan hwinfo.Result
|
||||
mu sync.RWMutex
|
||||
sensorIDByIdx []string
|
||||
readingsBySensorID map[string][]hwinfo.Reading
|
||||
shmem *hwinfo.SharedMemory
|
||||
readingsBuilt bool
|
||||
}
|
||||
|
||||
// Start starts the service providing updating hardware info
|
||||
func StartService() *Service {
|
||||
return &Service{
|
||||
streamch: hwinfo.StreamSharedMem(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) recvShmem(shmem *hwinfo.SharedMemory) error {
|
||||
if shmem == nil {
|
||||
return fmt.Errorf("shmem nil")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.shmem = shmem
|
||||
|
||||
s.sensorIDByIdx = s.sensorIDByIdx[:0]
|
||||
for k, v := range s.readingsBySensorID {
|
||||
s.readingsBySensorID[k] = v[:0]
|
||||
}
|
||||
s.readingsBuilt = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recv receives new hardware sensor updates
|
||||
func (s *Service) Recv() error {
|
||||
select {
|
||||
case r := <-s.streamch:
|
||||
if r.Err != nil {
|
||||
return r.Err
|
||||
}
|
||||
return s.recvShmem(r.Shmem)
|
||||
}
|
||||
}
|
||||
|
||||
// Shmem provides access to underlying hwinfo shared memory
|
||||
func (s *Service) Shmem() (*hwinfo.SharedMemory, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.shmem != nil {
|
||||
return s.shmem, nil
|
||||
}
|
||||
return nil, fmt.Errorf("shmem nil")
|
||||
}
|
||||
|
||||
// SensorIDByIdx returns ordered slice of sensor IDs
|
||||
func (s *Service) SensorIDByIdx() ([]string, error) {
|
||||
s.mu.RLock()
|
||||
if len(s.sensorIDByIdx) > 0 {
|
||||
defer s.mu.RUnlock()
|
||||
return s.sensorIDByIdx, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for sens := range s.shmem.IterSensors() {
|
||||
s.sensorIDByIdx = append(s.sensorIDByIdx, sens.ID())
|
||||
}
|
||||
|
||||
return s.sensorIDByIdx, nil
|
||||
}
|
||||
|
||||
// ReadingsBySensorID returns slice of hwinfoReading for a given sensor ID
|
||||
func (s *Service) ReadingsBySensorID(id string) ([]hwinfo.Reading, error) {
|
||||
s.mu.RLock()
|
||||
if s.readingsBySensorID != nil && s.readingsBuilt {
|
||||
defer s.mu.RUnlock()
|
||||
readings, ok := s.readingsBySensorID[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("readings for sensor id %s do not exist", id)
|
||||
}
|
||||
return readings, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
sids, err := s.SensorIDByIdx()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.readingsBySensorID == nil {
|
||||
s.readingsBySensorID = make(map[string][]hwinfo.Reading)
|
||||
}
|
||||
|
||||
for r := range s.shmem.IterReadings() {
|
||||
sidx := int(r.SensorIndex())
|
||||
if sidx < len(sids) {
|
||||
sid := sids[sidx]
|
||||
s.readingsBySensorID[sid] = append(s.readingsBySensorID[sid], r)
|
||||
} else {
|
||||
return nil, fmt.Errorf("sensor at index %d out of range ", sidx)
|
||||
}
|
||||
}
|
||||
s.readingsBuilt = true
|
||||
|
||||
readings, ok := s.readingsBySensorID[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("readings for sensor id %s do not exist", id)
|
||||
}
|
||||
return readings, nil
|
||||
}
|
||||
124
internal/hwinfo/reading.go
Normal file
124
internal/hwinfo/reading.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package hwinfo
|
||||
|
||||
/*
|
||||
#include <windows.h>
|
||||
#include "hwisenssm2.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util"
|
||||
)
|
||||
|
||||
// ReadingType enum of value/unit type for reading
|
||||
type ReadingType int
|
||||
|
||||
const (
|
||||
// ReadingTypeNone no type
|
||||
ReadingTypeNone ReadingType = iota
|
||||
// ReadingTypeTemp temperature in celsius
|
||||
ReadingTypeTemp
|
||||
// ReadingTypeVolt voltage
|
||||
ReadingTypeVolt
|
||||
// ReadingTypeFan RPM
|
||||
ReadingTypeFan
|
||||
// ReadingTypeCurrent amps
|
||||
ReadingTypeCurrent
|
||||
// ReadingTypePower watts
|
||||
ReadingTypePower
|
||||
// ReadingTypeClock Mhz
|
||||
ReadingTypeClock
|
||||
// ReadingTypeUsage e.g. MBs
|
||||
ReadingTypeUsage
|
||||
// ReadingTypeOther other
|
||||
ReadingTypeOther
|
||||
)
|
||||
|
||||
func (t ReadingType) String() string {
|
||||
return [...]string{"None", "Temp", "Volt", "Fan", "Current", "Power", "Clock", "Usage", "Other"}[t]
|
||||
}
|
||||
|
||||
// Reading element (e.g. usage, power, mhz...)
|
||||
type Reading struct {
|
||||
cr C.PHWiNFO_SENSORS_READING_ELEMENT
|
||||
}
|
||||
|
||||
// NewReading contructs a Reading
|
||||
func NewReading(data []byte) Reading {
|
||||
return Reading{
|
||||
cr: C.PHWiNFO_SENSORS_READING_ELEMENT(unsafe.Pointer(&data[0])),
|
||||
}
|
||||
}
|
||||
|
||||
// ID unique ID of the reading within a particular sensor
|
||||
func (r *Reading) ID() int32 {
|
||||
return int32(r.cr.dwReadingID)
|
||||
}
|
||||
|
||||
// Type of sensor reading
|
||||
func (r *Reading) Type() ReadingType {
|
||||
return ReadingType(r.cr.tReading)
|
||||
}
|
||||
|
||||
// SensorIndex this is the index of sensor in the Sensors[] array to
|
||||
// which this reading belongs to
|
||||
func (r *Reading) SensorIndex() uint64 {
|
||||
return uint64(r.cr.dwSensorIndex)
|
||||
}
|
||||
|
||||
// ReadingID a unique ID of the reading within a particular sensor
|
||||
func (r *Reading) ReadingID() uint64 {
|
||||
return uint64(r.cr.dwReadingID)
|
||||
}
|
||||
|
||||
// LabelOrig original label (e.g. "Chassis2 Fan")
|
||||
func (r *Reading) LabelOrig() string {
|
||||
return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szLabelOrig), C.HWiNFO_SENSORS_STRING_LEN2)
|
||||
}
|
||||
|
||||
// LabelUser label displayed, which might have been renamed by user
|
||||
func (r *Reading) LabelUser() string {
|
||||
return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szLabelUser), C.HWiNFO_SENSORS_STRING_LEN2)
|
||||
}
|
||||
|
||||
// Unit e.g. "RPM"
|
||||
func (r *Reading) Unit() string {
|
||||
return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szUnit), C.HWiNFO_UNIT_STRING_LEN)
|
||||
}
|
||||
|
||||
func (r *Reading) valuePtr() unsafe.Pointer {
|
||||
return unsafe.Pointer(uintptr(unsafe.Pointer(&r.cr.szUnit)) + C.HWiNFO_UNIT_STRING_LEN)
|
||||
}
|
||||
|
||||
// Value current value
|
||||
func (r *Reading) Value() float64 {
|
||||
return float64(*(*C.double)(r.valuePtr()))
|
||||
}
|
||||
|
||||
func (r *Reading) valueMinPtr() unsafe.Pointer {
|
||||
return unsafe.Pointer(uintptr(r.valuePtr()) + C.sizeof_double)
|
||||
}
|
||||
|
||||
// ValueMin current value
|
||||
func (r *Reading) ValueMin() float64 {
|
||||
return float64(*(*C.double)(r.valueMinPtr()))
|
||||
}
|
||||
|
||||
func (r *Reading) valueMaxPtr() unsafe.Pointer {
|
||||
return unsafe.Pointer(uintptr(r.valueMinPtr()) + C.sizeof_double)
|
||||
}
|
||||
|
||||
// ValueMax current value
|
||||
func (r *Reading) ValueMax() float64 {
|
||||
return float64(*(*C.double)(r.valueMaxPtr()))
|
||||
}
|
||||
|
||||
func (r *Reading) valueAvgPtr() unsafe.Pointer {
|
||||
return unsafe.Pointer(uintptr(r.valueMaxPtr()) + C.sizeof_double)
|
||||
}
|
||||
|
||||
// ValueAvg current value
|
||||
func (r *Reading) ValueAvg() float64 {
|
||||
return float64(*(*C.double)(r.valueAvgPtr()))
|
||||
}
|
||||
52
internal/hwinfo/sensor.go
Normal file
52
internal/hwinfo/sensor.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package hwinfo
|
||||
|
||||
/*
|
||||
#include <windows.h>
|
||||
#include "hwisenssm2.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util"
|
||||
)
|
||||
|
||||
// Sensor element (e.g. motherboard, cpu, gpu...)
|
||||
type Sensor struct {
|
||||
cs C.PHWiNFO_SENSORS_SENSOR_ELEMENT
|
||||
}
|
||||
|
||||
// NewSensor constructs a Sensor
|
||||
func NewSensor(data []byte) Sensor {
|
||||
return Sensor{
|
||||
cs: C.PHWiNFO_SENSORS_SENSOR_ELEMENT(unsafe.Pointer(&data[0])),
|
||||
}
|
||||
}
|
||||
|
||||
// SensorID a unique Sensor ID
|
||||
func (s *Sensor) SensorID() uint64 {
|
||||
return uint64(s.cs.dwSensorID)
|
||||
}
|
||||
|
||||
// SensorInst the instance of the sensor (together with SensorID forms a unique ID)
|
||||
func (s *Sensor) SensorInst() uint64 {
|
||||
return uint64(s.cs.dwSensorInst)
|
||||
}
|
||||
|
||||
// ID a unique ID combining SensorID and SensorInst
|
||||
func (s *Sensor) ID() string {
|
||||
// keeping old method used in legacy steam deck plugin
|
||||
return strconv.FormatUint(s.SensorID()*100+s.SensorInst(), 10)
|
||||
}
|
||||
|
||||
// NameOrig original name of sensor
|
||||
func (s *Sensor) NameOrig() string {
|
||||
return util.DecodeCharPtr(unsafe.Pointer(&s.cs.szSensorNameOrig), C.HWiNFO_SENSORS_STRING_LEN2)
|
||||
}
|
||||
|
||||
// NameUser sensor name displayed, which might have been renamed by user
|
||||
func (s *Sensor) NameUser() string {
|
||||
return util.DecodeCharPtr(unsafe.Pointer(&s.cs.szSensorNameUser), C.HWiNFO_SENSORS_STRING_LEN2)
|
||||
}
|
||||
95
internal/hwinfo/shmem/shmem.go
Normal file
95
internal/hwinfo/shmem/shmem.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package shmem
|
||||
|
||||
/*
|
||||
#include <windows.h>
|
||||
#include "../hwisenssm2.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo/mutex"
|
||||
"github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var buf = make([]byte, 200000)
|
||||
|
||||
func copyBytes(addr uintptr) []byte {
|
||||
headerLen := C.sizeof_HWiNFO_SENSORS_SHARED_MEM2
|
||||
|
||||
var d []byte
|
||||
dh := (*reflect.SliceHeader)(unsafe.Pointer(&d))
|
||||
|
||||
dh.Data = addr
|
||||
dh.Len, dh.Cap = headerLen, headerLen
|
||||
|
||||
cheader := C.PHWiNFO_SENSORS_SHARED_MEM2(unsafe.Pointer(&d[0]))
|
||||
fullLen := int(cheader.dwOffsetOfReadingSection + (cheader.dwSizeOfReadingElement * cheader.dwNumReadingElements))
|
||||
|
||||
if fullLen > cap(buf) {
|
||||
buf = append(buf, make([]byte, fullLen-cap(buf))...)
|
||||
}
|
||||
|
||||
dh.Len, dh.Cap = fullLen, fullLen
|
||||
|
||||
copy(buf, d)
|
||||
|
||||
return buf[:fullLen]
|
||||
}
|
||||
|
||||
// ReadBytes copies bytes from global shared memory
|
||||
func ReadBytes() ([]byte, error) {
|
||||
err := mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hnd, err := openFileMapping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr, err := mapViewOfFile(hnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unmapViewOfFile(addr)
|
||||
defer windows.CloseHandle(windows.Handle(unsafe.Pointer(hnd)))
|
||||
|
||||
return copyBytes(addr), nil
|
||||
}
|
||||
|
||||
func openFileMapping() (C.HANDLE, error) {
|
||||
lpName := C.CString("Global\\HWiNFO_SENS_SM2_REMOTE_0")
|
||||
defer C.free(unsafe.Pointer(lpName))
|
||||
|
||||
hnd := C.OpenFileMapping(syscall.FILE_MAP_READ, 0, lpName)
|
||||
if hnd == C.HANDLE(C.NULL) {
|
||||
errstr := util.HandleLastError(uint64(C.GetLastError()))
|
||||
return nil, fmt.Errorf("OpenFileMapping: %w", errstr)
|
||||
}
|
||||
|
||||
return hnd, nil
|
||||
}
|
||||
|
||||
func mapViewOfFile(hnd C.HANDLE) (uintptr, error) {
|
||||
addr, err := windows.MapViewOfFile(windows.Handle(unsafe.Pointer(hnd)), C.FILE_MAP_READ, 0, 0, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("MapViewOfFile: %w", err)
|
||||
}
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func unmapViewOfFile(ptr uintptr) error {
|
||||
err := windows.UnmapViewOfFile(ptr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UnmapViewOfFile: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
61
internal/hwinfo/util/util.go
Normal file
61
internal/hwinfo/util/util.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package util
|
||||
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
)
|
||||
|
||||
// ErrFileNotFound Windows error
|
||||
var ErrFileNotFound = errors.New("file not found")
|
||||
|
||||
// ErrInvalidHandle Windows error
|
||||
var ErrInvalidHandle = errors.New("invalid handle")
|
||||
|
||||
// UnknownError unhandled Windows error
|
||||
type UnknownError struct {
|
||||
Code uint64
|
||||
}
|
||||
|
||||
func (e UnknownError) Error() string {
|
||||
return fmt.Sprintf("unknown error code: %d", e.Code)
|
||||
}
|
||||
|
||||
// HandleLastError converts C.GetLastError() to golang error
|
||||
func HandleLastError(code uint64) error {
|
||||
switch code {
|
||||
case 2: // ERROR_FILE_NOT_FOUND
|
||||
return ErrFileNotFound
|
||||
case 6: // ERROR_INVALID_HANDLE
|
||||
return ErrInvalidHandle
|
||||
default:
|
||||
return UnknownError{Code: code}
|
||||
}
|
||||
}
|
||||
|
||||
func goStringFromPtr(ptr unsafe.Pointer, len int) string {
|
||||
s := C.GoStringN((*C.char)(ptr), C.int(len))
|
||||
return s[:strings.IndexByte(s, 0)]
|
||||
}
|
||||
|
||||
// DecodeCharPtr decodes ISO8859_1 string to UTF-8
|
||||
func DecodeCharPtr(ptr unsafe.Pointer, len int) string {
|
||||
s := goStringFromPtr(ptr, len)
|
||||
ds, err := decodeISO8859_1(s)
|
||||
if err != nil {
|
||||
log.Fatalf("TODO: failed to decode: %v", err)
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
var isodecoder = charmap.ISO8859_1.NewDecoder()
|
||||
|
||||
func decodeISO8859_1(in string) (string, error) {
|
||||
return isodecoder.String(in)
|
||||
}
|
||||
Reference in New Issue
Block a user