From 147c63af2caa9262b9d7ed078828ea74a78011ca Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Mon, 8 Sep 2025 10:26:26 +0200 Subject: [PATCH] gpio(hx711): add driver for 24 bit ADC e.g. measure with load cells --- README.md | 1 + drivers/gpio/README.md | 1 + drivers/gpio/hx711_driver.go | 253 ++++++++++++++++++ drivers/gpio/hx711_driver_test.go | 416 ++++++++++++++++++++++++++++++ examples/tinkerboard_hx711.go | 85 ++++++ go.mod | 6 +- go.sum | 6 +- 7 files changed, 765 insertions(+), 3 deletions(-) create mode 100644 drivers/gpio/hx711_driver.go create mode 100644 drivers/gpio/hx711_driver_test.go create mode 100644 examples/tinkerboard_hx711.go diff --git a/README.md b/README.md index 2cae53f1d..732b05317 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,7 @@ the `gobot/drivers/gpio` package: - Grove Touch Sensor (by using driver for Button) - HC-SR04 Ultrasonic Ranging Module - HD44780 LCD controller + - HX711 24 bit ADC e.g. used for weight cells - LED - Makey Button (by using driver for Button) - MAX7219 LED Dot Matrix diff --git a/drivers/gpio/README.md b/drivers/gpio/README.md index f01681556..172075083 100644 --- a/drivers/gpio/README.md +++ b/drivers/gpio/README.md @@ -25,6 +25,7 @@ Gobot has a extensible system for connecting to hardware devices. The following - Grove Touch Sensor (by using driver for Button) - HC-SR04 Ultrasonic Ranging Module - HD44780 LCD controller +- HX711 24 bit ADC e.g. used for weight cells - LED - Makey Button (by using driver for Button) - MAX7219 LED Dot Matrix diff --git a/drivers/gpio/hx711_driver.go b/drivers/gpio/hx711_driver.go new file mode 100644 index 000000000..026ff5338 --- /dev/null +++ b/drivers/gpio/hx711_driver.go @@ -0,0 +1,253 @@ +package gpio + +import ( + "fmt" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/hx711" + + gobot "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/system" +) + +const ( + defaultGainAndChannelCfg = hx711.A128 + defaultReadTimeout = 2 * time.Second + defaultReadTriesMax = 100 + defaultTickSleep = 0 * time.Microsecond // 0..50 us, depending on CPU performance and scheduling +) + +// hx711OptionApplier needs to be implemented by each configurable option type +type hx711OptionApplier interface { + apply(cfg *hx711Configuration) +} + +// hx711Configuration contains all changeable attributes of the driver. +type hx711Configuration struct { + gainAndChannelCfg hx711.GainAndChannelCfg + readTimeout time.Duration + readTriesMax uint8 // how often a check for ready state is done until the read timeout is reached + tickSleep time.Duration +} + +// hx711GainAndChannelCfgOption is the type for applying another gain and channel configuration. +type hx711GainAndChannelCfgOption hx711.GainAndChannelCfg + +// hx711ReadTimeoutOption is the type for applying another duration for the read timeout. +type hx711ReadTimeoutOption time.Duration + +// hx711ReadTriesMaxOption is the type for applying another value for maximum read tries. +type hx711ReadTriesMaxOption uint8 + +// hx711TickSleepOption is the type for applying another duration for the tick H-level-time. +type hx711TickSleepOption time.Duration + +type sensorer interface { + Configure(cfg *hx711.DeviceConfig) error + Zero(secondReading bool) error + Calibrate(setValue int32, secondReading bool) error + Update(m drivers.Measurement) error + Values() (int64, int64) + OffsetAndCalibrationFactor(secondReading bool) (int32, float32) + SetOffsetAndCalibrationFactor(offset int32, calibrationFactor float32, secondReading bool) error +} + +// HX711 is the gobot driver for the HX711 chip. +type HX711Driver struct { + *driver + + hx711Cfg *hx711Configuration + newSensor func(clockPin, dataPin gobot.DigitalPinner) sensorer + sensor sensorer +} + +// NewHX711 creates a new driver for HX711 24 bit, 2 channel, configurable ADC with serial output to measure small +// differential voltages. The device is handy for load cells but can be used to read all kind of Wheatstone bridges. +// Therefore the usage of the phrases "mass", "weight" or "load" are prevented in this driver - "value" is used instead. +// The implementation just wraps the according TinyGo-driver. +// +// Datasheet: https://cdn.sparkfun.com/datasheets/Sensors/ForceFlex/hx711_english.pdf +// +// Supported options: +// +// "WithName" +// "WithHX711GainAndChannelCfg" +// "WithHX711ReadTimeout" +// "WithHX711ReadTriesMax" +// "WithHX711TickSleep" +func NewHX711Driver(a gobot.Connection, clockPinID, dataPinID string, opts ...interface{}) *HX711Driver { + d := HX711Driver{ + driver: newDriver(a, "HX711"), + hx711Cfg: &hx711Configuration{ + gainAndChannelCfg: defaultGainAndChannelCfg, + readTimeout: defaultReadTimeout, + readTriesMax: defaultReadTriesMax, + tickSleep: defaultTickSleep, + }, + } + + for _, opt := range opts { + switch o := opt.(type) { + case optionApplier: + o.apply(d.driverCfg) + case hx711OptionApplier: + o.apply(d.hx711Cfg) + default: + panic(fmt.Sprintf("'%s' can not be applied on '%s'", opt, d.driverCfg.name)) + } + } + + // preparation in this way opens the possibility to change the sensor for unit tests before Connect() + d.newSensor = func(clockPin, dataPin gobot.DigitalPinner) sensorer { + clockPinSetState := func(v bool) error { + if v { + return clockPin.Write(1) + } + + return clockPin.Write(0) + } + + dataPinState := func() (bool, error) { + val, err := dataPin.Read() + + return val > 0, err + } + + return hx711.New(clockPinSetState, dataPinState, d.hx711Cfg.gainAndChannelCfg) + } + + // note: Unexport() of all pins will be done on adaptor.Finalize() + d.afterStart = func() error { + clockPin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(clockPinID) //nolint:forcetypeassert // ok here + if err != nil { + return fmt.Errorf("error on get clock pin: %v", err) + } + if err := clockPin.ApplyOptions(system.WithPinDirectionOutput(0)); err != nil { + return fmt.Errorf("error on apply output for clock pin: %v", err) + } + + // pins are inputs by default + dataPin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(dataPinID) //nolint:forcetypeassert // ok here + if err != nil { + return fmt.Errorf("error on get data pin: %v", err) + } + + if err := dataPin.ApplyOptions(system.WithPinPullUp()); err != nil { + return fmt.Errorf("error on apply pull up option for data pin: %v", err) + } + + d.sensor = d.newSensor(clockPin, dataPin) + + cfg := hx711.DefaultConfig + cfg.ReadTimeout = d.hx711Cfg.readTimeout + cfg.ReadTriesMax = d.hx711Cfg.readTriesMax + cfg.TickSleep = d.hx711Cfg.tickSleep + + // this leads to an active reset of the sensor hardware, so pins must be ready at this point + return d.sensor.Configure(&cfg) + } + + return &d +} + +// WithHX711GainAndChannelCfg use the given value for the gain and channel configuration instead the default hx711.A128. +func WithHX711GainAndChannelCfg(gc hx711.GainAndChannelCfg) hx711OptionApplier { + return hx711GainAndChannelCfgOption(gc) +} + +// WithHX711ReadTimeout use the given duration for read timeout instead the default of 2 seconds. +func WithHX711ReadTimeout(timeout time.Duration) hx711OptionApplier { + return hx711ReadTimeoutOption(timeout) +} + +// WithHX711ReadTriesMax use the given value for maximum read tries instead the default of 100. A value of zero will be +// automatically adjusted to the minimum of 1 try by the underlying sensor driver. +func WithHX711ReadTriesMax(tries uint8) hx711OptionApplier { + return hx711ReadTriesMaxOption(tries) +} + +// WithHX711TickSleep use the given duration for the H-level of the clock tick. The default is set to 0, which performs +// best for most CPUs. The value must be smaller than 60us, otherwise a hardware reset on each measure will occur. This +// will be automatically adjusted by the underlying sensor driver to this maximum, but the CPU will not rely on that +// value in any case, depending on its performance and scheduling. E.g. on a tinkerboard a value of 1us leads to a +// real duration of H-level between 10 and 70us, which causes wrong measures with high occurrence. +func WithHX711TickSleep(tickHLevelDuration time.Duration) hx711OptionApplier { + return hx711TickSleepOption(tickHLevelDuration) +} + +// Zero sets the offset for the reading. If the given flag is true, this is done for the second reading. +func (d *HX711Driver) Zero(secondReading bool) error { + // locking concurrent calls is done at sensor side + return d.sensor.Zero(secondReading) +} + +// Calibrate calculates, after a measurement of the set value is done, a factor for linear scaling the values of the +// subsequent measurements. The unit of the given set value define the unit of the measurement result later. Before +// using this function, the offset value should be obtained by calling Zero() function with no load. +// If the given flag is true, this is done for the second reading. +func (d *HX711Driver) Calibrate(setValue int32, secondReading bool) error { + // locking concurrent calls is done at sensor side + return d.sensor.Calibrate(setValue, secondReading) +} + +// OffsetAndCalibrationFactor returns linear correction values, used for reading. +// If the given flag is true, this values are related to the second reading. +func (d *HX711Driver) OffsetAndCalibrationFactor(secondReading bool) (int32, float32) { + // locking concurrent calls is done at sensor side + return d.sensor.OffsetAndCalibrationFactor(secondReading) +} + +// SetOffsetAndCalibrationFactor sets linear correction values, used for reading. +// If the given flag is true, this values are related to the second reading. +func (d *HX711Driver) SetOffsetAndCalibrationFactor(offset int32, calibrationFactor float32, secondReading bool) error { + // locking concurrent calls is done at sensor side + return d.sensor.SetOffsetAndCalibrationFactor(offset, calibrationFactor, secondReading) +} + +// Measure retrieves the values of the sensor and returns the measure. If only channel A is configured, channel B just +// returns always zero. Because the timing is somewhat difficult on CPUs regarding the low time of the clock pin (>60us +// leads to a reset of the hardware), it is suggested to implement a validation for the values and drop or repeat the +// measure in case of a value is unexpected. +func (d *HX711Driver) Measure() (int64, int64, error) { + // locking concurrent calls is done at sensor side + if err := d.sensor.Update(0); err != nil { + return 0, 0, err + } + + v1, v2 := d.sensor.Values() + + return v1, v2, nil +} + +func (o hx711GainAndChannelCfgOption) String() string { + return "hx711 gain and channel configuration option" +} + +func (o hx711GainAndChannelCfgOption) apply(cfg *hx711Configuration) { + cfg.gainAndChannelCfg = hx711.GainAndChannelCfg(o) +} + +func (o hx711ReadTimeoutOption) String() string { + return "hx711 read timeout option" +} + +func (o hx711ReadTimeoutOption) apply(cfg *hx711Configuration) { + cfg.readTimeout = time.Duration(o) +} + +func (o hx711ReadTriesMaxOption) String() string { + return "hx711 maximum read tries option" +} + +func (o hx711ReadTriesMaxOption) apply(cfg *hx711Configuration) { + cfg.readTriesMax = uint8(o) +} + +func (o hx711TickSleepOption) String() string { + return "hx711 tick sleep option" +} + +func (o hx711TickSleepOption) apply(cfg *hx711Configuration) { + cfg.tickSleep = time.Duration(o) +} diff --git a/drivers/gpio/hx711_driver_test.go b/drivers/gpio/hx711_driver_test.go new file mode 100644 index 000000000..623d9a7d8 --- /dev/null +++ b/drivers/gpio/hx711_driver_test.go @@ -0,0 +1,416 @@ +package gpio + +import ( + "errors" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/hx711" + + gobot "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/drivers/aio" +) + +func initTestHX711DriverWithStubbedAdaptor() (*HX711Driver, *sensorMock) { + const ( + clockPinID = "3" + dataPinID = "4" + ) + a := newGpioTestAdaptor() + _ = a.addDigitalPin(clockPinID) + _ = a.addDigitalPin(dataPinID) + d := NewHX711Driver(a, clockPinID, dataPinID) + s := sensorMock{} + d.newSensor = func(gobot.DigitalPinner, gobot.DigitalPinner) sensorer { return &s } + if err := d.Start(); err != nil { + panic(err) + } + + return d, &s +} + +func TestNewHX711Driver(t *testing.T) { + // arrange + const ( + clockPinID = "3" + dataPinID = "4" + ) + a := newGpioTestAdaptor() + _ = a.addDigitalPin(clockPinID) + _ = a.addDigitalPin(dataPinID) + // act + d := NewHX711Driver(a, clockPinID, dataPinID) + // assert + assert.IsType(t, &HX711Driver{}, d) + // assert: gpio.driver attributes + assert.NotNil(t, d.driver) + assert.True(t, strings.HasPrefix(d.driverCfg.name, "HX711")) + assert.Equal(t, a, d.connection) + assert.Nil(t, d.sensor) + require.NoError(t, d.afterStart()) // TODO: takes some time without options to reduce timings + assert.NotNil(t, d.sensor) + require.NoError(t, d.beforeHalt()) + assert.NotNil(t, d.Commander) + assert.NotNil(t, d.mutex) + // assert: driver specific attributes + assert.IsType(t, &hx711.Device{}, d.sensor) + assert.NotNil(t, d.newSensor) +} + +func TestNewHX711Driver_options(t *testing.T) { + // This is a general test, that options are applied in constructor by using the common WithName() option, least one + // option of this driver and one of another driver (which should lead to panic). Further tests for options can also + // be done by call of "WithOption(val).apply(cfg)". + // arrange + const ( + myName = "count up" + newGc = hx711.A64 + ) + panicFunc := func() { + NewHX711Driver(newGpioTestAdaptor(), "1", "2", WithName("crazy"), + aio.WithActuatorScaler(func(float64) int { return 0 })) + } + // act + d := NewHX711Driver(newGpioTestAdaptor(), "1", "2", WithName(myName), WithHX711GainAndChannelCfg(newGc)) + // assert + assert.Equal(t, newGc, d.hx711Cfg.gainAndChannelCfg) + assert.Equal(t, myName, d.Name()) + assert.PanicsWithValue(t, "'scaler option for analog actuators' can not be applied on 'crazy'", panicFunc) +} + +func TestHX711_WithHX711ReadTimeout(t *testing.T) { + // arrange + const myReadTimeout = 11 * time.Second + cfg := hx711Configuration{} + // act + WithHX711ReadTimeout(myReadTimeout).apply(&cfg) + // assert + assert.Equal(t, myReadTimeout, cfg.readTimeout) +} + +func TestHX711_WithHX711ReadTriesMax(t *testing.T) { + // arrange + const myReadTriesMax = uint8(12) + cfg := hx711Configuration{} + // act + WithHX711ReadTriesMax(myReadTriesMax).apply(&cfg) + // assert + assert.Equal(t, myReadTriesMax, cfg.readTriesMax) +} + +func TestHX711_WithHX711TickSleep(t *testing.T) { + // arrange + const myTickSleep = 13 * time.Nanosecond + cfg := hx711Configuration{} + // act + WithHX711TickSleep(myTickSleep).apply(&cfg) + // assert + assert.Equal(t, myTickSleep, cfg.tickSleep) +} + +func TestHX711Start(t *testing.T) { + const ( + clockPinID = "13" + dataPinID = "14" + ) + + cfg := hx711.DefaultConfig + cfg.TickSleep = defaultTickSleep + + tests := map[string]struct { + simulateIdErr string + simulateConfigErr error + wantCfgCalled []*hx711.DeviceConfig + wantErr string + }{ + "start_ok": { + wantCfgCalled: []*hx711.DeviceConfig{&cfg}, + }, + "error_no_clock_pin": { + simulateIdErr: clockPinID, + wantErr: "error on get clock pin: pin '13' not found", + }, + "error_no_data_pin": { + simulateIdErr: dataPinID, + wantErr: "error on get data pin: pin '14' not found", + }, + "error_configure": { + simulateConfigErr: errors.New("cfg error"), + wantCfgCalled: []*hx711.DeviceConfig{&cfg}, + wantErr: "cfg error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newGpioTestAdaptor() + var cpin gobot.DigitalPinner + if tc.simulateIdErr != clockPinID { + cpin = a.addDigitalPin(clockPinID) + } + var dpin gobot.DigitalPinner + if tc.simulateIdErr != dataPinID { + dpin = a.addDigitalPin(dataPinID) + } + d := NewHX711Driver(a, clockPinID, dataPinID) + s := sensorMock{cfgErr: tc.simulateConfigErr} + d.newSensor = func(clockPin, dataPin gobot.DigitalPinner) sensorer { + s.clockPin = clockPin + s.dataPin = dataPin + + return &s + } + // act + err := d.Start() + // assert + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + assert.Equal(t, cpin, s.clockPin) + assert.Equal(t, dpin, s.dataPin) + } + assert.Equal(t, tc.wantCfgCalled, s.cfgCalled) + }) + } +} + +func TestHX711Zero(t *testing.T) { + tests := map[string]struct { + secondReading bool + simulateErr error + wantCalled []bool + wantErr string + }{ + "zero_first_reading": { + wantCalled: []bool{false}, + }, + "zero_second_reading": { + secondReading: true, + wantCalled: []bool{true}, + }, + "error_zero": { + simulateErr: errors.New("zeroing error"), + wantCalled: []bool{false}, + wantErr: "zeroing error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, s := initTestHX711DriverWithStubbedAdaptor() + s.zeroErr = tc.simulateErr + // act + err := d.Zero(tc.secondReading) + // assert + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tc.wantCalled, s.zeroCalled) + }) + } +} + +func TestHX711Calibrate(t *testing.T) { + tests := map[string]struct { + setValue int32 + secondReading bool + simulateErr error + wantSecondReadingCalled []bool + wantErr string + }{ + "calibrate_first_reading": { + setValue: 1234, + wantSecondReadingCalled: []bool{false}, + }, + "calibrate_second_reading": { + setValue: -2345, + secondReading: true, + wantSecondReadingCalled: []bool{true}, + }, + "error_calibrate": { + simulateErr: errors.New("calibrate error"), + wantSecondReadingCalled: []bool{false}, + wantErr: "calibrate error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, s := initTestHX711DriverWithStubbedAdaptor() + s.calibrateErr = tc.simulateErr + // act + err := d.Calibrate(tc.setValue, tc.secondReading) + // assert + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.InDelta(t, tc.setValue, s.calibrateSetValueCalled, 0) + assert.Equal(t, tc.wantSecondReadingCalled, s.calibrateCalled) + }) + } +} + +func TestHX711SetOffsetAndCalibrationFactor(t *testing.T) { + tests := map[string]struct { + setOffsVal int32 + setFacVal float32 + secondReading bool + simulateErr error + wantSecondReadingCalled []bool + wantErr string + }{ + "setcal_first_reading": { + setOffsVal: 7654, + setFacVal: 123.4, + wantSecondReadingCalled: []bool{false}, + }, + "setcal_second_reading": { + setOffsVal: 654, + setFacVal: -23.45, + secondReading: true, + wantSecondReadingCalled: []bool{true}, + }, + "error_calibrate": { + setOffsVal: 54, + setFacVal: 3, + simulateErr: errors.New("calibrate error"), + wantSecondReadingCalled: []bool{false}, + wantErr: "calibrate error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, s := initTestHX711DriverWithStubbedAdaptor() + s.setCalValsErr = tc.simulateErr + // act + err := d.SetOffsetAndCalibrationFactor(tc.setOffsVal, tc.setFacVal, tc.secondReading) + gotOffs, gotFac := d.OffsetAndCalibrationFactor(tc.secondReading) + // assert + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tc.setOffsVal, s.setCalValsOffs) + assert.InDelta(t, tc.setFacVal, s.setCalValsFac, 0) + assert.Equal(t, tc.wantSecondReadingCalled, s.setCalValsCalled) + assert.Equal(t, tc.setOffsVal, gotOffs) + assert.InDelta(t, tc.setFacVal, gotFac, 0) + assert.Equal(t, tc.wantSecondReadingCalled, s.getCalValsCalled) + }) + } +} + +func TestHX711Measure(t *testing.T) { + tests := map[string]struct { + secondReading bool + simulateErr error + wantUpdateCalled []drivers.Measurement + wantValuesCalled int + wantErr string + }{ + "measure_ok": { + wantUpdateCalled: []drivers.Measurement{0}, + }, + "error_measure": { + simulateErr: errors.New("measure error"), + wantUpdateCalled: []drivers.Measurement{0}, + wantErr: "measure error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + const ( + v1 = 12345 + v2 = -5432 + ) + d, s := initTestHX711DriverWithStubbedAdaptor() + s.updateErr = tc.simulateErr + s.retVal1 = v1 + s.retVal2 = v2 + // act + got1, got2, err := d.Measure() + // assert + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + assert.InDelta(t, v1, got1, 0.0001) + assert.InDelta(t, v2, got2, 0.0001) + } + assert.Equal(t, tc.wantUpdateCalled, s.updateCalled) + }) + } +} + +type sensorMock struct { + clockPin gobot.DigitalPinner + dataPin gobot.DigitalPinner + + cfgCalled []*hx711.DeviceConfig + cfgErr error + zeroCalled []bool + zeroErr error + calibrateCalled []bool + calibrateSetValueCalled int32 + calibrateErr error + setCalValsCalled []bool + setCalValsOffs int32 + setCalValsFac float32 + setCalValsErr error + getCalValsCalled []bool + updateCalled []drivers.Measurement + updateErr error + valuesCalled int + retVal1 int64 + retVal2 int64 +} + +func (s *sensorMock) Configure(cfg *hx711.DeviceConfig) error { + s.cfgCalled = append(s.cfgCalled, cfg) + return s.cfgErr +} + +func (s *sensorMock) Zero(secondReading bool) error { + s.zeroCalled = append(s.zeroCalled, secondReading) + return s.zeroErr +} + +func (s *sensorMock) Calibrate(setValue int32, secondReading bool) error { + s.calibrateSetValueCalled = setValue + s.calibrateCalled = append(s.calibrateCalled, secondReading) + return s.calibrateErr +} + +func (s *sensorMock) Update(m drivers.Measurement) error { + s.updateCalled = append(s.updateCalled, m) + return s.updateErr +} + +func (s *sensorMock) Values() (int64, int64) { + s.valuesCalled++ + return s.retVal1, s.retVal2 +} + +func (s *sensorMock) OffsetAndCalibrationFactor(secondReading bool) (int32, float32) { + s.getCalValsCalled = append(s.getCalValsCalled, secondReading) + return s.setCalValsOffs, s.setCalValsFac +} + +func (s *sensorMock) SetOffsetAndCalibrationFactor(offset int32, calibrationFactor float32, secondReading bool) error { + s.setCalValsOffs = offset + s.setCalValsFac = calibrationFactor + s.setCalValsCalled = append(s.setCalValsCalled, secondReading) + return s.setCalValsErr +} diff --git a/examples/tinkerboard_hx711.go b/examples/tinkerboard_hx711.go new file mode 100644 index 000000000..95f7e096f --- /dev/null +++ b/examples/tinkerboard_hx711.go @@ -0,0 +1,85 @@ +//go:build example +// +build example + +// +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/drivers/gpio" + "gobot.io/x/gobot/v2/platforms/asus/tinkerboard" +) + +const ( + clockPinOutputID = "24" + dataPinInputID = "26" + calibrationWait = 10 * time.Second + cycleTime = 1 * time.Second +) + +// please adjust to your load used for calibration +const ( + setLoad = 100 // used unit will equal the measured unit + unit = "gram" +) + +// Wiring +// PWR Tinkerboard: 2(+5V), 6, 9, 14, 20 (GND) +// GPIO Tinkerboard: +// * header pin 24 is the clock output +// * pin 26 is used as data input, an external pull up resistor is suggested (e.g. 10K) +// HX711: the power (VCC) is wired to +5V and GND of tinkerboard, the same for SCK output and the DT input pin +func main() { + a := tinkerboard.NewAdaptor() + hx711 := gpio.NewHX711Driver(a, clockPinOutputID, dataPinInputID, + gpio.WithHX711ReadTimeout(1*time.Second), gpio.WithHX711ReadTriesMax(10)) + + work := func() { + fmt.Println("First lets calibrate the sensor...") + fmt.Printf("Please remove the mass completely for zeroing within %s\n", calibrationWait) + time.Sleep(calibrationWait) + + fmt.Println("Zeroing starts") + if err := hx711.Zero(false); err != nil { + fmt.Println("Zeroing failed") + panic(err) + } + + fmt.Printf("Please apply the load (%d%s) for calibration within %s\n", setLoad, unit, calibrationWait) + time.Sleep(calibrationWait) + + fmt.Println("Calibration starts") + if err := hx711.Calibrate(setLoad, false); err != nil { + fmt.Println("Calibration failed") + panic(err) + } + + offs, factor := hx711.OffsetAndCalibrationFactor(false) + fmt.Printf("Calibration done completely, offset: %d factor: %f\n", offs, factor) + + fmt.Println("Now start repeated measurement...") + _ = gobot.Every(cycleTime, func() { + if v, _, err := hx711.Measure(); err != nil { + log.Fatal(err) + } else { + fmt.Printf("measure done: %5.3f gram\n", v) + } + }) + } + + robot := gobot.NewRobot("loadCellBot", + []gobot.Connection{a}, + []gobot.Device{hx711}, + work, + ) + + if err := robot.Start(); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index 703aaf743..24a883ea4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.24.0 toolchain go1.24.6 +replace tinygo.org/x/drivers => ../../../tinygo.org/drivers + require ( github.com/0xcafed00d/joystick v1.0.1 github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f @@ -16,7 +18,7 @@ require ( github.com/nats-io/nats.go v1.45.0 github.com/nsf/termbox-go v1.1.1 github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/warthog618/go-gpiocdev v0.9.1 go.bug.st/serial v1.6.4 gocv.io/x/gocv v0.42.0 @@ -25,6 +27,7 @@ require ( periph.io/x/conn/v3 v3.7.2 periph.io/x/host/v3 v3.8.5 tinygo.org/x/bluetooth v0.13.0 + tinygo.org/x/drivers v0.33.0 ) require ( @@ -32,6 +35,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect diff --git a/go.sum b/go.sum index 639b3db03..2af199f96 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -64,8 +66,8 @@ github.com/soypat/seqs v0.0.0-20250630134107-01c3f05666ba/go.mod h1:oCVCNGCHMKoB github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU= github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU=