New mechanism
This commit is contained in:
		
							
								
								
									
										69
									
								
								internal/temper/discovery_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								internal/temper/discovery_linux.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
package temper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Function which scans /dev for temperXX devices (configured by udev)
 | 
			
		||||
//
 | 
			
		||||
// A timeout of 250ms is recommended but as YMMV, this function allows
 | 
			
		||||
// for an arbitrary timeout.
 | 
			
		||||
func FindTempersWithTimeout(timeout time.Duration) ([]*Temper, error) {
 | 
			
		||||
	// list over dev folder for temperXX devices
 | 
			
		||||
	dirEnts, err := os.ReadDir("/dev")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return []*Temper{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tempers := []*Temper{}
 | 
			
		||||
	for _, d := range dirEnts {
 | 
			
		||||
		if name := d.Name(); strings.HasPrefix(name, "temper") {
 | 
			
		||||
			if isInputDevice(name) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			temper, err := New(filepath.Join("/dev", name))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			// attempt to take a reading from the temper
 | 
			
		||||
			// if the reading times out, assume it's a false positive
 | 
			
		||||
			ctx, cancel := context.WithTimeout(context.Background(), timeout)
 | 
			
		||||
			_, err = temper.ReadCWithContext(ctx)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				tempers = append(tempers, temper)
 | 
			
		||||
			} else {
 | 
			
		||||
				// prevent file descriptor leaks
 | 
			
		||||
				temper.Close()
 | 
			
		||||
			}
 | 
			
		||||
			cancel()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return tempers, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to return list of temper devices available in /dev
 | 
			
		||||
//
 | 
			
		||||
// Uses the recommended default timeout of 250ms. See
 | 
			
		||||
// FindTempersWithTimeout for more details
 | 
			
		||||
func FindTempers() ([]*Temper, error) {
 | 
			
		||||
	return FindTempersWithTimeout(time.Millisecond * 250)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Determines if the current hidraw device also doubles as a virtual keyboard
 | 
			
		||||
//
 | 
			
		||||
// some temper devices also have a keyboard emulation mode.
 | 
			
		||||
// The regular discovery function can trigger data entry mode, and cause
 | 
			
		||||
// annoying and distracting typing to happen, so this function allows us to
 | 
			
		||||
// skip the check on devices we know aren't temper sensors
 | 
			
		||||
func isInputDevice(temperDescriptor string) bool {
 | 
			
		||||
	hidrawDesc := strings.ReplaceAll(temperDescriptor, "temper", "hidraw")
 | 
			
		||||
	inputPath := filepath.Join("/sys/class/hidraw", hidrawDesc, "device/input")
 | 
			
		||||
	if _, statErr := os.Stat(inputPath); statErr == nil {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								internal/temper/readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								internal/temper/readme.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
# temper
 | 
			
		||||
 | 
			
		||||
[](https://github.com/taigrr/temper/releases)
 | 
			
		||||
[](/LICENSE)
 | 
			
		||||
[](https://goreportcard.com/report/taigrr/temper)
 | 
			
		||||
[](https://pkg.go.dev/github.com/taigrr/temper)
 | 
			
		||||
 | 
			
		||||
A zero-dependency library to read USB TEMPer thermometers on Linux.
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
 | 
			
		||||
On Linux you need to set up some udev rules to be able to access the device as
 | 
			
		||||
a non-root/regular user.
 | 
			
		||||
Edit `/etc/udev/rules.d/99-temper.rules` and add these lines:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="e025", GROUP="plugdev", SYMLINK+="temper%n"
 | 
			
		||||
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0c45", ATTRS{idProduct}=="7401", GROUP="plugdev", SYMLINK+="temper%n"
 | 
			
		||||
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0c45", ATTRS{idProduct}=="7402", GROUP="plugdev", SYMLINK+="temper%n"
 | 
			
		||||
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1130", ATTRS{idProduct}=="660c", GROUP="plugdev", SYMLINK+="temper%n"
 | 
			
		||||
```
 | 
			
		||||
Note that there are many versions of the TEMPer USB and your
 | 
			
		||||
`idVendor` and `idProduct` ATTRs may differ.
 | 
			
		||||
 | 
			
		||||
Make sure your user is part of the `plugdev` group and reload the rules with
 | 
			
		||||
`sudo udevadm control --reload-rules`.
 | 
			
		||||
Unplug and replug the device.
 | 
			
		||||
 | 
			
		||||
## Example Code
 | 
			
		||||
 | 
			
		||||
There are examples on how to use this repo in [examples/main.go](/examples/main.go)
 | 
			
		||||
 | 
			
		||||
Additionally, there is a cli-tool available at [temper-cli](https://github.com/taigrr/temper-cli)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Acknowledgement
 | 
			
		||||
During my development I tested my code against the shell script found in [this article](https://funprojects.blog/2021/05/02/temper-usb-temperature-sensor/).
 | 
			
		||||
 | 
			
		||||
As I only have one TEMPer device, I have sourced the product and vendor IDs for
 | 
			
		||||
other TEMPer devices for the sample `.rules` file (above) from [this repo](https://github.com/edorfaus/TEMPered/blob/master/libtempered/temper_type.c).
 | 
			
		||||
 | 
			
		||||
Full credit to [taigrr](https://github.com/taigrr/temper?tab=readme-ov-file)
 | 
			
		||||
@@ -1,62 +1,109 @@
 | 
			
		||||
package temper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/gousb"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetTemperature() (float64, error) {
 | 
			
		||||
	ctx := gousb.NewContext()
 | 
			
		||||
	defer ctx.Close()
 | 
			
		||||
 | 
			
		||||
	vid, pid := gousb.ID(0x0c45), gousb.ID(0x7401)
 | 
			
		||||
	devs, err := ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool {
 | 
			
		||||
		return desc.Vendor == vid && desc.Product == pid
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(devs) == 0 {
 | 
			
		||||
		return 0, fmt.Errorf("no devices found matching VID %s and PID %s", vid, pid)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	devs[0].SetAutoDetach(true)
 | 
			
		||||
 | 
			
		||||
	for _, d := range devs {
 | 
			
		||||
		defer d.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg, err := devs[0].Config(1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	defer cfg.Close()
 | 
			
		||||
 | 
			
		||||
	intf, err := cfg.Interface(1, 0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	defer intf.Close()
 | 
			
		||||
 | 
			
		||||
	epIn, err := intf.InEndpoint(0x82)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = devs[0].Control(
 | 
			
		||||
		0x21, 0x09, 0x0200, 0x01, []byte{0x01, 0x80, 0x33, 0x01, 0x00, 0x00, 0x00, 0x00},
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := make([]byte, 8)
 | 
			
		||||
	if _, err = epIn.Read(buf); err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return float64(buf[2]) + float64(buf[3])/256, nil
 | 
			
		||||
type Temper struct {
 | 
			
		||||
	descriptor string
 | 
			
		||||
	reader     *os.File
 | 
			
		||||
	writer     *os.File
 | 
			
		||||
	lock       sync.Mutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type reading struct {
 | 
			
		||||
	value float32
 | 
			
		||||
	error error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Open a new Temper Device
 | 
			
		||||
//
 | 
			
		||||
// It is the caller's responsibility to call Close()
 | 
			
		||||
// to prevent a file descriptor leak
 | 
			
		||||
func New(descriptor string) (*Temper, error) {
 | 
			
		||||
	if _, statErr := os.Stat(descriptor); statErr != nil {
 | 
			
		||||
		return &Temper{}, statErr
 | 
			
		||||
	}
 | 
			
		||||
	r, readErr := os.Open(descriptor)
 | 
			
		||||
	if readErr != nil {
 | 
			
		||||
		return &Temper{}, readErr
 | 
			
		||||
	}
 | 
			
		||||
	w, writeErr := os.OpenFile(descriptor,
 | 
			
		||||
		os.O_APPEND|os.O_WRONLY, os.ModeDevice)
 | 
			
		||||
	if writeErr != nil {
 | 
			
		||||
		r.Close()
 | 
			
		||||
		return &Temper{}, writeErr
 | 
			
		||||
	}
 | 
			
		||||
	t := Temper{reader: r, writer: w, descriptor: descriptor}
 | 
			
		||||
	return &t, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Temper) Descriptor() string {
 | 
			
		||||
	return t.descriptor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Temper) String() string {
 | 
			
		||||
	return t.Descriptor()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Close the file descriptors for the Temper Device
 | 
			
		||||
func (t *Temper) Close() error {
 | 
			
		||||
	t.lock.Lock()
 | 
			
		||||
	defer t.lock.Unlock()
 | 
			
		||||
	rErr := t.reader.Close()
 | 
			
		||||
	wErr := t.writer.Close()
 | 
			
		||||
	if rErr != nil {
 | 
			
		||||
		return rErr
 | 
			
		||||
	}
 | 
			
		||||
	return wErr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Read the internal sensor temperature in Celcius
 | 
			
		||||
func (t *Temper) ReadC() (float32, error) {
 | 
			
		||||
	t.lock.Lock()
 | 
			
		||||
	defer t.lock.Unlock()
 | 
			
		||||
	tempChan := make(chan reading)
 | 
			
		||||
	go func() {
 | 
			
		||||
		// prepare a buffer and get ready to read
 | 
			
		||||
		// from the temper hid device
 | 
			
		||||
		response := make([]byte, 8)
 | 
			
		||||
		_, err := t.reader.Read(response)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			tempChan <- reading{0, err}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// interpret the bytes as hex
 | 
			
		||||
		hexStr := hex.EncodeToString(response)
 | 
			
		||||
		// extract the temperature fields from the string
 | 
			
		||||
		temp := hexStr[4:8]
 | 
			
		||||
		// convert the hex ints to an integer
 | 
			
		||||
		tempInt, err := strconv.ParseInt(temp, 16, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			tempChan <- reading{0, err}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// divide the result by 100 and send to chan
 | 
			
		||||
		float := float32(tempInt) / 100
 | 
			
		||||
		tempChan <- reading{error: nil, value: float}
 | 
			
		||||
	}()
 | 
			
		||||
	// send magic byte sequence to request a temperature reading
 | 
			
		||||
	_, err := t.writer.Write([]byte{0, 1, 128, 51, 1, 0, 0, 0, 0})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	read := <-tempChan
 | 
			
		||||
	return read.value, read.error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Read the internal sensor temperature in Fahrenheit
 | 
			
		||||
func (t *Temper) ReadF() (float32, error) {
 | 
			
		||||
	c, err := t.ReadC()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	f := c*9.0/5.0 + 32.0
 | 
			
		||||
	return f, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								internal/temper/temperctx_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								internal/temper/temperctx_linux.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
package temper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (t *Temper) ReadCWithContext(ctx context.Context) (float32, error) {
 | 
			
		||||
	t.lock.Lock()
 | 
			
		||||
	defer t.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	deadline, ok := ctx.Deadline()
 | 
			
		||||
	if !ok {
 | 
			
		||||
		deadline = time.Time{}
 | 
			
		||||
	}
 | 
			
		||||
	err := t.writer.SetWriteDeadline(deadline)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer t.writer.SetDeadline(time.Time{})
 | 
			
		||||
	err = t.reader.SetReadDeadline(deadline)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer t.reader.SetDeadline(time.Time{})
 | 
			
		||||
	tempChan := make(chan reading)
 | 
			
		||||
	go func() {
 | 
			
		||||
		// prepare a buffer and get ready to read
 | 
			
		||||
		// from the temper hid device
 | 
			
		||||
		response := make([]byte, 8)
 | 
			
		||||
		err := t.reader.SetDeadline(deadline)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
		_, err = t.reader.Read(response)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			tempChan <- reading{0, err}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// interpret the bytes as hex
 | 
			
		||||
		hexStr := hex.EncodeToString(response)
 | 
			
		||||
		// extract the temperature fields from the string
 | 
			
		||||
		temp := hexStr[4:8]
 | 
			
		||||
		// convert the hex ints to an integer
 | 
			
		||||
		tempInt, err := strconv.ParseInt(temp, 16, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			tempChan <- reading{0, err}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// divide the result by 100 and send to chan
 | 
			
		||||
		float := float32(tempInt) / 100
 | 
			
		||||
		tempChan <- reading{error: nil, value: float}
 | 
			
		||||
	}()
 | 
			
		||||
	// send magic byte sequence to request a temperature reading
 | 
			
		||||
	_, wErr := t.writer.Write([]byte{0, 1, 128, 51, 1, 0, 0, 0, 0})
 | 
			
		||||
	if wErr != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	read := <-tempChan
 | 
			
		||||
	return read.value, read.error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Read the internal sensor temperature in Fahrenheit
 | 
			
		||||
func (t *Temper) ReadFWithContext(ctx context.Context) (float32, error) {
 | 
			
		||||
	c, err := t.ReadCWithContext(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	f := c*9.0/5.0 + 32.0
 | 
			
		||||
	return f, err
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user