├── .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 | [](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 := "
"
45 | files, err := os.ReadDir(*dest)
46 | if err != nil {
47 | c.String(http.StatusInternalServerError, "Error reading directory")
48 | return
49 | }
50 | for i := len(files) - 1; i >= 0 && i >= len(files)-50; i-- {
51 | if files[i].IsDir() {
52 | continue
53 | }
54 | list += "- " + files[i].Name() + "
"
55 | }
56 | 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 |
--------------------------------------------------------------------------------