diff --git a/components/board/beaglebone/board.go b/components/board/beaglebone/board.go index 46d95762e95..b30d221cc0d 100644 --- a/components/board/beaglebone/board.go +++ b/components/board/beaglebone/board.go @@ -22,6 +22,5 @@ func init() { golog.Global().Debugw("error getting beaglebone GPIO board mapping", "error", err) } - // The false on this line means we're not using Periph. This lets us enable hardware PWM pins. - genericlinux.RegisterBoard(modelName, gpioMappings, false) + genericlinux.RegisterBoard(modelName, gpioMappings) } diff --git a/components/board/genericlinux/board.go b/components/board/genericlinux/board.go index c398e73eefa..7170fb9e2a3 100644 --- a/components/board/genericlinux/board.go +++ b/components/board/genericlinux/board.go @@ -18,9 +18,6 @@ import ( commonpb "go.viam.com/api/common/v1" pb "go.viam.com/api/component/board/v1" goutils "go.viam.com/utils" - "periph.io/x/conn/v3/gpio" - "periph.io/x/conn/v3/gpio/gpioreg" - "periph.io/x/conn/v3/physic" "go.viam.com/rdk/components/board" "go.viam.com/rdk/grpc" @@ -28,7 +25,7 @@ import ( ) // RegisterBoard registers a sysfs based board of the given model. -func RegisterBoard(modelName string, gpioMappings map[int]GPIOBoardMapping, usePeriphGpio bool) { +func RegisterBoard(modelName string, gpioMappings map[int]GPIOBoardMapping) { resource.RegisterComponent( board.API, resource.DefaultModelFamily.WithModel(modelName), @@ -39,7 +36,7 @@ func RegisterBoard(modelName string, gpioMappings map[int]GPIOBoardMapping, useP conf resource.Config, logger golog.Logger, ) (board.Board, error) { - return newBoard(ctx, conf, gpioMappings, usePeriphGpio, logger) + return newBoard(ctx, conf, gpioMappings, logger) }, }) } @@ -48,22 +45,18 @@ func newBoard( ctx context.Context, conf resource.Config, gpioMappings map[int]GPIOBoardMapping, - usePeriphGpio bool, logger golog.Logger, ) (board.Board, error) { cancelCtx, cancelFunc := context.WithCancel(context.Background()) b := sysfsBoard{ - Named: conf.ResourceName().AsNamed(), - usePeriphGpio: usePeriphGpio, - gpioMappings: gpioMappings, - logger: logger, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - - spis: map[string]*spiBus{}, - analogs: map[string]*wrappedAnalog{}, - // this is not yet modified during reconfiguration but maybe should be - pwms: map[string]pwmSetting{}, + Named: conf.ResourceName().AsNamed(), + gpioMappings: gpioMappings, + logger: logger, + cancelCtx: cancelCtx, + cancelFunc: cancelFunc, + + spis: map[string]*spiBus{}, + analogs: map[string]*wrappedAnalog{}, i2cs: map[string]*I2cBus{}, gpios: map[string]*gpioPin{}, interrupts: map[string]*digitalInterrupt{}, @@ -237,16 +230,8 @@ func findNewDigIntConfig( } func (b *sysfsBoard) reconfigureInterrupts(newConf *Config) error { - if b.usePeriphGpio { - if len(newConf.DigitalInterrupts) != 0 { - return errors.New("digital interrupts on Periph GPIO pins are not yet supported") - } - return nil // No digital interrupts to reconfigure. - } - - // If we get here, we need to reconfigure b.interrupts. Any pin that already exists in the - // right configuration should just be copied over; closing and re-opening it risks losing its - // state. + // Any pin that already exists in the right configuration should just be copied over; closing + // and re-opening it risks losing its state. newInterrupts := make(map[string]*digitalInterrupt, len(newConf.DigitalInterrupts)) // Reuse any old interrupts that have new configs @@ -362,12 +347,9 @@ type sysfsBoard struct { gpioMappings map[int]GPIOBoardMapping spis map[string]*spiBus analogs map[string]*wrappedAnalog - pwms map[string]pwmSetting i2cs map[string]*I2cBus logger golog.Logger - usePeriphGpio bool - // These next two are only used for non-periph.io pins gpios map[string]*gpioPin interrupts map[string]*digitalInterrupt @@ -376,11 +358,6 @@ type sysfsBoard struct { activeBackgroundWorkers sync.WaitGroup } -type pwmSetting struct { - dutyCycle gpio.Duty - frequency physic.Frequency -} - func (b *sysfsBoard) SPIByName(name string) (board.SPI, bool) { s, ok := b.spis[name] return s, ok @@ -397,10 +374,6 @@ func (b *sysfsBoard) AnalogReaderByName(name string) (board.AnalogReader, bool) } func (b *sysfsBoard) DigitalInterruptByName(name string) (board.DigitalInterrupt, bool) { - if b.usePeriphGpio { - return nil, false // Digital interrupts aren't supported. - } - b.mu.Lock() defer b.mu.Unlock() @@ -491,26 +464,12 @@ func (b *sysfsBoard) GPIOPinNames() []string { return names } -func (b *sysfsBoard) getGPIOLine(hwPin string) (gpio.PinIO, error) { - pinName := hwPin - - pin := gpioreg.ByName(pinName) - if pin == nil { - return nil, errors.Errorf("no global pin found for %q", pinName) - } - return pin, nil -} - func (b *sysfsBoard) GPIOPinByName(pinName string) (board.GPIOPin, error) { - if b.usePeriphGpio { - return b.periphGPIOPinByName(pinName) - } - // Otherwise, the pins are stored in b.gpios. if pin, ok := b.gpios[pinName]; ok { return pin, nil } - // check if pin is a digital interrupt + // Check if pin is a digital interrupt: those can still be used as inputs. if interrupt, interruptOk := b.interrupts[pinName]; interruptOk { return &gpioInterruptWrapperPin{*interrupt}, nil } @@ -518,59 +477,6 @@ func (b *sysfsBoard) GPIOPinByName(pinName string) (board.GPIOPin, error) { return nil, errors.Errorf("cannot find GPIO for unknown pin: %s", pinName) } -func (b *sysfsBoard) periphGPIOPinByName(pinName string) (board.GPIOPin, error) { - pin, err := b.getGPIOLine(pinName) - hwPWMSupported := false - if err != nil { - return nil, err - } - - return periphGpioPin{b, pin, pinName, hwPWMSupported}, nil -} - -// expects to already have lock acquired. -func (b *sysfsBoard) startSoftwarePWMLoop(gp periphGpioPin) { - b.activeBackgroundWorkers.Add(1) - goutils.ManagedGo(func() { - b.softwarePWMLoop(b.cancelCtx, gp) - }, b.activeBackgroundWorkers.Done) -} - -func (b *sysfsBoard) softwarePWMLoop(ctx context.Context, gp periphGpioPin) { - for { - cont := func() bool { - b.mu.RLock() - defer b.mu.RUnlock() - pwmSetting, ok := b.pwms[gp.pinName] - if !ok { - b.logger.Debug("pwm setting deleted; stopping") - return false - } - - if err := gp.set(true); err != nil { - b.logger.Errorw("error setting pin", "pin_name", gp.pinName, "error", err) - return true - } - onPeriod := time.Duration( - int64((float64(pwmSetting.dutyCycle) / float64(gpio.DutyMax)) * float64(pwmSetting.frequency.Period())), - ) - if !goutils.SelectContextOrWait(ctx, onPeriod) { - return false - } - if err := gp.set(false); err != nil { - b.logger.Errorw("error setting pin", "pin_name", gp.pinName, "error", err) - return true - } - offPeriod := pwmSetting.frequency.Period() - onPeriod - - return goutils.SelectContextOrWait(ctx, offPeriod) - }() - if !cont { - return - } - } -} - func (b *sysfsBoard) Status(ctx context.Context, extra map[string]interface{}) (*commonpb.BoardStatus, error) { return &commonpb.BoardStatus{}, nil } @@ -589,11 +495,6 @@ func (b *sysfsBoard) Close(ctx context.Context) error { b.mu.Unlock() b.activeBackgroundWorkers.Wait() - // For non-Periph boards, shut down all our open pins so we don't leak file descriptors - if b.usePeriphGpio { - return nil - } - var err error for _, pin := range b.gpios { err = multierr.Combine(err, pin.Close()) diff --git a/components/board/genericlinux/board_nonlinux.go b/components/board/genericlinux/board_nonlinux.go index 869d4843ee2..3b00e04f6fa 100644 --- a/components/board/genericlinux/board_nonlinux.go +++ b/components/board/genericlinux/board_nonlinux.go @@ -16,7 +16,7 @@ import ( // RegisterBoard would register a sysfs based board of the given model. However, this one never // creates a board, and instead returns errors about making a Linux board on a non-Linux OS. -func RegisterBoard(modelName string, gpioMappings map[int]GPIOBoardMapping, usePeriphGpio bool) { +func RegisterBoard(modelName string, gpioMappings map[int]GPIOBoardMapping) { resource.RegisterComponent( board.API, resource.DefaultModelFamily.WithModel(modelName), diff --git a/components/board/genericlinux/board_test.go b/components/board/genericlinux/board_test.go index c0abd801ad2..3c255148080 100644 --- a/components/board/genericlinux/board_test.go +++ b/components/board/genericlinux/board_test.go @@ -9,33 +9,25 @@ package genericlinux import ( "context" "testing" - "time" "github.com/edaniels/golog" commonpb "go.viam.com/api/common/v1" "go.viam.com/test" - "periph.io/x/conn/v3/gpio/gpiotest" "go.viam.com/rdk/components/board" ) -func TestRegisterBoard(t *testing.T) { - RegisterBoard("test", map[int]GPIOBoardMapping{}, true) -} - func TestGenericLinux(t *testing.T) { ctx := context.Background() - gp1 := &periphGpioPin{b: &sysfsBoard{ + b := &sysfsBoard{ logger: golog.NewTestLogger(t), - }} + } t.Run("test empty sysfs board", func(t *testing.T) { - test.That(t, gp1.b.GPIOPinNames(), test.ShouldBeNil) - test.That(t, gp1.b.SPINames(), test.ShouldBeNil) - _, err := gp1.PWM(ctx, nil) - test.That(t, err, test.ShouldNotBeNil) - _, err = gp1.b.GPIOPinByName("10") + test.That(t, b.GPIOPinNames(), test.ShouldBeNil) + test.That(t, b.SPINames(), test.ShouldBeNil) + _, err := b.GPIOPinByName("10") test.That(t, err, test.ShouldNotBeNil) }) @@ -54,103 +46,74 @@ func TestGenericLinux(t *testing.T) { boardSPIs["open"].bus.Store(&twoStr) boardSPIs["open"].openHandle.bus.bus.Store(&twoStr) - gp2 := &periphGpioPin{ - b: &sysfsBoard{ - Named: board.Named("foo").AsNamed(), - gpioMappings: nil, - spis: boardSPIs, - analogs: map[string]*wrappedAnalog{"an": {}}, - pwms: map[string]pwmSetting{ - "10": {dutyCycle: 1, frequency: 1}, - }, - logger: golog.NewTestLogger(t), - cancelCtx: ctx, - cancelFunc: func() { - }, + b = &sysfsBoard{ + Named: board.Named("foo").AsNamed(), + gpioMappings: nil, + spis: boardSPIs, + analogs: map[string]*wrappedAnalog{"an": {}}, + logger: golog.NewTestLogger(t), + cancelCtx: ctx, + cancelFunc: func() { }, - pinName: "10", - pin: &gpiotest.Pin{N: "10", Num: 10}, - hwPWMSupported: false, } t.Run("test analogs spis i2cs digital-interrupts and gpio names", func(t *testing.T) { - ans := gp2.b.AnalogReaderNames() + ans := b.AnalogReaderNames() test.That(t, ans, test.ShouldResemble, []string{"an"}) - an1, ok := gp2.b.AnalogReaderByName("an") + an1, ok := b.AnalogReaderByName("an") test.That(t, an1, test.ShouldHaveSameTypeAs, &wrappedAnalog{}) test.That(t, ok, test.ShouldBeTrue) - an2, ok := gp2.b.AnalogReaderByName("missing") + an2, ok := b.AnalogReaderByName("missing") test.That(t, an2, test.ShouldBeNil) test.That(t, ok, test.ShouldBeFalse) - sns := gp2.b.SPINames() + sns := b.SPINames() test.That(t, len(sns), test.ShouldEqual, 2) - sn1, ok := gp2.b.SPIByName("closed") + sn1, ok := b.SPIByName("closed") test.That(t, sn1, test.ShouldHaveSameTypeAs, &spiBus{}) test.That(t, ok, test.ShouldBeTrue) - sn2, ok := gp2.b.SPIByName("missing") + sn2, ok := b.SPIByName("missing") test.That(t, sn2, test.ShouldBeNil) test.That(t, ok, test.ShouldBeFalse) - ins := gp2.b.I2CNames() + ins := b.I2CNames() test.That(t, ins, test.ShouldBeNil) - in1, ok := gp2.b.I2CByName("in") + in1, ok := b.I2CByName("in") test.That(t, in1, test.ShouldBeNil) test.That(t, ok, test.ShouldBeFalse) - dns := gp2.b.DigitalInterruptNames() + dns := b.DigitalInterruptNames() test.That(t, dns, test.ShouldBeNil) - dn1, ok := gp2.b.DigitalInterruptByName("dn") + dn1, ok := b.DigitalInterruptByName("dn") test.That(t, dn1, test.ShouldBeNil) test.That(t, ok, test.ShouldBeFalse) - gns := gp2.b.GPIOPinNames() + gns := b.GPIOPinNames() test.That(t, gns, test.ShouldResemble, []string(nil)) - gn1, err := gp2.b.GPIOPinByName("10") + gn1, err := b.GPIOPinByName("10") test.That(t, err, test.ShouldNotBeNil) test.That(t, gn1, test.ShouldBeNil) }) t.Run("test genericlinux gpio pin functionality", func(t *testing.T) { - err := gp2.SetPWM(ctx, 50, nil) - test.That(t, err, test.ShouldBeNil) - - err = gp2.SetPWMFreq(ctx, 1000, nil) - test.That(t, err, test.ShouldBeNil) - - freq, err := gp2.PWMFreq(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, freq, test.ShouldEqual, 1000) - - duty, err := gp2.PWM(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, duty, test.ShouldEqual, 50) - - err = gp2.Set(ctx, true, nil) - test.That(t, err, test.ShouldBeNil) - - high, err := gp2.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, high, test.ShouldBeTrue) - - bs, err := gp2.b.Status(ctx, nil) + bs, err := b.Status(ctx, nil) test.That(t, err, test.ShouldBeNil) test.That(t, bs, test.ShouldResemble, &commonpb.BoardStatus{}) - bma := gp2.b.ModelAttributes() + bma := b.ModelAttributes() test.That(t, bma, test.ShouldResemble, board.ModelAttributes{}) }) t.Run("test spi functionality", func(t *testing.T) { - spi1 := gp2.b.spis["closed"] - spi2 := gp2.b.spis["open"] + spi1 := b.spis["closed"] + spi2 := b.spis["open"] sph1, err := spi1.OpenHandle() test.That(t, sph1, test.ShouldHaveSameTypeAs, &spiHandle{}) test.That(t, err, test.ShouldBeNil) @@ -164,24 +127,6 @@ func TestGenericLinux(t *testing.T) { test.That(t, err.Error(), test.ShouldContainSubstring, "closed") test.That(t, rx, test.ShouldBeNil) }) - - t.Run("test software pwm loop", func(t *testing.T) { - newCtx, cancel := context.WithTimeout(ctx, time.Duration(10)) - defer cancel() - gp2.b.softwarePWMLoop(newCtx, *gp2) - - gp2.b.pwms = map[string]pwmSetting{ - "10": {dutyCycle: 1, frequency: 1}, - } - gp2.b.startSoftwarePWMLoop(*gp2) - - gp2.b.softwarePWMLoop(newCtx, *gp2) - }) - - t.Run("test getGPIOLine", func(t *testing.T) { - _, err := gp2.b.getGPIOLine("10") - test.That(t, err.Error(), test.ShouldContainSubstring, "no global pin") - }) } func TestConfigValidate(t *testing.T) { diff --git a/components/board/genericlinux/periph_gpio.go b/components/board/genericlinux/periph_gpio.go deleted file mode 100644 index 705326efb3a..00000000000 --- a/components/board/genericlinux/periph_gpio.go +++ /dev/null @@ -1,114 +0,0 @@ -//go:build linux - -package genericlinux - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - "periph.io/x/conn/v3/gpio" - "periph.io/x/conn/v3/physic" -) - -type periphGpioPin struct { - b *sysfsBoard - pin gpio.PinIO - pinName string - hwPWMSupported bool -} - -func (gp periphGpioPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - gp.b.mu.Lock() - defer gp.b.mu.Unlock() - - delete(gp.b.pwms, gp.pinName) - - return gp.set(high) -} - -// This function is separate from Set(), above, because this one does not remove the pin from the -// board's pwms map. When simulating PWM in software, we use this function to turn the pin on and -// off while continuing to treat it as a PWM pin. -func (gp periphGpioPin) set(high bool) error { - l := gpio.Low - if high { - l = gpio.High - } - return gp.pin.Out(l) -} - -func (gp periphGpioPin) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { - return gp.pin.Read() == gpio.High, nil -} - -func (gp periphGpioPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - gp.b.mu.RLock() - defer gp.b.mu.RUnlock() - - pwm, ok := gp.b.pwms[gp.pinName] - if !ok { - return 0, fmt.Errorf("missing pin %s", gp.pinName) - } - return float64(pwm.dutyCycle) / float64(gpio.DutyMax), nil -} - -func (gp periphGpioPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - gp.b.mu.Lock() - defer gp.b.mu.Unlock() - - last, alreadySet := gp.b.pwms[gp.pinName] - var freqHz physic.Frequency - if last.frequency != 0 { - freqHz = last.frequency - } - duty := gpio.Duty(dutyCyclePct * float64(gpio.DutyMax)) - last.dutyCycle = duty - gp.b.pwms[gp.pinName] = last - - if gp.hwPWMSupported { - err := gp.pin.PWM(duty, freqHz) - // TODO: [RSDK-569] (rh) find or implement a PWM sysfs that works with hardware pwm mappings - // periph.io does not implement PWM - if err != nil { - return errors.New("sysfs PWM not currently supported, use another pin for software PWM loops") - } - } - - if !alreadySet { - gp.b.startSoftwarePWMLoop(gp) - } - - return nil -} - -func (gp periphGpioPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - gp.b.mu.RLock() - defer gp.b.mu.RUnlock() - - return uint(gp.b.pwms[gp.pinName].frequency / physic.Hertz), nil -} - -func (gp periphGpioPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - gp.b.mu.Lock() - defer gp.b.mu.Unlock() - - last, alreadySet := gp.b.pwms[gp.pinName] - var duty gpio.Duty - if last.dutyCycle != 0 { - duty = last.dutyCycle - } - frequency := physic.Hertz * physic.Frequency(freqHz) - last.frequency = frequency - gp.b.pwms[gp.pinName] = last - - if gp.hwPWMSupported { - return gp.pin.PWM(duty, frequency) - } - - if !alreadySet { - gp.b.startSoftwarePWMLoop(gp) - } - - return nil -} diff --git a/components/board/jetson/board.go b/components/board/jetson/board.go index 2c617c080bb..2c19c4ef7bf 100644 --- a/components/board/jetson/board.go +++ b/components/board/jetson/board.go @@ -22,5 +22,5 @@ func init() { golog.Global().Debugw("error getting jetson GPIO board mapping", "error", err) } - genericlinux.RegisterBoard(modelName, gpioMappings, false) + genericlinux.RegisterBoard(modelName, gpioMappings) } diff --git a/components/board/nanopi/board.go b/components/board/nanopi/board.go deleted file mode 100644 index 4a5ec7757aa..00000000000 --- a/components/board/nanopi/board.go +++ /dev/null @@ -1,21 +0,0 @@ -// Package nanopi implements a nanopi based board. -// This is an experimental package. -// Supported functionality: GPIO pins, I2C, SPI, Software PWM -// Unsupported functionality: Digital Interrupts, Hardware PWM -package nanopi - -import ( - "github.com/edaniels/golog" - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" -) - -const modelName = "nanopi" - -func init() { - if _, err := host.Init(); err != nil { - golog.Global().Debugw("error initializing host", "error", err) - } - genericlinux.RegisterBoard(modelName, nil, true) -} diff --git a/components/board/register/register.go b/components/board/register/register.go index aa0cccfcdba..f49727d3576 100644 --- a/components/board/register/register.go +++ b/components/board/register/register.go @@ -7,7 +7,6 @@ import ( _ "go.viam.com/rdk/components/board/fake" _ "go.viam.com/rdk/components/board/hat/pca9685" _ "go.viam.com/rdk/components/board/jetson" - _ "go.viam.com/rdk/components/board/nanopi" _ "go.viam.com/rdk/components/board/numato" _ "go.viam.com/rdk/components/board/pi" _ "go.viam.com/rdk/components/board/ti" diff --git a/components/board/ti/board.go b/components/board/ti/board.go index 36ec63c8d00..701cbe06c89 100644 --- a/components/board/ti/board.go +++ b/components/board/ti/board.go @@ -22,5 +22,5 @@ func init() { golog.Global().Debugw("error getting ti GPIO board mapping", "error", err) } - genericlinux.RegisterBoard(modelName, gpioMappings, false) + genericlinux.RegisterBoard(modelName, gpioMappings) } diff --git a/components/board/upboard/board.go b/components/board/upboard/board.go index c00142505cd..8b553e00ca9 100644 --- a/components/board/upboard/board.go +++ b/components/board/upboard/board.go @@ -28,6 +28,5 @@ func init() { golog.Global().Debugw("error getting up board GPIO board mapping", "error", err) } - // Not using Periph io for GPIO - genericlinux.RegisterBoard(modelName, gpioMappings, false) + genericlinux.RegisterBoard(modelName, gpioMappings) } diff --git a/go.mod b/go.mod index 9205668040c..b758d5d4d11 100644 --- a/go.mod +++ b/go.mod @@ -229,7 +229,6 @@ require ( github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jonboulle/clockwork v0.3.0 // indirect github.com/julz/importas v0.1.0 // indirect github.com/junk1tm/musttag v0.4.5 // indirect github.com/kisielk/errcheck v1.6.3 // indirect diff --git a/go.sum b/go.sum index 67620ec98d6..b8ea4675ddc 100644 --- a/go.sum +++ b/go.sum @@ -799,7 +799,6 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= -github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=