New mechanism

This commit is contained in:
2025-05-12 21:34:47 -05:00
parent 1416362d08
commit aa04754f66
8 changed files with 434 additions and 569 deletions

View 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
View File

@@ -0,0 +1,42 @@
# temper
[![Latest Release](https://img.shields.io/github/release/taigrr/temper.svg?style=for-the-badge)](https://github.com/taigrr/temper/releases)
[![Software License](https://img.shields.io/badge/license-0BSD-blue.svg?style=for-the-badge)](/LICENSE)
[![Go ReportCard](https://goreportcard.com/badge/github.com/taigrr/temper?style=for-the-badge)](https://goreportcard.com/report/taigrr/temper)
[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](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)

View 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
}

View 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
}