Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rtlamr for openSUSE:Factory checked in at 2026-11-01 22:25:06 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rtlamr (Old) and /work/SRC/openSUSE:Factory/.rtlamr.new.1255 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rtlamr" Sun Nov 1 22:25:06 2026 rev:2 rq:1394296 version:0.9.6 Changes: -------- --- /work/SRC/openSUSE:Factory/rtlamr/rtlamr.changes 2026-09-01 15:48:15.373546212 +0200 +++ /work/SRC/openSUSE:Factory/.rtlamr.new.1255/rtlamr.changes 2026-11-01 22:26:11.660911227 +0100 @@ -1,0 +2,21 @@ +Sun Nov 1 14:39:02 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to version 0.9.6: + * Signal processing switched from float64 to float32, with + combined filter and quantize step in the R900 stage + * Reworked error handling on top of stdlib errors: exit 1 when + the receiver stops on an error, propagate csv write errors, + fail gracefully on invalid message types + * Set the missed-sample threshold to 90% and log slow sample + consumption as a warning + * rtltcp connection arguments moved into rtlamr flags + * Fix nil-pointer dereference and IDM message handling + * Pin the scmplus JSON field name PacketCRC for compatibility +- Drop rtlamr-no-debug-stdout-print.patch, contained upstream: + no more build directory printed to stdout, which corrupted + the first line of csv/json output +- Regenerate vendor.tar.gz, now shipping only rtltcp +- License is AGPL-3.0-only AND BSD-3-Clause: pkg/errors is gone, + so the BSD-2-Clause grant no longer applies + +------------------------------------------------------------------- Old: ---- rtlamr-0.9.5.tar.gz rtlamr-no-debug-stdout-print.patch New: ---- rtlamr-0.9.6.tar.gz ----------(Old B)---------- Old: * Pin the scmplus JSON field name PacketCRC for compatibility - Drop rtlamr-no-debug-stdout-print.patch, contained upstream: no more build directory printed to stdout, which corrupted ----------(Old E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rtlamr.spec ++++++ --- /var/tmp/diff_new_pack.uQRVas/_old 2026-11-01 22:26:12.937964167 +0100 +++ /var/tmp/diff_new_pack.uQRVas/_new 2026-11-01 22:26:12.940964291 +0100 @@ -18,30 +18,21 @@ Name: rtlamr -Version: 0.9.5 +Version: 0.9.6 Release: 0 Summary: SDR receiver for Itron ERT compatible smart meters # Legal-Review-Notice: aggregate licence of the statically linked binary. # Tagged AGPL-3.0-only although rtlamr's own headers grant "version 3 or (at # your option) any later version": the vendored github.com/bemasher/rtltcp -# ships NO licence text at the revision go.mod pins, and its upstream repo -# carries the bare AGPLv3 with no or-later grant. A later-version upgrade is -# therefore not granted for the aggregate, so -or-later would overstate it. -# Please confirm the rtltcp revision's status. +# grants no or-later upgrade (its sources carry no licence header and its +# repo ships the stock AGPLv3 text), so -or-later would overstate the +# aggregate. Please confirm the rtltcp revision's status. # BSD-3-Clause: in-tree r900/gf (Go Authors, r900/gf/LICENSE), imported by -# r900, plus vendored golang.org/x/xerrors. -# BSD-2-Clause: vendored github.com/pkg/errors. -# go.mod also requires github.com/sirupsen/logrus, but no source file imports -# it, so go mod vendor omits it and it is not linked. -License: AGPL-3.0-only AND BSD-2-Clause AND BSD-3-Clause +# r900. +License: AGPL-3.0-only AND BSD-3-Clause URL: https://github.com/bemasher/rtlamr Source: https://github.com/bemasher/rtlamr/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz Source1: vendor.tar.gz -# PATCH-FIX-UPSTREAM rtlamr-no-debug-stdout-print.patch gh#bemasher/rtlamr@a1f6df8 -# -- init() printed the build dir to stdout, corrupting the first line of -# csv/json output; upstream's fix is one commit past v0.9.5 and unreleased, so -# drop this only once a release actually contains a1f6df8 -Patch0: rtlamr-no-debug-stdout-print.patch BuildRequires: golang(API) >= 1.21 %description @@ -52,8 +43,6 @@ %autosetup -p1 -a1 # Notices for the BSD-licensed code compiled in (see Legal-Review-Notice) cp r900/gf/LICENSE LICENSE.r900-gf -cp vendor/github.com/pkg/errors/LICENSE LICENSE.pkg-errors -cp vendor/golang.org/x/xerrors/LICENSE LICENSE.xerrors %build # -trimpath: keep the build root out of the binary (reproducible builds); it @@ -70,7 +59,7 @@ go test -mod=vendor ./... %files -%license LICENSE LICENSE.pkg-errors LICENSE.r900-gf LICENSE.xerrors +%license LICENSE LICENSE.r900-gf %doc README.md %{_bindir}/rtlamr ++++++ rtlamr-0.9.5.tar.gz -> rtlamr-0.9.6.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/csv/csv.go new/rtlamr-0.9.6/csv/csv.go --- old/rtlamr-0.9.5/csv/csv.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/csv/csv.go 2026-10-31 23:50:59.000000000 +0100 @@ -2,9 +2,8 @@ import ( "encoding/csv" + "fmt" "io" - - "golang.org/x/xerrors" ) // Produces a list of fields making up a record. @@ -26,13 +25,21 @@ // newline character. Value given must implement the Recorder interface. func (enc *Encoder) Encode(v interface{}) (err error) { defer func() { - if err, _ = recover().(error); err != nil { - err = xerrors.Errorf("recovered: %w", err) + // Only override err if we actually recovered from a panic; otherwise + // keep whatever error the write returned. + switch r := recover().(type) { + case nil: + case error: + err = fmt.Errorf("recovered: %w", r) + default: + err = fmt.Errorf("recovered: %v", r) } }() - err = enc.w.Write(v.(Recorder).Record()) + if err = enc.w.Write(v.(Recorder).Record()); err != nil { + return err + } enc.w.Flush() - return nil + return enc.w.Error() } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/csv/csv_test.go new/rtlamr-0.9.6/csv/csv_test.go --- old/rtlamr-0.9.5/csv/csv_test.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/csv/csv_test.go 2026-10-31 23:50:59.000000000 +0100 @@ -3,10 +3,9 @@ import ( "bytes" "encoding/csv" + "errors" "runtime" "testing" - - "golang.org/x/xerrors" ) func TestRecorderNil(t *testing.T) { @@ -42,7 +41,37 @@ err := enc.Encode(NonRecorder{}) var runtimeErr runtime.Error - if !xerrors.As(err, &runtimeErr) { + if !errors.As(err, &runtimeErr) { t.Fatalf("%+v\n", runtimeErr) } } + +type failWriter struct{} + +func (failWriter) Write([]byte) (int, error) { return 0, errWrite } + +var errWrite = errors.New("write failed") + +// TestWriteError checks that an error writing to the underlying stream is +// returned. Previously Encode always returned nil on a non-panicking path. +func TestWriteError(t *testing.T) { + enc := NewEncoder(failWriter{}) + + if err := enc.Encode(Msg{}); !errors.Is(err, errWrite) { + t.Fatalf("Encode() = %v, want %v", err, errWrite) + } +} + +type panicRecorder struct{} + +func (panicRecorder) Record() []string { panic("not an error value") } + +// TestNonErrorPanic checks that a panic with a non-error value is still +// reported rather than silently swallowed. +func TestNonErrorPanic(t *testing.T) { + enc := NewEncoder(&bytes.Buffer{}) + + if err := enc.Encode(panicRecorder{}); err == nil { + t.Fatal("Encode() = nil, want an error") + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/go.mod new/rtlamr-0.9.6/go.mod --- old/rtlamr-0.9.5/go.mod 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/go.mod 2026-10-31 23:50:59.000000000 +0100 @@ -1,10 +1,5 @@ module github.com/bemasher/rtlamr -go 1.12 +go 1.21 -require ( - github.com/bemasher/rtltcp v0.0.0-20151011062038-3aed81c166c5 - github.com/pkg/errors v0.8.1 - github.com/sirupsen/logrus v1.9.3 - golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 -) +require github.com/bemasher/rtltcp v0.0.0-20261031224328-8792921d55f4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/go.sum new/rtlamr-0.9.6/go.sum --- old/rtlamr-0.9.5/go.sum 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/go.sum 2026-10-31 23:50:59.000000000 +0100 @@ -1,22 +1,2 @@ -github.com/bemasher/rtltcp v0.0.0-20151011062038-3aed81c166c5 h1:QFs9NpZKQY/WPjOfgNN8Qt83scnnhDueQrnjZOG884M= -github.com/bemasher/rtltcp v0.0.0-20151011062038-3aed81c166c5/go.mod h1:O6JJfPo2Vr2FA+N401mWyEVhwq5Wo/z1dfX+tIKGRUU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/bemasher/rtltcp v0.0.0-20261031224328-8792921d55f4 h1:pcRtQ998Ux2r/amGzlUCejEddwac8U3bkM5PxsI1gYM= +github.com/bemasher/rtltcp v0.0.0-20261031224328-8792921d55f4/go.mod h1:9ivUuu07OXdqEEu1OwUojD1ORWKit39zDLr1cglzvcE= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/idm/idm.go new/rtlamr-0.9.6/idm/idm.go --- old/rtlamr-0.9.5/idm/idm.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/idm/idm.go 2026-10-31 23:50:59.000000000 +0100 @@ -128,9 +128,9 @@ idm.ERTSerialNumber = binary.BigEndian.Uint32(data.Bytes[9:13]) idm.ConsumptionIntervalCount = data.Bytes[13] idm.ModuleProgrammingState = data.Bytes[14] - idm.TamperCounters = data.Bytes[15:21] + idm.TamperCounters = append([]byte(nil), data.Bytes[15:21]...) idm.AsynchronousCounters = binary.BigEndian.Uint16(data.Bytes[21:23]) - idm.PowerOutageFlags = data.Bytes[23:29] + idm.PowerOutageFlags = append([]byte(nil), data.Bytes[23:29]...) idm.LastConsumptionCount = binary.BigEndian.Uint32(data.Bytes[29:33]) offset := 264 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/main.go new/rtlamr-0.9.6/main.go --- old/rtlamr-0.9.5/main.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/main.go 2026-10-31 23:50:59.000000000 +0100 @@ -19,6 +19,7 @@ import ( "bytes" "context" + "errors" "flag" "fmt" "io" @@ -32,10 +33,9 @@ "sync" "time" - "github.com/pkg/errors" - "github.com/bemasher/rtlamr/protocol" "github.com/bemasher/rtltcp" + "github.com/bemasher/rtltcp/si" _ "github.com/bemasher/rtlamr/idm" _ "github.com/bemasher/rtlamr/netidm" @@ -47,20 +47,25 @@ var rcvr Receiver +// errSingleDone stops the receiver once -single has heard from every meter. +// It is a normal exit, not a failure. +var errSingleDone = errors.New("single: received messages from all meters") + type Receiver struct { rtltcp.SDR + flags SdrFlags + d protocol.Decoder fc protocol.FilterChain - ctx context.Context - cancel context.CancelFunc - wg *sync.WaitGroup - - err error + ctx context.Context + canc context.CancelCauseFunc + wg *sync.WaitGroup } -func (rcvr *Receiver) NewReceiver() { - rcvr.ctx, rcvr.cancel = context.WithCancel(context.Background()) +func (rcvr *Receiver) NewReceiver(ctx context.Context) { + rcvr.ctx, rcvr.canc = context.WithCancelCause(ctx) + rcvr.wg = &sync.WaitGroup{} rcvr.d = protocol.NewDecoder() @@ -78,7 +83,8 @@ for name := range msgType { p, err := protocol.NewParser(name, *symbolLength) if err != nil { - slog.Error("message type", "error", err) + rcvr.canc(fmt.Errorf("protocol.NewParser: %w", err)) + return } rcvr.d.RegisterProtocol(p) @@ -88,9 +94,14 @@ rcvr.d.Allocate() // Connect to rtl_tcp server. - if rcvr.err = rcvr.Connect(nil); rcvr.err != nil { - slog.Error("receiver connect", "error", errors.Wrap(rcvr.err, "rcvr.Connect")) - os.Exit(1) + if err := rcvr.Connect(nil); err != nil { + rcvr.canc(fmt.Errorf("rcvr.Connect: %w", err)) + return + } + + if err := rcvr.HandleFlags(); err != nil { + rcvr.canc(fmt.Errorf("rcvr.HandleFlags: %w", err)) + return } cfg := rcvr.d.Cfg @@ -99,9 +110,9 @@ flag.Visit(func(f *flag.Flag) { switch f.Name { case "centerfreq": - cfg.CenterFreq = uint32(rcvr.Flags.CenterFreq) + cfg.CenterFreq = uint32(rcvr.flags.CenterFreq) case "samplerate": - cfg.SampleRate = int(rcvr.Flags.SampleRate) + cfg.SampleRate = int(rcvr.flags.SampleRate) case "gainbyindex", "tunergainmode", "tunergain", "agcmode": gainFlagSet = true case "unique": @@ -129,8 +140,90 @@ slog.Info("rtl_tcp", "GainCount", rcvr.SDR.Info.GainCount) } +type SdrFlags struct { + ServerAddr string + CenterFreq si.ScientificNotation + SampleRate si.ScientificNotation + TunerGainMode bool + TunerGain float64 + FreqCorrection int + TestMode bool + AgcMode bool + DirectSampling bool + OffsetTuning bool + RtlXtalFreq uint + TunerXtalFreq uint + GainByIndex uint +} + +// Registers command line flags for rtltcp commands. +func (rcvr *Receiver) RegisterFlags() { + flag.StringVar(&rcvr.flags.ServerAddr, "server", "127.0.0.1:1234", "address or hostname of rtl_tcp instance") + flag.Var(&rcvr.flags.CenterFreq, "centerfreq", "center frequency to receive on") + flag.Lookup("centerfreq").DefValue = "100M" + flag.Var(&rcvr.flags.SampleRate, "samplerate", "sample rate") + flag.Lookup("samplerate").DefValue = "2.4M" + flag.BoolVar(&rcvr.flags.TunerGainMode, "tunergainmode", false, "enable/disable tuner gain") + flag.Float64Var(&rcvr.flags.TunerGain, "tunergain", 0.0, "set tuner gain in dB") + flag.IntVar(&rcvr.flags.FreqCorrection, "freqcorrection", 0, "frequency correction in ppm") + flag.BoolVar(&rcvr.flags.TestMode, "testmode", false, "enable/disable test mode") + flag.BoolVar(&rcvr.flags.AgcMode, "agcmode", false, "enable/disable rtl agc") + flag.BoolVar(&rcvr.flags.DirectSampling, "directsampling", false, "enable/disable direct sampling") + flag.BoolVar(&rcvr.flags.OffsetTuning, "offsettuning", false, "enable/disable offset tuning") + flag.UintVar(&rcvr.flags.RtlXtalFreq, "rtlxtalfreq", 0, "set rtl xtal frequency") + flag.UintVar(&rcvr.flags.TunerXtalFreq, "tunerxtalfreq", 0, "set tuner xtal frequency") + flag.UintVar(&rcvr.flags.GainByIndex, "gainbyindex", 0, "set gain by index") +} + +// Parses flags and executes commands associated with each flag. Should only +// be called once connected to rtl_tcp. +func (rcvr *Receiver) HandleFlags() (err error) { + // Catch any errors panicked while visiting flags. + defer func() { + if r := recover(); r != nil { + err = r.(error) + } + }() + + flag.CommandLine.Visit(func(f *flag.Flag) { + var err error + switch f.Name { + case "centerfreq": + err = rcvr.SetCenterFreq(uint32(rcvr.flags.CenterFreq)) + case "samplerate": + err = rcvr.SetSampleRate(uint32(rcvr.flags.SampleRate)) + case "tunergainmode": + err = rcvr.SetGainMode(rcvr.flags.TunerGainMode) + case "tunergain": + err = rcvr.SetGain(uint32(rcvr.flags.TunerGain * 10.0)) + case "freqcorrection": + err = rcvr.SetFreqCorrection(int32(rcvr.flags.FreqCorrection)) + case "testmode": + err = rcvr.SetTestMode(rcvr.flags.TestMode) + case "agcmode": + err = rcvr.SetAGCMode(rcvr.flags.AgcMode) + case "directsampling": + err = rcvr.SetDirectSampling(rcvr.flags.DirectSampling) + case "offsettuning": + err = rcvr.SetOffsetTuning(rcvr.flags.OffsetTuning) + case "rtlxtalfreq": + err = rcvr.SetRTLXtalFreq(uint32(rcvr.flags.RtlXtalFreq)) + case "tunerxtalfreq": + err = rcvr.SetTunerXtalFreq(uint32(rcvr.flags.TunerXtalFreq)) + case "gainbyindex": + err = rcvr.SetGainByIndex(uint32(rcvr.flags.GainByIndex)) + } + + // If we encounter an error, panic to catch in parent scope. + if err != nil { + panic(err) + } + }) + + return +} + func (rcvr *Receiver) Close() { - rcvr.cancel() rcvr.wg.Wait() rcvr.SDR.Close() } @@ -157,7 +250,7 @@ // Read and send sample blocks to the decoder. go func() { - defer rcvr.cancel() + defer rcvr.canc(nil) defer close(blockCh) defer rcvr.wg.Done() @@ -168,9 +261,9 @@ for { block := make([]byte, rcvr.d.Cfg.BlockSize2) - rcvr.err = rcvr.SetDeadline(time.Now().Add(5 * time.Second)) - if rcvr.err != nil { - rcvr.err = errors.Wrap(rcvr.err, "rcvr.SetDeadline") + err := rcvr.SetDeadline(time.Now().Add(5 * time.Second)) + if err != nil { + rcvr.canc(fmt.Errorf("rcvr.SetDeadline: %w", err)) return } @@ -178,9 +271,9 @@ for offset := 0; offset < len(block); { var n int - n, rcvr.err = rcvr.Read(block[offset:]) - if rcvr.err != nil { - rcvr.err = errors.Wrap(rcvr.err, "rcvr.Read") + n, err = rcvr.Read(block[offset:]) + if err != nil { + rcvr.canc(fmt.Errorf("rcvr.Read: %w", err)) return } @@ -190,8 +283,9 @@ select { case <-tick: - if bytesRead>>1 < rcvr.d.Cfg.SampleRate { - slog.Error("not keeping up with rtl_tcp", "rate", bytesRead>>1) + // Complain if received samples are less than 90% configured rate. + if bytesRead>>1 < (rcvr.d.Cfg.SampleRate * 9 / 10) { + slog.Warn("not keeping up with rtl_tcp", "rate", bytesRead>>1) } bytesRead = 0 default: @@ -207,7 +301,7 @@ }() go func() { - defer rcvr.cancel() + defer rcvr.canc(nil) defer rcvr.wg.Done() for { @@ -262,10 +356,9 @@ } // Encode the message - rcvr.err = encoder.Encode(logMsg) - rcvr.err = errors.Wrap(rcvr.err, "encoder.Encode") - - if rcvr.err != nil { + err := encoder.Encode(logMsg) + if err != nil { + rcvr.canc(fmt.Errorf("encoder.Encode: %w", err)) return } @@ -286,7 +379,7 @@ os.Exit(1) } if *single && len(meterID.UintMap) == 0 { - rcvr.cancel() + rcvr.canc(errSingleDone) return } } @@ -304,7 +397,6 @@ if ok { dir = filepath.Dir(file) dir = filepath.Dir(dir) + string(filepath.Separator) - fmt.Println(dir) } slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, @@ -321,11 +413,17 @@ } func main() { + os.Exit(run()) +} + +// run is main's body. Returning the exit code (rather than calling os.Exit +// here) lets the deferred cleanup below finish first, and lets a panic crash +// with its stack trace instead of being masked by an exit status. +func run() int { rcvr.RegisterFlags() RegisterFlags() EnvOverride() flag.Parse() - rcvr.HandleFlags() if *version { if info, ok := debug.ReadBuildInfo(); ok { @@ -333,46 +431,68 @@ } else { slog.Error("could not read build info") } - os.Exit(0) + return 0 } HandleFlags() - rcvr.NewReceiver() + // Setup signal channel for interruption. + ctx, canc := signal.NotifyContext(context.Background(), os.Interrupt) + defer canc() + // Make sure sampleWriter is closed. defer func() { if c, ok := sampleWriter.(io.Closer); ok { + slog.Info("closing sampleWriter") c.Close() } - rcvr.Close() - - if rcvr.err != nil { - slog.Error("receiver", "error", rcvr.err) - os.Exit(1) - } }() - start := time.Now() - rcvr.Run() - - // Setup signal channel for interruption. - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt) - - // Setup time limit channel - timeLimitCh := make(<-chan time.Time, 1) + // Set context timeout when provided. if *timeLimit != 0 { - timeLimitCh = time.After(*timeLimit) + ctx, canc = context.WithTimeout(ctx, *timeLimit) + defer canc() + } + + rcvr.NewReceiver(ctx) + if rcvr.ctx.Err() != nil { + slog.Error("rcvr.NewReceiver", "error", context.Cause(rcvr.ctx)) + return 1 } + defer rcvr.Close() + + rcvr.HandleFlags() + + rcvr.Run() select { - case sig := <-sigCh: - slog.Info("signal received", "signal", sig) - case <-timeLimitCh: - slog.Info("time limit reached:", "since", time.Since(start)) + case <-ctx.Done(): + err := context.Cause(ctx) + slog.Info("main context cancelled", "cause", err) case <-rcvr.ctx.Done(): - slog.Info("receiver context cancelled") + err := context.Cause(rcvr.ctx) + slog.Info("receiver context cancelled", "cause", err) + } + + if err := exitError(ctx, rcvr.ctx); err != nil { + slog.Error("receiver stopped", "error", err) + return 1 } + return 0 +} - rcvr.Close() +// exitError returns the error that stopped the receiver, or nil if it stopped +// normally: interrupted, -duration elapsed, or -single heard from every meter. +func exitError(mainCtx, rcvrCtx context.Context) error { + cause := context.Cause(rcvrCtx) + switch { + case cause == nil, + errors.Is(cause, context.Canceled), + errors.Is(cause, errSingleDone): + return nil + case mainCtx.Err() != nil && cause == context.Cause(mainCtx): + // Inherited from main's context: interrupt or -duration. + return nil + } + return cause } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/protocol/decode.go new/rtlamr-0.9.6/protocol/decode.go --- old/rtlamr-0.9.5/protocol/decode.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/protocol/decode.go 2026-10-31 23:50:59.000000000 +0100 @@ -41,34 +41,15 @@ CenterFreq uint32 } -func (d Decoder) Log() { - log.Println("CenterFreq:", d.Cfg.CenterFreq) - log.Println("SampleRate:", d.Cfg.SampleRate) - log.Println("DataRate:", d.Cfg.DataRate) - log.Println("ChipLength:", d.Cfg.ChipLength) - log.Println("PreambleSymbols:", d.Cfg.PreambleSymbols) - log.Println("PreambleLength:", d.Cfg.PreambleLength) - log.Println("PacketSymbols:", d.Cfg.PacketSymbols) - log.Println("PacketLength:", d.Cfg.PacketLength) - - var preambles []string - for preamble, _ := range d.preambleStrs { - preambles = append(preambles, preamble) - } - - log.Println("Protocols:", strings.Join(d.protocols, ",")) - log.Println("Preambles:", strings.Join(preambles, ",")) -} - // Decoder contains buffers and radio configuration. type Decoder struct { Cfg PacketConfig wg *sync.WaitGroup - Signal []float64 + Signal []float32 Quantized []byte - csum []float64 + csum []float32 demod Demodulator preambleStrs map[string]bool @@ -89,6 +70,25 @@ } } +func (d Decoder) Log() { + log.Println("CenterFreq:", d.Cfg.CenterFreq) + log.Println("SampleRate:", d.Cfg.SampleRate) + log.Println("DataRate:", d.Cfg.DataRate) + log.Println("ChipLength:", d.Cfg.ChipLength) + log.Println("PreambleSymbols:", d.Cfg.PreambleSymbols) + log.Println("PreambleLength:", d.Cfg.PreambleLength) + log.Println("PacketSymbols:", d.Cfg.PacketSymbols) + log.Println("PacketLength:", d.Cfg.PacketLength) + + var preambles []string + for preamble := range d.preambleStrs { + preambles = append(preambles, preamble) + } + + log.Println("Protocols:", strings.Join(d.protocols, ",")) + log.Println("Preambles:", strings.Join(preambles, ",")) +} + func max(a, b int) int { if a > b { return a @@ -141,10 +141,10 @@ d.Cfg.BufferLength = d.Cfg.PacketLength + d.Cfg.BlockSize // Allocate necessary buffers. - d.Signal = make([]float64, d.Cfg.BlockSize+d.Cfg.SymbolLength) + d.Signal = make([]float32, d.Cfg.BlockSize+d.Cfg.SymbolLength) d.Quantized = make([]byte, d.Cfg.BufferLength) - d.csum = make([]float64, len(d.Signal)+1) + d.csum = make([]float32, len(d.Signal)+1) // Calculate magnitude lookup table specified by -fastmag flag. d.demod = NewMagLUT() @@ -157,8 +157,6 @@ d.sIdxB = make([]int, 0, d.Cfg.BlockSize) d.packed = make([]byte, (d.Cfg.BlockSize+d.Cfg.PreambleLength+7)>>3) - - return } // Decode accepts a sample block and returns a channel of messages. @@ -199,26 +197,26 @@ } // A Demodulator knows how to demodulate an array of uint8 IQ samples into an -// array of float64 samples. +// array of float32 samples. type Demodulator interface { - Execute([]byte, []float64) + Execute([]byte, []float32) } // Default Magnitude Lookup Table -type MagLUT []float64 +type MagLUT []float32 // Pre-computes normalized squares with most common DC offset for rtl-sdr dongles. func NewMagLUT() (lut MagLUT) { - lut = make([]float64, 0x100) + lut = make([]float32, 0x100) for idx := range lut { - lut[idx] = (127.5 - float64(idx)) / 127.5 + lut[idx] = (127.5 - float32(idx)) / 127.5 lut[idx] *= lut[idx] } return } // Calculates complex magnitude on given IQ stream writing result to output. -func (lut MagLUT) Execute(input []byte, output []float64) { +func (lut MagLUT) Execute(input []byte, output []float32) { i := 0 for idx := range output { output[idx] = lut[input[i]] + lut[input[i+1]] @@ -228,10 +226,10 @@ // Matched filter for Manchester coded signals. Output signal's sign at each // sample determines the bit-value due to Manchester symbol odd symmetry. -func (d Decoder) Filter(input []float64, output []byte) { +func (d Decoder) Filter(input []float32, output []byte) { // Computing the cumulative summation over the signal simplifies // filtering to the difference of a pair of subtractions. - var sum float64 + var sum float32 for idx, v := range input { sum += v d.csum[idx+1] = sum @@ -242,20 +240,18 @@ upper := d.csum[d.Cfg.SymbolLength:] for idx, l := range lower[:len(output)] { f := (l - d.csum[idx]) - (upper[idx] - l) - output[idx] = 1 - byte(math.Float64bits(f)>>63) + output[idx] = 1 - byte(math.Float32bits(f)>>31) } - - return } // Return a list of indices into the quantized signal at which a valid preamble // exists. -// 1. Pack the quantized signal into bytes. -// 2. Build a list of indices by eliminating bytes that contain no bits matching -// the first bit of the preamble. -// 3. Continue eliminating indices at which the preamble cannot exist. -// 4. Convert indices from byte-based to sample-based. -// 5. Check each of these indices for the preamble. +// 1. Pack the quantized signal into bytes. +// 2. Build a list of indices by eliminating bytes that contain no bits matching +// the first bit of the preamble. +// 3. Continue eliminating indices at which the preamble cannot exist. +// 4. Convert indices from byte-based to sample-based. +// 5. Check each of these indices for the preamble. func (d *Decoder) Search(preamble []byte) []int { symLenByte := d.Cfg.SymbolLength >> 3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/protocol/parse.go new/rtlamr-0.9.6/protocol/parse.go --- old/rtlamr-0.9.5/protocol/parse.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/protocol/parse.go 2026-10-31 23:50:59.000000000 +0100 @@ -21,7 +21,7 @@ type NewParserFunc func(symbolLength int) Parser // Given a name and a parser, register a parser for use. -// Later used by underscore improting each parser package: +// Later used by underscore importing each parser package: // // import _ "github.com/bemasher/rtlamr/scm" // diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/r900/r900.go new/rtlamr-0.9.6/r900/r900.go --- old/rtlamr-0.9.5/r900/r900.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/r900/r900.go 2026-10-31 23:50:59.000000000 +0100 @@ -19,7 +19,6 @@ import ( "bytes" "fmt" - "math" "strconv" "sync" @@ -36,7 +35,6 @@ } func NewPacketConfig(chipLength int) (cfg protocol.PacketConfig) { - return } @@ -46,9 +44,8 @@ field *gf.Field rsBuf [31]byte - signal []float64 - csum []float64 - filtered [][3]float64 + signal []float32 + csum []float32 quantized []byte once sync.Once @@ -77,12 +74,12 @@ p.Decoder = d } -func (p Parser) Cfg() protocol.PacketConfig { +func (p *Parser) Cfg() protocol.PacketConfig { return p.cfg } // Perform matched filtering. -func (p Parser) filter() { +func (p *Parser) filter() { // This function computes the convolution of each symbol kernel with the // signal. The naive approach requires for each symbol to calculate the // summation of samples between a pair of indices. @@ -96,7 +93,7 @@ // signal. This reduces each summation to the difference between the two // indices of the cumulative sum. - var sum float64 + var sum float32 for idx, v := range p.signal { sum += v p.csum[idx+1] = sum @@ -114,58 +111,57 @@ // simplification but is necessary for efficiency. cfg := p.Decoder.Cfg - for idx := 0; idx < cfg.BufferLength-cfg.ChipLength*4; idx++ { - c0 := p.csum[idx] - c1 := p.csum[idx+cfg.ChipLength] * 2 - c2 := p.csum[idx+cfg.ChipLength*2] * 2 - c3 := p.csum[idx+cfg.ChipLength*3] * 2 - c4 := p.csum[idx+cfg.ChipLength*4] - - p.filtered[idx][0] = c2 - c4 - c0 // 1100 - p.filtered[idx][1] = c1 - c2 + c3 - c4 - c0 // 1010 - p.filtered[idx][2] = c1 - c3 + c4 - c0 // 1001 - } -} + cl := cfg.ChipLength + cl2 := cl * 2 + cl3 := cl * 3 + cl4 := cl * 4 + + limit := cfg.BufferLength - cl4 -// Determine the symbol that exists at each sample of the signal. -func (p Parser) quantize() { - // 0 0011, 3 1100 - // 1 0101, 4 1010 - // 2 0110, 5 1001 + for idx := 0; idx < limit; idx++ { + c0 := p.csum[idx] + c1 := p.csum[idx+cl] + p.csum[idx+cl] + c2 := p.csum[idx+cl2] + p.csum[idx+cl2] + c3 := p.csum[idx+cl3] + p.csum[idx+cl3] + c4 := p.csum[idx+cl4] + + a0 := c2 - c4 - c0 // 1100 + a1 := c1 - c2 + c3 - c4 - c0 // 1010 + a2 := c1 - c3 + c4 - c0 // 1001 - for idx, vec := range p.filtered { + // Quantize in the same loop + maxAbs := abs(a0) argmax := byte(0) - max := math.Abs(vec[0]) + if abs(a1) > maxAbs { - // If v1 is larger than v0, update max and argmax. - if v1 := math.Abs(vec[1]); v1 > max { - max = v1 + maxAbs = abs(a1) argmax = 1 } - - // If v2 is larger than the greater of v1 or v0, update max and argmax. - if v2 := math.Abs(vec[2]); v2 > max { - max = v2 + if abs(a2) > maxAbs { + maxAbs = abs(a2) argmax = 2 } - // Set the output symbol index. p.quantized[idx] = argmax - - // If the sign is negative, jump to the index of the inverted symbol. - if vec[argmax] > 0 { + if [3]float32{a0, a1, a2}[argmax] > 0 { p.quantized[idx] += 3 } } } +func abs[F float32 | float64](x F) F { + if x < 0 { + return -x + } + return x +} + // Given a list of indices the preamble exists at, decode and parse a message. func (p *Parser) Parse(pkts []protocol.Data, msgCh chan protocol.Message, wg *sync.WaitGroup) { p.once.Do(func() { p.cfg = p.Decoder.Cfg - p.signal = make([]float64, p.Decoder.Cfg.BufferLength) - p.csum = make([]float64, p.Decoder.Cfg.BufferLength+1) - p.filtered = make([][3]float64, p.Decoder.Cfg.BufferLength) + p.signal = make([]float32, p.Decoder.Cfg.BufferLength) + p.csum = make([]float32, p.Decoder.Cfg.BufferLength+1) p.quantized = make([]byte, p.Decoder.Cfg.BufferLength) }) @@ -174,7 +170,6 @@ copy(p.signal[cfg.PacketLength:], p.Decoder.Signal[cfg.SymbolLength:]) p.filter() - p.quantize() preambleLength := cfg.PreambleLength chipLength := cfg.ChipLength diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/scmplus/scmplus.go new/rtlamr-0.9.6/scmplus/scmplus.go --- old/rtlamr-0.9.5/scmplus/scmplus.go 2026-04-25 22:35:30.000000000 +0200 +++ new/rtlamr-0.9.6/scmplus/scmplus.go 2026-10-31 23:50:59.000000000 +0100 @@ -99,7 +99,7 @@ EndpointID uint32 `xml:",attr"` Consumption uint32 `xml:",attr"` Tamper uint16 `xml:",attr"` - PacketCRC uint16 `xml:"Checksum,attr",json:"Checksum"` + PacketCRC uint16 `xml:"Checksum,attr"` } func NewSCM(data protocol.Data) (scm SCM) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/vendor/github.com/bemasher/rtltcp/README.md new/rtlamr-0.9.6/vendor/github.com/bemasher/rtltcp/README.md --- old/rtlamr-0.9.5/vendor/github.com/bemasher/rtltcp/README.md 1970-01-01 01:00:00.000000000 +0100 +++ new/rtlamr-0.9.6/vendor/github.com/bemasher/rtltcp/README.md 2026-10-31 23:50:59.000000000 +0100 @@ -0,0 +1,7 @@ +## rtltcp + +Go wrapper for the TCP protocol implemented by the `rtl_tcp` tool for Realtek DVB-T based SDR's by OsmoSDR. More documentation to come. + +## Documentation + +[](https://godoc.org/github.com/bemasher/rtltcp) \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/vendor/github.com/bemasher/rtltcp/rtltcp.go new/rtlamr-0.9.6/vendor/github.com/bemasher/rtltcp/rtltcp.go --- old/rtlamr-0.9.5/vendor/github.com/bemasher/rtltcp/rtltcp.go 1970-01-01 01:00:00.000000000 +0100 +++ new/rtlamr-0.9.6/vendor/github.com/bemasher/rtltcp/rtltcp.go 2026-10-31 23:50:59.000000000 +0100 @@ -0,0 +1,210 @@ +// This package provides a wrapper for the TCP protocol implemented by the rtl_tcp tool used with Realtek DVB-T based SDR's. +package rtltcp + +import ( + "encoding/binary" + "fmt" + "net" +) + +var magic = [...]byte{'R', 'T', 'L', '0'} + +// Contains dongle information and an embedded tcp connection to the spectrum server +type SDR struct { + *net.TCPConn + Info Info +} + +// Give an address of the form "127.0.0.1:1234" connects to the spectrum +// server at the given address or returns an error. The user is responsible +// for closing this connection. If addr is nil, use "127.0.0.1:1234" or +// command line flag value. +func (sdr *SDR) Connect(addr *net.TCPAddr) (err error) { + if addr == nil { + // Parse and resolve rtl_tcp server address. + addr, err = net.ResolveTCPAddr("tcp", "127.0.0.1:1234") + if err != nil { + return + } + } + + sdr.TCPConn, err = net.DialTCP("tcp", nil, addr) + if err != nil { + err = fmt.Errorf("net.DialTCP: %s", err) + return + } + + // If we exit this function due to an error, close the connection. + defer func() { + if err != nil { + sdr.Close() + } + }() + + err = binary.Read(sdr.TCPConn, binary.BigEndian, &sdr.Info) + if err != nil { + err = fmt.Errorf("error reading dongle information: %s", err) + return + } + + if !sdr.Info.Valid() { + err = fmt.Errorf("invalid magic number: expected %q received %q", magic, sdr.Info.Magic) + } + + return +} + +// Contains the Magic number, tuner information and the number of valid gain values. +type Info struct { + Magic [4]byte + Tuner Tuner + GainCount uint32 // Useful for setting gain by index +} + +func (d Info) String() string { + return fmt.Sprintf("SDR{Magic:%q Tuner:%s GainCount:%d}", d.Magic, d.Tuner, d.GainCount) +} + +// Checks that the magic number received matches the expected byte string 'RTL0'. +func (d Info) Valid() bool { + return d.Magic == magic +} + +// Provides mapping of tuner value to tuner string. +type Tuner uint32 + +func (t Tuner) String() string { + switch t { + case 1: + return "E4000" + case 2: + return "FC0012" + case 3: + return "FC0013" + case 4: + return "FC2580" + case 5: + return "R820T" + case 6: + return "R828D" + } + return "UNKNOWN" +} + +func (sdr SDR) execute(cmd command) (err error) { + return binary.Write(sdr.TCPConn, binary.BigEndian, cmd) +} + +type command struct { + Command uint8 + Parameter uint32 +} + +// Command constants defined in rtl_tcp.c +const ( + centerFreq = 0x01 + sampleRate = 0x02 + tunerGainMode = 0x03 + tunerGain = 0x04 + freqCorrection = 0x05 + tunerIfGain = 0x06 + testMode = 0x07 + agcMode = 0x08 + directSampling = 0x09 + offsetTuning = 0x0A + rtlXtalFreq = 0x0B + tunerXtalFreq = 0x0C + gainByIndex = 0x0D + biasTee = 0x0E +) + +// Set the center frequency in Hz. +func (sdr SDR) SetCenterFreq(freq uint32) (err error) { + return sdr.execute(command{centerFreq, freq}) +} + +// Set the sample rate in Hz. +func (sdr SDR) SetSampleRate(rate uint32) (err error) { + return sdr.execute(command{sampleRate, rate}) +} + +// Set gain in tenths of dB. (197 => 19.7dB) +func (sdr SDR) SetGain(gain uint32) (err error) { + return sdr.execute(command{tunerGain, gain}) +} + +// Set gain mode, true enables manual gain and false enables automatic gain. +func (sdr SDR) SetGainMode(manual bool) (err error) { + if manual { + return sdr.execute(command{tunerGainMode, 1}) + } + return sdr.execute(command{tunerGainMode, 0}) +} + +// Set gain by index, must be < DongleInfo.GainCount. +func (sdr SDR) SetGainByIndex(idx uint32) (err error) { + if idx >= sdr.Info.GainCount { + return fmt.Errorf("invalid gain index: %d", idx) + } + return sdr.execute(command{gainByIndex, idx}) +} + +// Set frequency correction in ppm. +func (sdr SDR) SetFreqCorrection(ppm int32) (err error) { + return sdr.execute(command{freqCorrection, uint32(ppm)}) +} + +// Set tuner intermediate frequency stage and gain in tenths of a dB (-30 means -3.0dB). +func (sdr SDR) SetTunerIfGain(stage, gain int16) (err error) { + return sdr.execute(command{tunerIfGain, (uint32(stage) << 16) | uint32(uint16(gain))}) +} + +// Set test mode, true for enabled. +func (sdr SDR) SetTestMode(state bool) (err error) { + if state { + return sdr.execute(command{testMode, 1}) + } + return sdr.execute(command{testMode, 0}) +} + +// Set RTL AGC mode, true for enabled. +func (sdr SDR) SetAGCMode(state bool) (err error) { + if state { + return sdr.execute(command{agcMode, 1}) + } + return sdr.execute(command{agcMode, 0}) +} + +// Set direct sampling mode. +func (sdr SDR) SetDirectSampling(state bool) (err error) { + if state { + return sdr.execute(command{directSampling, 1}) + } + return sdr.execute(command{directSampling, 0}) +} + +// Set offset tuning, true for enabled. +func (sdr SDR) SetOffsetTuning(state bool) (err error) { + if state { + return sdr.execute(command{offsetTuning, 1}) + } + return sdr.execute(command{offsetTuning, 0}) +} + +// Set bias tee, true for enabled. +func (sdr SDR) SetBiasTee(state bool) (err error) { + if state { + return sdr.execute(command{biasTee, 1}) + } + return sdr.execute(command{biasTee, 0}) +} + +// Set RTL xtal frequency. +func (sdr SDR) SetRTLXtalFreq(freq uint32) (err error) { + return sdr.execute(command{rtlXtalFreq, freq}) +} + +// Set tuner xtal frequency. +func (sdr SDR) SetTunerXtalFreq(freq uint32) (err error) { + return sdr.execute(command{tunerXtalFreq, freq}) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/vendor/github.com/bemasher/rtltcp/si/si.go new/rtlamr-0.9.6/vendor/github.com/bemasher/rtltcp/si/si.go --- old/rtlamr-0.9.5/vendor/github.com/bemasher/rtltcp/si/si.go 1970-01-01 01:00:00.000000000 +0100 +++ new/rtlamr-0.9.6/vendor/github.com/bemasher/rtltcp/si/si.go 2026-10-31 23:50:59.000000000 +0100 @@ -0,0 +1,99 @@ +package si + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +const ( + Y = 1e24 + Z = 1e21 + E = 1e18 + P = 1e15 + T = 1e12 + G = 1e9 + M = 1e6 + k = 1e3 + m = 1e-3 + u = 1e-6 + n = 1e-9 + p = 1e-12 + f = 1e-15 + a = 1e-18 + z = 1e-21 + y = 1e-24 +) + +type ScientificNotation float64 + +func (si ScientificNotation) String() (s string) { + return strconv.FormatFloat(float64(si), 'g', -1, 64) +} + +func (si *ScientificNotation) Set(value string) error { + // Trim all non-digits from the right, to remove suffix. + mantissaStr := strings.TrimRightFunc(value, func(r rune) bool { + return !unicode.IsDigit(r) + }) + + // Recover the suffix by trimming the prefix. + suffix := strings.TrimPrefix(value, mantissaStr) + + // Parse mantissa. + mantissa, err := strconv.ParseFloat(mantissaStr, 64) + if err != nil { + return fmt.Errorf("strconv.ParseFloat: %w", err) + } + + // Set value. + *si = ScientificNotation(mantissa) + + // Apply suffix. + for i, s := range suffix { + // Ignore everything after first suffix. + if i > 0 { + return nil + } + + switch s { + case 'Y': + *si *= Y + case 'Z': + *si *= Z + case 'E': + *si *= E + case 'P': + *si *= P + case 'T': + *si *= T + case 'G': + *si *= G + case 'M': + *si *= M + case 'k': + *si *= k + case 'm': + *si *= m + case 'u': + *si *= u + case 'n': + *si *= n + case 'p': + *si *= p + case 'f': + *si *= f + case 'a': + *si *= a + case 'z': + *si *= z + case 'y': + *si *= y + default: + return fmt.Errorf("invalid suffix: %q", suffix) + } + } + + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rtlamr-0.9.5/vendor/modules.txt new/rtlamr-0.9.6/vendor/modules.txt --- old/rtlamr-0.9.5/vendor/modules.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/rtlamr-0.9.6/vendor/modules.txt 2026-10-31 23:50:59.000000000 +0100 @@ -0,0 +1,4 @@ +# github.com/bemasher/rtltcp v0.0.0-20261031224328-8792921d55f4 +## explicit; go 1.13 +github.com/bemasher/rtltcp +github.com/bemasher/rtltcp/si ++++++ vendor.tar.gz ++++++ ++++ 2620 lines of diff (skipped)
