From 760d293044d538f0d02df91ecb3bb397fd065914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Thu, 10 Oct 2024 22:40:29 +0200 Subject: [PATCH] Add debouncer --- .gitignore | 1 + debouncer/debouncer.go | 53 ++++++++++++++++++++++ debouncer/debouncer_test.go | 89 +++++++++++++++++++++++++++++++++++++ flake.nix | 64 ++++++++++++++++++-------- go.mod | 16 ++++--- go.sum | 18 ++++++++ main.go | 51 +++++++++++++++------ 7 files changed, 254 insertions(+), 38 deletions(-) create mode 100644 .gitignore create mode 100644 debouncer/debouncer.go create mode 100644 debouncer/debouncer_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2be92b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result diff --git a/debouncer/debouncer.go b/debouncer/debouncer.go new file mode 100644 index 0000000..9d6a1b6 --- /dev/null +++ b/debouncer/debouncer.go @@ -0,0 +1,53 @@ +package debouncer + +import "time" + +type Debouncer struct { + input <-chan bool + C <-chan bool +} + +func New(ch <-chan bool) *Debouncer { + output := make(chan bool) + var currentState bool + var lastState bool + falseTimer := time.NewTimer(0) + falseTimer.Stop() + go func() { + for { + select { + case <-falseTimer.C: + if !currentState && lastState { + output <- false + lastState = false + } else { + falseTimer.Reset(200 * time.Millisecond) + } + case v, ok := <-ch: + if !ok { + falseTimer.Reset(200 * time.Millisecond) + <-falseTimer.C + output <- false + close(output) + return + } + if v { + if !lastState { + output <- true + lastState = true + falseTimer.Reset(200 * time.Millisecond) + } + + currentState = true + } else { + falseTimer.Reset(200 * time.Millisecond) + + currentState = false + } + } + } + }() + d := &Debouncer{input: ch, C: output} + + return d +} diff --git a/debouncer/debouncer_test.go b/debouncer/debouncer_test.go new file mode 100644 index 0000000..550ece8 --- /dev/null +++ b/debouncer/debouncer_test.go @@ -0,0 +1,89 @@ +package debouncer_test + +import ( + "testing" + "time" + + "git.t-juice.club/torjus/ghettoptt/debouncer" +) + +func TestDebouncer(t *testing.T) { + t.Run("Simple", func(t *testing.T) { + ch := make(chan bool) + d := debouncer.New(ch) + + go func() { + ch <- true + close(ch) + }() + + if v := <-d.C; v != true { + t.Errorf("Expected true, got %v", v) + } + }) + t.Run("TrueTrueTrueFalse", func(t *testing.T) { + ch := make(chan bool) + d := debouncer.New(ch) + go func() { + ch <- true + ch <- true + ch <- true + ch <- false + close(ch) + }() + if v := <-d.C; v != true { + t.Errorf("Expected true, got %v", v) + } + if v := <-d.C; v != false { + t.Errorf("Expected false, got %v", v) + } + }) + t.Run("Debounce", func(t *testing.T) { + ch := make(chan bool) + d := debouncer.New(ch) + go func() { + ch <- true + time.Sleep(10 * time.Millisecond) + ch <- false + time.Sleep(1 * time.Millisecond) + ch <- true + time.Sleep(1 * time.Millisecond) + ch <- false + time.Sleep(10 * time.Millisecond) + close(ch) + }() + if v := <-d.C; v != true { + t.Errorf("Expected first value to be true, got %v", v) + } + if v := <-d.C; v != false { + t.Errorf("Expected second value to be false, got %v", v) + } + + if v, ok := <-d.C; ok { + t.Errorf("Expected closed channel, got %v", v) + } + }) + t.Run("DebounceDelay", func(t *testing.T) { + ch := make(chan bool) + d := debouncer.New(ch) + go func() { + ch <- true + ch <- false + close(ch) + }() + start := time.Now() + if v := <-d.C; v != true { + t.Errorf("Expected first value to be true, got %v", v) + } + if v, ok := <-d.C; v != false || !ok { + if ok { + t.Errorf("Expected second value to be false, got %v", v) + } else { + t.Error("Unexpected closed channel") + } + } + if duration := time.Since(start); duration < 200*time.Millisecond { + t.Errorf("Got false too soon: %s", duration) + } + }) +} diff --git a/flake.nix b/flake.nix index 42237bf..8ede2c1 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,8 @@ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - outputs = { self, nixpkgs }: + outputs = + { self, nixpkgs }: let allSystems = [ "x86_64-linux" @@ -11,28 +12,55 @@ "x86_64-darwin" "aarch64-darwin" ]; - forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f { - pkgs = import nixpkgs { inherit system; }; - }); + forAllSystems = + f: + nixpkgs.lib.genAttrs allSystems ( + system: + f { + pkgs = import nixpkgs { inherit system; }; + } + ); in { overlays.default = final: prev: { ghettoptt = self.packages.${prev.system}.default; }; - packages = forAllSystems ({ pkgs }: { - default = pkgs.buildGoModule { - name = "ghettoptt"; - src = ./.; - vendorHash = "sha256-WhlifAFIqoBNhBWGmYgHc5DWmdhHnK/2kYbEF/DOclw="; - }; - }); - devShells = forAllSystems ({ pkgs }: { - default = pkgs.mkShell { - packages = with pkgs; [ - go - ]; - }; - }); + packages = forAllSystems ( + { pkgs }: + { + default = + let + src = pkgs.lib.sourceFilesBySuffices ./. [ + "go.mod" + "go.sum" + ".go" + ]; + version = pkgs.lib.strings.removePrefix "v" ( + builtins.elemAt (pkgs.lib.strings.split "\"" ( + pkgs.lib.lists.findFirst (x: pkgs.lib.strings.hasInfix "Version" x) null ( + pkgs.lib.strings.splitString "\n" (builtins.readFile ./main.go) + ) + )) 2 + ); + in + pkgs.buildGoModule { + version = version; + name = "ghettoptt"; + src = src; + vendorHash = "sha256-7K0LFYHJ1NBEL7lRCqsKcWpCJY0btRv9eUhyRN9U7/I="; + }; + } + ); + devShells = forAllSystems ( + { pkgs }: + { + default = pkgs.mkShell { + packages = with pkgs; [ + go + ]; + }; + } + ); }; } diff --git a/go.mod b/go.mod index ea7f588..1bcf0a0 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,17 @@ go 1.21.6 require ( github.com/godbus/dbus/v5 v5.1.0 github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 - github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/client_golang v1.20.4 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - golang.org/x/sys v0.16.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.17.10 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/sys v0.25.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 7119073..3848ab0 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -10,15 +12,31 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 h1:92OsBIf5KB1Tatx+uUGOhah73jyNUrt7DmfDRXXJ5Xo= github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/main.go b/main.go index d042745..b77e695 100644 --- a/main.go +++ b/main.go @@ -9,12 +9,13 @@ import ( "os/signal" "git.t-juice.club/torjus/ghettoptt/bus" + "git.t-juice.club/torjus/ghettoptt/debouncer" "git.t-juice.club/torjus/ghettoptt/metrics" "github.com/holoplot/go-evdev" "github.com/prometheus/client_golang/prometheus/promhttp" ) -const Version = "v0.1.2" +const Version = "v0.1.3" func main() { // Setup logger @@ -71,24 +72,48 @@ func main() { // Start listening for PTT key slog.Info("Starting event listener", "version", Version) - for !done { - ev, err := input.ReadOne() - if err != nil { - if errors.Is(err, os.ErrClosed) { - continue + eventCh := make(chan bool) + doneCtx, doneCancel := context.WithCancel(srvCtx) + defer doneCancel() + + debouncer := debouncer.New(eventCh) + go func() { + var done bool + for !done { + ev, err := input.ReadOne() + if err != nil { + if errors.Is(err, os.ErrClosed) { + continue + } + slog.Error("Error reading from input device", "error", err) + mbus.StopTalking() + os.Exit(1) + } + if ev.Code == evdev.KEY_F24 && ev.Value == 1 { + slog.Debug("PTT ON") + eventCh <- true + } + if ev.Code == evdev.KEY_F24 && ev.Value == 0 { + slog.Debug("PTT OFF") + eventCh <- false + } + if doneCtx.Err() != nil { + close(eventCh) + done = true } - slog.Error("Error reading from input device", "error", err) - os.Exit(1) } - if ev.Code == evdev.KEY_F24 && ev.Value == 1 { - slog.Info("PTT ON") + }() + + for v := range debouncer.C { + if v { mbus.StartTalking() - } - if ev.Code == evdev.KEY_F24 && ev.Value == 0 { - slog.Info("PTT OFF") + slog.Info("Started talking") + } else { mbus.StopTalking() + slog.Info("Stopped talking") } } + mbus.StopTalking() <-srvCtx.Done() slog.Info("Exiting")