├── .gitignore ├── detect ├── alarm.wav ├── go.mod ├── Makefile ├── go.sum └── main.go ├── server ├── Makefile ├── go.mod ├── main.go └── go.sum ├── scripts ├── pd.sh └── snap.sh ├── LICENSE └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | server/server 2 | detect/detect -------------------------------------------------------------------------------- /detect/alarm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pldubouilh/alarm/HEAD/detect/alarm.wav -------------------------------------------------------------------------------- /detect/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pldubouilh/detect 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5 7 | github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 8 | ) 9 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | FLAGS := -ldflags "-s -w" -trimpath 2 | NOCGO := CGO_ENABLED=0 3 | 4 | build:: 5 | go vet && go fmt 6 | ${NOCGO} go build ${FLAGS} 7 | 8 | run:: 9 | go run main.go 10 | 11 | watch:: 12 | git ls-files | entr -c make run -------------------------------------------------------------------------------- /detect/Makefile: -------------------------------------------------------------------------------- 1 | FLAGS := -ldflags "-s -w" -trimpath 2 | 3 | build:: 4 | go vet && go fmt 5 | go build ${FLAGS} 6 | 7 | run:: build 8 | ./detect -device="Logitech StreamCam: USB Audio (hw:1,0)" 9 | 10 | watch:: 11 | git ls-files | entr -rc make run -------------------------------------------------------------------------------- /scripts/pd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PD_EMAIL=EMAIL 4 | PD_TOKEN=PDTOKEN 5 | 6 | curl -vvv -H "From: ${PD_EMAIL}" -H "Authorization: Token token=${TOKEN}" \ 7 | -H 'Content-Type: application/json' -H 'Accept: application/vnd.pagerduty+json;version=2' \ 8 | -d '{ "incident": { "type": "incident", "title": "fire, fire", "service": { "id": "PFCSJTG", "type": "service_reference" } } }'\ 9 | https://api.pagerduty.com/incidents 10 | -------------------------------------------------------------------------------- /detect/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5 h1:5AlozfqaVjGYGhms2OsdUyfdJME76E6rx5MdGpjzZpc= 2 | github.com/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5/go.mod h1:WY8R6YKlI2ZI3UyzFk7P6yGSuS+hFwNtEzrexRyD7Es= 3 | github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 h1:dd7vnTDfjtwCETZDrRe+GPYNLA1jBtbZeyfyE8eZCyk= 4 | github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12/go.mod h1:i/KKcxEWEO8Yyl11DYafRPKOPVYTrhxiTRigjtEEXZU= 5 | -------------------------------------------------------------------------------- /scripts/snap.sh: -------------------------------------------------------------------------------- 1 | CAMERA=/dev/video0 2 | FILE=`date +%s` 3 | 4 | # Basic auth user / password 5 | TOKEN=`printf "user:pass" | base64` 6 | URL="https://server.com/send" 7 | 8 | mkdir /home/pi/pics 9 | cd /home/pi/pics 10 | 11 | # take picture 12 | ffmpeg -hide_banner -loglevel error -y -f video4linux2 -s 1280x1024 -i ${CAMERA} -frames 1 pic.jpeg 13 | 14 | # add timestamp on image 15 | ffmpeg -hide_banner -loglevel error -y -i pic.jpeg -vf "drawtext=text='%{localtime}': x=(w-tw)/2: y=h-(2*lh): fontcolor=white: box=1: boxcolor=0x00000000@1: fontsize=30" -r 25 -t 5 ${FILE}.jpeg 16 | 17 | # upload 18 | curl -H"Authorization: Basic ${TOKEN}" -F "file=@./${FILE}.jpeg" ${URL} 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pierre Dubouilh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pldubouilh/server 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/gin-gonic/autotls v1.0.0 7 | github.com/gin-gonic/gin v1.9.1 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.9.1 // indirect 12 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 13 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 14 | github.com/gin-contrib/sse v0.1.0 // indirect 15 | github.com/go-playground/locales v0.14.1 // indirect 16 | github.com/go-playground/universal-translator v0.18.1 // indirect 17 | github.com/go-playground/validator/v10 v10.14.0 // indirect 18 | github.com/goccy/go-json v0.10.2 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 21 | github.com/leodido/go-urn v1.2.4 // indirect 22 | github.com/mattn/go-isatty v0.0.19 // indirect 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 24 | github.com/modern-go/reflect2 v1.0.2 // indirect 25 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 26 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 27 | github.com/ugorji/go/codec v1.2.11 // indirect 28 | golang.org/x/arch v0.3.0 // indirect 29 | golang.org/x/crypto v0.19.0 // indirect 30 | golang.org/x/net v0.21.0 // indirect 31 | golang.org/x/sync v0.6.0 // indirect 32 | golang.org/x/sys v0.17.0 // indirect 33 | golang.org/x/text v0.14.0 // indirect 34 | google.golang.org/protobuf v1.30.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Fire Alarm Detector 2 | ==== 3 | 4 | [![fire fire fire](https://img.youtube.com/vi/1EBfxjSFAxQ/0.jpg)](https://www.youtube.com/watch?v=1EBfxjSFAxQ) 5 | 6 | _Note: also works for water leak detector_ 7 | 8 | Featuring: 9 | - fire alarm audio signature detection using FFT and pattern matching 10 | - exec' a custom script if the alarm is detected - example pagerduty & picture-snap-n-upload scripts provided 11 | - an optional server to handle the uploaded pictures (with automatic https and basic auth) 12 | 13 | ## Motivation 14 | 15 | I wanted a local solution running on a Raspberry Pi Zero, and _not_ constantly pushing data to a server, surely it's doable with simple FFTs and pattern matching ! 16 | 17 | Optionally, being able to alert with pagerduty, or take pictures when the alarm is detected (server code included). 18 | 19 | ## Usage 20 | 21 | ```console 22 | $ cd detect 23 | $ go mod tidy 24 | $ go build 25 | $ ./detect --help 26 | Usage of ./detect: 27 | -beeps int 28 | How many beeps to alert (default 3) 29 | -device string 30 | Target device. If empty, will list devices. 31 | -duration duration 32 | Duration of a beep (default 400ms) 33 | -frequency int 34 | Target frequency in Hz (default 3500) 35 | -script string 36 | Script to exec when an alarm is detected 37 | -threshold string 38 | Audio target threshold (default "7") 39 | 40 | $ ./detect -device="Logitech StreamCam: USB Audio (hw:1,0)" 41 | [play alarm.wav] 42 | 2024-04-07 13:41:47 -- alarm detected! 43 | ``` 44 | 45 | Feel free to explore the codebase, this is more of a glorified (but fuctioning !) script :). 46 | 47 | A pre-compiled binary for Raspberry Pi is provided in the release section. It only has a dependency on `portaudio19-dev`. 48 | The two scripts have dependencies on `curl`, and `ffmpeg` for the picture uploading script. -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/gin-gonic/autotls" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func main() { 14 | host := flag.String("h", "127.0.0.1:8123", "host string (can be an IP or a domain)") 15 | username := flag.String("u", "", "basic auth user") 16 | password := flag.String("p", "", "basic auth password") 17 | dest := flag.String("f", "pics/", " folder for pictures") 18 | flag.Parse() 19 | 20 | // check if 21 | ip, port, err := net.SplitHostPort(*host) 22 | runsLocal := err == nil && ip != "" && port != "" 23 | 24 | // enforce basic auth when not running locally 25 | if !runsLocal && (*username == "" || *password == "") { 26 | println("Missing username or password") 27 | flag.PrintDefaults() 28 | os.Exit(1) 29 | } 30 | 31 | r := gin.Default() 32 | 33 | if !runsLocal { 34 | r.Use(gin.BasicAuth(gin.Accounts{*username: *password})) 35 | } 36 | 37 | // server last picure on TLD 38 | r.GET("/", func(c *gin.Context) { 39 | c.File(*dest + "/last.jpg") 40 | }) 41 | 42 | // list gives a list of the last 50 pictures 43 | r.GET("/list", func(c *gin.Context) { 44 | list := "" 57 | c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(list)) 58 | }) 59 | 60 | // serve the static pictures 61 | r.Static("/pics", *dest) 62 | 63 | // receive a picture 64 | r.POST("/send", func(c *gin.Context) { 65 | file, _ := c.FormFile("file") 66 | c.SaveUploadedFile(file, *dest+file.Filename) 67 | c.SaveUploadedFile(file, *dest+"last.jpg") 68 | c.String(http.StatusOK, "ok") 69 | }) 70 | 71 | if runsLocal { 72 | r.Run(*host) 73 | } else { 74 | autotls.Run(r, *host) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /detect/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math" 7 | "math/cmplx" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "strconv" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/gordonklaus/portaudio" 16 | "github.com/mjibson/go-dsp/fft" 17 | ) 18 | 19 | const ( 20 | sampleRate = 48000 21 | frameSize = 512 22 | numChannels = 1 23 | ) 24 | 25 | var device = flag.String("device", "", "Target device. If empty, will list devices.") 26 | var script = flag.String("script", "", "Script to exec when an alarm is detected") 27 | var threshold_str = flag.String("threshold", "7", "Audio target threshold") 28 | var threshold float64 29 | 30 | var targetFreq = flag.Int("frequency", 3500, "Target frequency in Hz") 31 | var beepDuration = flag.Duration("duration", 400*time.Millisecond, "Duration of a beep") 32 | var howManyBeepsToAlert = flag.Int("beeps", 3, "How many beeps to aloers") 33 | 34 | func main() { 35 | flag.Parse() 36 | i, err := strconv.Atoi(*threshold_str) 37 | if err != nil { 38 | fmt.Println("Failed to parse threshold:", err) 39 | os.Exit(1) 40 | } 41 | threshold = float64(i) * 1_000_000_000 42 | 43 | for { 44 | if !run() { 45 | println("Failed to find device, will retry in 5 seconds") 46 | time.Sleep(5 * time.Second) 47 | } else { 48 | break 49 | } 50 | } 51 | } 52 | 53 | func run() bool { 54 | // Initialize PortAudio 55 | if err := portaudio.Initialize(); err != nil { 56 | fmt.Println("Failed to initialize PortAudio:", err) 57 | return false 58 | } 59 | defer portaudio.Terminate() 60 | 61 | var info *portaudio.DeviceInfo 62 | ds, err := portaudio.Devices() 63 | if err != nil { 64 | fmt.Println("Failed to get devices:", err) 65 | return false 66 | } 67 | for i, d := range ds { 68 | if *device == "" { 69 | fmt.Printf("Found device %s\n", d.Name) 70 | } 71 | if d.Name == *device { 72 | info = ds[i] 73 | break 74 | } 75 | } 76 | 77 | if *device == "" { 78 | os.Exit(0) 79 | } 80 | if info == nil { 81 | fmt.Println("Failed to find device:", *device) 82 | return false 83 | } 84 | 85 | // Prepare input parameters for the stream 86 | inputParams := portaudio.StreamParameters{ 87 | Input: portaudio.StreamDeviceParameters{Device: info, Channels: numChannels, Latency: info.DefaultHighInputLatency}, 88 | Output: portaudio.StreamDeviceParameters{Device: nil, Channels: 0, Latency: 0}, 89 | SampleRate: sampleRate, 90 | FramesPerBuffer: frameSize, 91 | } 92 | 93 | // Open stream 94 | stream, err := portaudio.OpenStream(inputParams, processAudio) 95 | if err != nil { 96 | fmt.Println("Failed to open stream:", err) 97 | return false 98 | } 99 | defer stream.Close() 100 | 101 | // Start stream 102 | if err := stream.Start(); err != nil { 103 | fmt.Println("Failed to start stream:", err) 104 | return false 105 | } 106 | defer stream.Stop() 107 | 108 | fmt.Println("Listening for audio...") 109 | 110 | // Check beeps and bops 111 | interrupt := make(chan os.Signal, 1) 112 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 113 | <-interrupt 114 | fmt.Println("\nExiting...") 115 | return true 116 | } 117 | 118 | func processAudio(in []int32) { 119 | // Windowing function to the audio samples before performing the FFT. 120 | window := make([]float64, len(in)) 121 | for i, x := range in { 122 | window[i] = float64(x) * (0.54 - 0.46*math.Cos(2*math.Pi*float64(i)/float64(len(in)-1))) 123 | } 124 | 125 | // Direct FFT 126 | // window := make([]float64, len(in)) 127 | // for i, x := range in { 128 | // window[i] = float64(x) 129 | // } 130 | 131 | fftData := fft.FFTReal(window) 132 | 133 | // Find the magnitude of the target frequency bin 134 | targetIndex := int(float64(len(fftData)) * float64(*targetFreq) / float64(sampleRate)) 135 | magnitude := cmplx.Abs(fftData[targetIndex]) 136 | 137 | // Check if the magnitude is above the threshold 138 | if magnitude > threshold { 139 | go checkBeeps(true) 140 | // fmt.Printf("Significant data detected %dHz, magnitude: %.0f\n", targetFreq, magnitude) 141 | } else { 142 | go checkBeeps(false) 143 | // fmt.Printf("\rNo significant magnitude at %d Hz\n", targetFreq) 144 | } 145 | } 146 | 147 | var lastChange = time.Now() 148 | var beeps = 0 149 | var current = false 150 | 151 | func checkBeeps(state bool) { 152 | if state == current { 153 | return 154 | } 155 | current = state 156 | delta := time.Since(lastChange) 157 | lastChange = time.Now() 158 | 159 | // too old 160 | if delta > 2*(*beepDuration) { 161 | beeps = 0 162 | return 163 | } 164 | 165 | if !state && delta > *beepDuration { 166 | beeps++ 167 | } 168 | 169 | if beeps >= *howManyBeepsToAlert { 170 | go execScript() 171 | fmt.Println(time.Now().Format("2006-01-02 15:04:05"), "-- alarm detected!") 172 | beeps = 0 173 | } 174 | } 175 | 176 | func execScript() { 177 | if *script == "" { 178 | return 179 | } 180 | cmd := exec.Command("/bin/sh", "-c", *script) 181 | err := cmd.Run() 182 | if err != nil { 183 | fmt.Println("Failed to run execScript script:", err) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 3 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 11 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 12 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 13 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 14 | github.com/gin-gonic/autotls v1.0.0 h1:ej32INxMNcgGqETkMlGv+vJM2+cu1oLmuMxndsU3D+c= 15 | github.com/gin-gonic/autotls v1.0.0/go.mod h1:Cdcp4ZsK4SYzYCJ3ojyAku0ldDa1RWLh24N4M9DEMJk= 16 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 17 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 23 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 24 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 25 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 26 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 27 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 28 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 29 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 30 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 34 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 35 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 36 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 37 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 38 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 39 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 40 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 47 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 54 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 58 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 59 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 60 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 61 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 62 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 63 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 64 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 65 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 66 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 67 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 68 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 69 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 70 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 71 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 72 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 73 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 74 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 77 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 78 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 79 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 80 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 83 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 84 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 91 | --------------------------------------------------------------------------------