Add debouncer #1
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| result | ||||
							
								
								
									
										53
									
								
								debouncer/debouncer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								debouncer/debouncer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										89
									
								
								debouncer/debouncer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								debouncer/debouncer_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										6
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							| @@ -2,11 +2,11 @@ | ||||
|   "nodes": { | ||||
|     "nixpkgs": { | ||||
|       "locked": { | ||||
|         "lastModified": 1716948383, | ||||
|         "narHash": "sha256-SzDKxseEcHR5KzPXLwsemyTR/kaM9whxeiJohbL04rs=", | ||||
|         "lastModified": 1727122398, | ||||
|         "narHash": "sha256-o8VBeCWHBxGd4kVMceIayf5GApqTavJbTa44Xcg5Rrk=", | ||||
|         "owner": "NixOS", | ||||
|         "repo": "nixpkgs", | ||||
|         "rev": "ad57eef4ef0659193044870c731987a6df5cf56b", | ||||
|         "rev": "30439d93eb8b19861ccbe3e581abf97bdc91b093", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|   | ||||
							
								
								
									
										64
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								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 | ||||
|             ]; | ||||
|           }; | ||||
|         } | ||||
|       ); | ||||
|     }; | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										18
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								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= | ||||
|   | ||||
							
								
								
									
										51
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								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") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user