├── .github └── workflows │ └── codeql.yml ├── .gitignore ├── .golangci.yaml ├── INSTALL.md ├── LICENSE ├── README.md ├── cmd ├── compile_dms3 │ └── compile_dms3.go ├── dms3client │ └── dms3client.go ├── dms3client_remote_installer │ └── dms3client_remote_installer.go ├── dms3mail │ └── dms3mail.go ├── dms3server │ └── dms3server.go ├── dms3server_remote_installer │ └── dms3server_remote_installer.go └── install_dms3 │ └── install_dms3.go ├── config ├── dms3build.toml ├── dms3client.toml ├── dms3dashboard.toml ├── dms3libs.toml ├── dms3mail.toml └── dms3server.toml ├── dms3build ├── compiler_config.go ├── installer_config.go └── lib_build.go ├── dms3client ├── client_config.go ├── client_connector.go ├── client_manager.go └── daemons │ └── systemd │ └── dms3client.service ├── dms3dashboard ├── assets │ ├── css │ │ ├── bootstrap.min.css │ │ ├── icomoon-icons.css │ │ └── paper-dashboard.css │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ └── img │ │ ├── dms3logo.png │ │ ├── favicon.png │ │ └── favicon.svg ├── dashboard_client.go ├── dashboard_config.go ├── dashboard_server.go └── dms3dashboard.html ├── dms3libs ├── lib_audio.go ├── lib_config.go ├── lib_detector_config.go ├── lib_file.go ├── lib_log.go ├── lib_network.go ├── lib_os.go ├── lib_process.go ├── lib_util.go └── tests │ ├── lib_audio_test.go │ ├── lib_audio_test.wav │ ├── lib_config_test.go │ ├── lib_detector_config_test.go │ ├── lib_file_test.go │ ├── lib_log_test.go │ ├── lib_network_test.go │ ├── lib_os_test.go │ ├── lib_process_test.go │ ├── lib_util_test.go │ └── lib_util_test.jpg ├── dms3mail ├── assets │ └── img │ │ ├── dms3github.png │ │ └── dms3logo.png ├── dms3mail.html ├── mail_config.go └── motion_mail.go ├── dms3server ├── daemons │ └── systemd │ │ └── dms3server.service ├── media │ ├── motion_start.wav │ └── motion_stop.wav ├── server_config.go ├── server_connector.go └── server_manager.go ├── go.mod └── go.sum /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '28 8 * * 1' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: go 47 | build-mode: autobuild 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # debug file generated in vscode 17 | debug 18 | 19 | # vscode config folder 20 | .vscode/ 21 | 22 | # markdownlint exceptions file 23 | .markdownlint.json 24 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - nlreturn 4 | - err113 5 | - decorder 6 | - dogsled 7 | - dupword 8 | - errcheck 9 | - exhaustive 10 | - exhaustruct 11 | - forbidigo 12 | - funlen 13 | - gocognit 14 | - goconst 15 | - gocritic 16 | - gocyclo 17 | - godox 18 | - gosec 19 | - ireturn 20 | - nestif 21 | - nilerr 22 | - nilnil 23 | - revive 24 | - unused 25 | 26 | linters-settings: 27 | 28 | errcheck: 29 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 30 | # Such cases aren't reported by default. 31 | # Default: false 32 | check-type-assertions: true 33 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. 34 | # Such cases aren't reported by default. 35 | # Default: false 36 | check-blank: true 37 | # To disable the errcheck built-in exclude list. 38 | # See `-excludeonly` option in https://github.com/kisielk/errcheck#excluding-functions for details. 39 | # Default: false 40 | disable-default-exclusions: true 41 | # List of functions to exclude from checking, where each entry is a single function to exclude. 42 | # See https://github.com/kisielk/errcheck#excluding-functions for details. 43 | exclude-functions: 44 | - io/ioutil.ReadFile 45 | - io.Copy(*bytes.Buffer) 46 | - io.Copy(os.Stdout) 47 | 48 | exhaustruct: 49 | # List of regular expressions to match struct packages and their names. 50 | # Regular expressions must match complete canonical struct package/name/structname. 51 | # If this list is empty, all structs are tested. 52 | # Default: [] 53 | include: 54 | - '.+\.Test' 55 | - 'example\.com/package\.ExampleStruct[\d]{1,2}' 56 | # List of regular expressions to exclude struct packages and their names from checks. 57 | # Regular expressions must match complete canonical struct package/name/structname. 58 | # Default: [] 59 | exclude: 60 | - '.+/cobra\.Command$' 61 | 62 | forbidigo: 63 | # Forbid the following identifiers (list of regexp). 64 | # Default: ["^(fmt\\.Print(|f|ln)|print|println)$"] 65 | forbid: 66 | # Built-in bootstrapping functions. 67 | - ^print(ln)?$ 68 | # Optional message that gets included in error reports. 69 | # - p: ^fmt\.Print.*$ 70 | # msg: Do not commit print statements. 71 | # Alternatively, put messages at the end of the regex, surrounded by `(# )?` 72 | # Escape any special characters. Those messages get included in error reports. 73 | # - 'fmt\.Print.*(# Do not commit print statements\.)?' 74 | # Forbid spew Dump, whether it is called as function or method. 75 | # Depends on analyze-types below. 76 | - ^spew\.(ConfigState\.)?Dump$ 77 | # The package name might be ambiguous. 78 | # The full import path can be used as additional criteria. 79 | # Depends on analyze-types below. 80 | - p: ^v1.Dump$ 81 | pkg: ^example.com/pkg/api/v1$ 82 | # Exclude godoc examples from forbidigo checks. 83 | # Default: true 84 | exclude-godoc-examples: false 85 | # Instead of matching the literal source code, 86 | # use type information to replace expressions with strings that contain the package name 87 | # and (for methods and fields) the type name. 88 | # This makes it possible to handle import renaming and forbid struct fields and methods. 89 | # Default: false 90 | analyze-types: true 91 | 92 | ireturn: 93 | # List of interfaces to allow. 94 | # Lists of the keywords and regular expressions matched to interface or package names can be used. 95 | # `allow` and `reject` settings cannot be used at the same time. 96 | # 97 | # Keywords: 98 | # - `empty` for `interface{}` 99 | # - `error` for errors 100 | # - `stdlib` for standard library 101 | # - `anon` for anonymous interfaces 102 | # - `generic` for generic interfaces added in go 1.18 103 | # 104 | # Default: [anon, error, empty, stdlib] 105 | allow: 106 | - anon 107 | - generic 108 | - empty 109 | - error 110 | # You can specify idiomatic endings for interface 111 | - (or|er)$ 112 | # List of interfaces to reject. 113 | # Lists of the keywords and regular expressions matched to interface or package names can be used. 114 | # `allow` and `reject` settings cannot be used at the same time. 115 | # 116 | # Keywords: 117 | # - `empty` for `interface{}` 118 | # - `error` for errors 119 | # - `stdlib` for standard library 120 | # - `anon` for anonymous interfaces 121 | # - `generic` for generic interfaces added in go 1.18 122 | # 123 | # Default: [] 124 | reject: 125 | # - github.com\/user\/package\/v4\.Type 126 | 127 | nlreturn: 128 | # Size of the block (including return statement that is still "OK") 129 | # so no return split required. 130 | # Default: 1 131 | block-size: 2 132 | 133 | gocognit: 134 | # Minimal code complexity to report. 135 | # Default: 30 (but we recommend 10-20) 136 | min-complexity: 20 137 | 138 | goconst: 139 | # Minimal length of string constant. 140 | # Default: 3 141 | min-len: 3 142 | # Minimum occurrences of constant string count to trigger issue. 143 | # Default: 3 144 | min-occurrences: 3 145 | # Ignore test files. 146 | # Default: false 147 | ignore-tests: true 148 | # Look for existing constants matching the values. 149 | # Default: true 150 | match-constant: false 151 | # Search also for duplicated numbers. 152 | # Default: false 153 | numbers: true 154 | # Minimum value, only works with goconst.numbers 155 | # Default: 3 156 | min: 2 157 | # Maximum value, only works with goconst.numbers 158 | # Default: 3 159 | max: 2 160 | # Ignore when constant is not used as function argument. 161 | # Default: true 162 | ignore-calls: false 163 | # Exclude strings matching the given regular expression. 164 | # Default: "" 165 | ignore-strings: 'foo.+' 166 | 167 | gocyclo: 168 | # Minimal code complexity to report. 169 | # Default: 30 (but we recommend 10-20) 170 | min-complexity: 20 171 | 172 | nestif: 173 | # Minimal complexity of if statements to report. 174 | # Default: 5 175 | min-complexity: 4 176 | 177 | nilnil: 178 | # In addition, detect opposite situation (simultaneous return of non-nil error and valid value). 179 | # Default: false 180 | detect-opposite: true 181 | # List of return types to check. 182 | # Default: ["chan", "func", "iface", "map", "ptr", "uintptr", "unsafeptr"] 183 | checked-types: 184 | - chan 185 | - func 186 | - iface 187 | - map 188 | - ptr 189 | - uintptr 190 | - unsafeptr 191 | 192 | wsl: 193 | # Do strict checking when assigning from append (x = append(x, y)). 194 | # If this is set to true - the append call must append either a variable 195 | # assigned, called or used on the line above. 196 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#strict-append 197 | # Default: true 198 | strict-append: false 199 | # Allows assignments to be cuddled with variables used in calls on 200 | # line above and calls to be cuddled with assignments of variables 201 | # used in call on line above. 202 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-assign-and-call 203 | # Default: true 204 | allow-assign-and-call: false 205 | # Allows assignments to be cuddled with anything. 206 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-assign-and-anything 207 | # Default: false 208 | allow-assign-and-anything: true 209 | # Allows cuddling to assignments even if they span over multiple lines. 210 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-multiline-assign 211 | # Default: true 212 | allow-multiline-assign: false 213 | # If the number of lines in a case block is equal to or lager than this number, 214 | # the case *must* end white a newline. 215 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-case-trailing-whitespace 216 | # Default: 0 217 | force-case-trailing-whitespace: 1 218 | # Allow blocks to end with comments. 219 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-trailing-comment 220 | # Default: false 221 | allow-trailing-comment: true 222 | # Allow multiple comments in the beginning of a block separated with newline. 223 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-separated-leading-comment 224 | # Default: false 225 | allow-separated-leading-comment: true 226 | # Allow multiple var/declaration statements to be cuddled. 227 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-cuddle-declarations 228 | # Default: false 229 | allow-cuddle-declarations: true 230 | # A list of call idents that everything can be cuddled with. 231 | # Defaults: [ "Lock", "RLock" ] 232 | allow-cuddle-with-calls: ["Foo", "Bar"] 233 | # AllowCuddleWithRHS is a list of right hand side variables that is allowed 234 | # to be cuddled with anything. 235 | # Defaults: [ "Unlock", "RUnlock" ] 236 | allow-cuddle-with-rhs: ["Foo", "Bar"] 237 | # Causes an error when an If statement that checks an error variable doesn't 238 | # cuddle with the assignment of that variable. 239 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-err-cuddling 240 | # Default: false 241 | force-err-cuddling: true 242 | # When force-err-cuddling is enabled this is a list of names 243 | # used for error variables to check for in the conditional. 244 | # Default: [ "err" ] 245 | error-variable-names: ["foo"] 246 | # Causes an error if a short declaration (:=) cuddles with anything other than 247 | # another short declaration. 248 | # This logic overrides force-err-cuddling among others. 249 | # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-short-decl-cuddling 250 | # Default: false 251 | force-short-decl-cuddling: true 252 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Distributed Motion Surveillance Security System (DMS3) Installation 2 | 3 | Installation details for **DMS3** are now available in the [DMS3 project wiki](https://github.com/richbl/go-distributed-motion-s3/wiki) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Rich Bloch 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Distributed Motion Surveillance Security System (DMS3) 2 | 3 | ![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/richbl/go-distributed-motion-s3?include_prereleases) [![Go Report Card](https://goreportcard.com/badge/github.com/richbl/go-distributed-motion-s3)](https://goreportcard.com/report/github.com/richbl/go-distributed-motion-s3) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d81b7869ac134229b78105544e783667)](https://app.codacy.com/gh/richbl/go-distributed-motion-s3/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=richbl_go-distributed-motion-s3&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=richbl_go-distributed-motion-s3) 4 | 5 | ## What Is **DMS3**? 6 | 7 |

8 | DMS3Mail Event 9 |

10 | 11 | **Distributed Motion Surveillance Security System (DMS3)** is a [Go-based](https://golang.org/ "Go") application that integrates third-party open-source motion detection applications (*e.g.*, the [Motion](https://motion-project.github.io/ "Motion") motion detection software package, or [OpenCV](http://opencv.org/ "OpenCV"), the Open Source Computer Vision Library) into an automated distributed motion surveillance system that: 12 | 13 | - Using a local network, wirelessly senses when someone is "at home" and when someone is "not at home" and automatically enables or disables the surveillance system 14 | - Through the **DMS3Server**, the system coordinates video stream processing, reporting, and user notification to participating device clients (*e.g.*, a Raspberry Pi or similar) running the **DMS3Client** component which: 15 | - Greatly minimizes network congestion, particularly during high-bandwidth surveillance events of interest 16 | - Better utilizes device client CPU/GPU processing power: keeping stream processing on-board and distributed around the network 17 | - Optionally, **DMS3Clients** can generate email reports for events of interest containing images or video using the available **DMS3Mail** component 18 | - Optionally, the **DMS3Server** can display the current state of all reporting **DMS3Clients** visually through the use of the **DMS3Dashboard** component 19 | - Works cooperatively with "less smart" device clients such as IP cameras (wired or WiFi), webcams, and other USB camera devices 20 | 21 | ## Want to Know More? 22 | 23 | For more information about **DMS3**, check out the [DMS3 project wiki](https://github.com/richbl/go-distributed-motion-s3/wiki). The wiki includes the following sections: 24 | 25 | - Project overview 26 | - Use cases 27 | - Features 28 | - Components 29 | - Architecture 30 | - How **DMS3** works 31 | - Requirements 32 | - **DMS3** Release Notes 33 | - Application installation 34 | - Downloading, building, and installing the application 35 | - Running the application 36 | - Project roadmap 37 | - Project license -------------------------------------------------------------------------------- /cmd/compile_dms3/compile_dms3.go: -------------------------------------------------------------------------------- 1 | // compiles dms3 components into platform-specific Go binary executables and copies configuration 2 | // and media files into a dms3_release folder 3 | // 4 | // the dms3_release folder is then used as the base object for performing dms3 component 5 | // installation on dms3client(s) and dms3server(s) device platforms (see install_dms3.go for 6 | // details) 7 | package main 8 | 9 | import ( 10 | "path/filepath" 11 | 12 | "github.com/richbl/go-distributed-motion-s3/dms3build" 13 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 14 | ) 15 | 16 | func main() { 17 | 18 | dms3libs.LoadLibConfig(filepath.Join(dms3libs.DMS3Config, dms3libs.DMS3libsTOML)) 19 | 20 | // create release folder 21 | dms3build.BuildReleaseFolder() 22 | 23 | // build platform-specific components into release folder 24 | dms3build.BuildComponents() 25 | 26 | // copy service daemons into release folder 27 | dms3build.CopyServiceDaemons() 28 | 29 | // copy dms3server media files into release folder 30 | dms3build.CopyMediaFiles() 31 | 32 | // copy dms3dashboard html file and assets into release folder 33 | dms3build.CopyComponents(dms3libs.DMS3Dashboard) 34 | 35 | // copy dms3mail html file and assets into release folder 36 | dms3build.CopyComponents(dms3libs.DMS3Mail) 37 | 38 | // copy TOML files into release folder 39 | dms3build.CopyConfigFiles() 40 | 41 | } 42 | -------------------------------------------------------------------------------- /cmd/dms3client/dms3client.go: -------------------------------------------------------------------------------- 1 | // Package main dms3client initializes a dms3client device component 2 | package main 3 | 4 | import "github.com/richbl/go-distributed-motion-s3/dms3client" 5 | 6 | func main() { 7 | dms3client.Init("/etc/distributed-motion-s3") 8 | } 9 | -------------------------------------------------------------------------------- /cmd/dms3client_remote_installer/dms3client_remote_installer.go: -------------------------------------------------------------------------------- 1 | // this script will be copied to the dms3 device component platform, executed, 2 | // and then deleted automatically 3 | // 4 | // NOTE: must be run with admin privileges on the remote device 5 | package main 6 | 7 | import ( 8 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 9 | ) 10 | 11 | func main() { 12 | 13 | // NOTE: this component is run on the remote client device (per dms3build.toml configuration) 14 | 15 | // Specific files and directories for the client 16 | binFiles := []string{ 17 | dms3libs.DMS3Client, 18 | dms3libs.DMS3Mail, 19 | } 20 | configDirs := []string{ 21 | dms3libs.DMS3Client, 22 | dms3libs.DMS3Dashboard, 23 | dms3libs.DMS3Libs, 24 | dms3libs.DMS3Mail, 25 | } 26 | 27 | // Call the shared installer function 28 | dms3libs.DeviceInstaller(binFiles, configDirs) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/dms3mail/dms3mail.go: -------------------------------------------------------------------------------- 1 | // Package main dms3mail initializes a dms3mail device component 2 | package main 3 | 4 | import "github.com/richbl/go-distributed-motion-s3/dms3mail" 5 | 6 | func main() { 7 | dms3mail.Init("/etc/distributed-motion-s3") 8 | } 9 | -------------------------------------------------------------------------------- /cmd/dms3server/dms3server.go: -------------------------------------------------------------------------------- 1 | // Package main dms3server initializes a dms3server device component 2 | package main 3 | 4 | import "github.com/richbl/go-distributed-motion-s3/dms3server" 5 | 6 | func main() { 7 | dms3server.Init("/etc/distributed-motion-s3") 8 | } 9 | -------------------------------------------------------------------------------- /cmd/dms3server_remote_installer/dms3server_remote_installer.go: -------------------------------------------------------------------------------- 1 | // this script will be copied to the dms3 device component platform, executed, 2 | // and then deleted automatically 3 | // 4 | // NOTE: must be run with admin privileges on the remote device 5 | package main 6 | 7 | import ( 8 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 9 | ) 10 | 11 | func main() { 12 | 13 | // NOTE: this component is run on the remote server device (per dms3build.toml configuration) 14 | 15 | // Specific files and directories for the server 16 | binFiles := []string{ 17 | dms3libs.DMS3Server, 18 | } 19 | configDirs := []string{ 20 | dms3libs.DMS3Server, 21 | dms3libs.DMS3Dashboard, 22 | dms3libs.DMS3Libs, 23 | } 24 | 25 | // Call the shared Setup function 26 | dms3libs.DeviceInstaller(binFiles, configDirs) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /cmd/install_dms3/install_dms3.go: -------------------------------------------------------------------------------- 1 | // installs dms3 components (dms3client, dms3server, and dms3mail) and supporting configuration, 2 | // service daemons, and media files onto the specified dms3 device component platforms (see 3 | // dms3build.toml for a list of platforms to install onto) 4 | // 5 | // this installer depends on a local dms3_release folder created through dms3 compilation (see 6 | // compile_dms3.go for details) 7 | package main 8 | 9 | import ( 10 | "path/filepath" 11 | 12 | "github.com/richbl/go-distributed-motion-s3/dms3build" 13 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 14 | ) 15 | 16 | func main() { 17 | 18 | releasePath := dms3libs.DMS3Release 19 | 20 | // confirm existence of dms3_release folder based on releasePath 21 | dms3build.ConfirmReleaseFolder(releasePath) 22 | 23 | // read configuration in from dms3build TOML 24 | dms3libs.LoadComponentConfig(&dms3build.BuildConfig, filepath.Join(releasePath, dms3libs.DMS3Config, "dms3build", dms3libs.DMS3buildTOML)) 25 | 26 | // install components onto device platforms 27 | dms3build.InstallClientComponents(releasePath) 28 | dms3build.InstallServerComponents(releasePath) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /config/dms3build.toml: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Build Component Configuration File 2 | # 1.4.4 3 | 4 | [Clients] 5 | 6 | [Clients.0] 7 | User = "pi" # Username of the client device 8 | DeviceName = "picam-alpha.local" # Domain name of the client device 9 | SSHPassword = "" # SSH password for the client device (empty if using SSH certificate) 10 | RemoteAdminPassword = "PASSWORD" # Remote administration password for the client device (required for component installation) 11 | Port = 22 # SSH port of the client device 12 | Platform = "linuxArm7" # Platform of the client device. `dms3build` will use this to copy over the correct binary executable 13 | 14 | [Clients.1] 15 | User = "pi" 16 | DeviceName = "picam-beta.local" 17 | SSHPassword = "" # using SSH certificate 18 | RemoteAdminPassword = "PASSWORD" 19 | Port = 22 20 | Platform = "linuxArm7" 21 | 22 | [Clients.2] 23 | User = "pi" 24 | DeviceName = "picam-gamma.local" 25 | SSHPassword = "" # using SSH certificate 26 | RemoteAdminPassword = "PASSWORD" 27 | Port = 22 28 | Platform = "linuxArm6" 29 | 30 | [Clients.3] 31 | User = "richbl" 32 | DeviceName = "main.local" 33 | SSHPassword = "" # using SSH certificate 34 | RemoteAdminPassword = "PASSWORD" 35 | Port = 22 36 | Platform = "linuxAMD64" 37 | 38 | [Servers] 39 | 40 | [Servers.0] 41 | User = "richbl" # Username of the server device 42 | DeviceName = "main.local" # Domain name of the server device 43 | SSHPassword = "" # SSH password for the server device (empty if using SSH certificate) 44 | RemoteAdminPassword = "PASSWORD" # Remote administration password for the server device (required for component installation) 45 | Port = 22 # SSH port of the server device 46 | Platform = "linuxAMD64" # Platform of the server device. `dms3build` will use this to copy over the correct binary executable 47 | -------------------------------------------------------------------------------- /config/dms3client.toml: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Client Component Configuration File 2 | # 1.4.4 3 | 4 | [Server] 5 | # IP is the address on which the DMS3Server is running 6 | # 7 | IP = "10.10.10.9" 8 | 9 | # Port is the port on which the DMS3Server is running 10 | # 11 | Port = 49300 12 | 13 | # CheckInterval is the interval (in seconds) for checking with the DMS3Server 14 | # 15 | CheckInterval = 15 16 | 17 | [Logging] 18 | 19 | # LogLevel sets the log levels for application logging using the following table: 20 | # Note that DEBUG > INFO > FATAL, so DEBUG includes all log levels 21 | # 22 | # 0 - OFF, no logging 23 | # 1 - FATAL, report fatal events 24 | # 2 - INFO, report informational events 25 | # 4 - DEBUG, report debugging events 26 | # 27 | LogLevel = 2 28 | 29 | # LogDevice determines to what output logging should be set using the following table: 30 | # Ignored if LogLevel == 0 31 | # 32 | # 0 - STDOUT (terminal) 33 | # 1 - log file 34 | # 35 | LogDevice = 0 36 | 37 | # LogFilename is the logging filename 38 | # Ignored if LogLevel == 0 or LogDevice == 0 39 | # 40 | LogFilename = "dms3client.log" 41 | 42 | # LogLocation is the location of logfile (absolute path; must have r/w permissions) 43 | # Ignored if LogLevel == 0 or LogDevice == 0 44 | # 45 | LogLocation = "/var/log/dms3" 46 | -------------------------------------------------------------------------------- /config/dms3dashboard.toml: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Dashboard Component Configuration File 2 | # 1.4.4 3 | 4 | [Server] 5 | 6 | # Configuration elements for DMS3Server 7 | # Ignored if Server.EnableDashboard == false (set in dms3server.toml) 8 | 9 | # Port is the port on which to run the DMS3Dashboard server 10 | # 11 | Port = 8081 12 | 13 | # Filename of DMS3Dashboard HTML dashboard template file 14 | # 15 | Filename = "dms3dashboard.html" 16 | 17 | # FileLocation is where the HTML dashboard template file is located 18 | # 19 | # By default, the value is "" (empty string), which sets to the path of the release dashboard 20 | # folder (e.g., /etc/distributed-motion-s3/dms3dashboard/dms3dashboard.html) 21 | # Any other filepath/filename will be used if valid 22 | # 23 | FileLocation = "" 24 | 25 | # Dashboard title 26 | # 27 | Title = "DMS3 Dashboard" 28 | 29 | # Enables (true) or disables (false) to alphabetically re-sort devices displayed in the 30 | # dashboard template 31 | # 32 | ReSort = true 33 | 34 | # Enables (true) or disables (false) to make DMS3Server the first of all devices displayed 35 | # in the dashboard template 36 | # Ignored if ReSort == false 37 | # 38 | ServerFirst = true 39 | 40 | # Device status identifies the stages when a device is no longer reporting status updates 41 | # to the dashboard server 42 | # 43 | # Device status values are defined as a multiplier of Server.CheckInterval 44 | # (default = 15 seconds) declared/defined in the dms3server.toml file 45 | # 46 | # If the device check interval for the dashboard server is every 15 seconds (default), and 47 | # the device status multiplier for caution (DeviceStatus.Caution) is 200 (default), then the 48 | # dashboard server will report a device caution status (yellow device icon) after 3000 49 | # seconds (50 minutes) if no status updates received from that device 50 | # 51 | # Device status will continue to progress through each of the stages identified below, or reset 52 | # to a normal device status if device again reports in to the dashboard server 53 | # 54 | [Server.DeviceStatus] 55 | Caution = 200 # yellow device icon on the dashboard after 50 minutes (200*15 seconds) 56 | Danger = 3000 # red device icon on the dashboard after 12.5 hours (3000*15 seconds) 57 | Missing = 28800 # removed from dashboard server after 5 days (28800*15 seconds) 58 | 59 | [Client] 60 | 61 | # Configuration elements for DMS3Client 62 | # 63 | # ImagesFolder is the location of where the motion detection application stores its 64 | # motion-triggered image/movie files on the client (e.g., matching the target_dir parameter 65 | # used in the Motion application) 66 | # 67 | # Used in determining the client "events" metric, presented through the dashboard 68 | # If the value is "" (empty string), this metric is disabled (not reported) on the dashboard 69 | # 70 | ImagesFolder = "/home/richbl/motion_pics" 71 | -------------------------------------------------------------------------------- /config/dms3libs.toml: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Libs Component Configuration File 2 | # 1.4.4 3 | 4 | [SysCommands] 5 | 6 | # SysCommands provide a location mapping of required system commands used by various DMS3 7 | # components 8 | # 9 | APLAY = "/usr/bin/aplay" 10 | BASH = "/usr/bin/bash" 11 | CAT = "/usr/bin/cat" 12 | ENV = "/usr/bin/env" 13 | GREP = "/usr/bin/grep" 14 | IP = "/usr/sbin/ip" 15 | PGREP = "/usr/bin/pgrep" 16 | PING = "/usr/bin/ping" 17 | PKILL = "/usr/bin/pkill" 18 | 19 | # the Motion application--currently used as the default for DMS3 motion detection--is configured 20 | # differently based on the version of Motion installed on the client and the client OS in use 21 | # 22 | # When in doubt, consult the Motion project documentation: https://motion-project.github.io/ 23 | # 24 | # ----------------- 25 | # 26 | # For Motion 4.7.x running on a Raspberry Pi with the Bookworm (or later) OS release, the 27 | # following command assignment should be used: 28 | # 29 | # MOTION = "/usr/bin/libcamerify motion" 30 | # 31 | # ----------------- 32 | # 33 | # For future versions of Motion (5.x.x is the next planned release), the following 34 | # command assignment should be used (the location of the motion binary with the -c switch used 35 | # to specify the configuration file location): 36 | # 37 | # MOTION = "/usr/local/bin/motion -c /usr/local/etc/motion/motion.conf" 38 | # 39 | # ----------------- 40 | # 41 | # Alternatively, when choosing to run DMS3 client(s) as a systemd service, the dms3client.service 42 | # file (included in this project) can be edited to specify the location of the motion.conf 43 | # configuration file: 44 | # 45 | # ... 46 | # [Service] 47 | # WorkingDirectory=/usr/local/etc/motion 48 | # ... 49 | # 50 | # In this case--when running DMS3 client(s) as a systemd service--there's no need to specify the 51 | # configuration file location, so the following command assignment is used: 52 | # 53 | MOTION = "/usr/local/bin/motion" 54 | -------------------------------------------------------------------------------- /config/dms3mail.toml: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Mail Component Configuration File 2 | # 1.4.4 3 | 4 | # Filename of HTML email template file 5 | # 6 | Filename = "dms3mail.html" 7 | 8 | # FileLocation is where the HTML email template file is located 9 | # By default, the value is "" (empty string), which sets the path to the release email 10 | # folder (e.g., /etc/distributed-motion-s3/dms3mail) 11 | # 12 | # Any other filepath/filename will be used, if valid 13 | # 14 | FileLocation = "" 15 | 16 | [Email] 17 | 18 | # EmailFrom is the email sender 19 | # 20 | From = "dms3mail@businesslearninginc.com" 21 | 22 | # EmailTo is the email recipient 23 | # 24 | To = "user@gmail.com" 25 | 26 | [SMTP] 27 | 28 | # SMTPAddress is the host of the SMTP server 29 | # 30 | Address = "smtp.gmail.com" 31 | 32 | # SMTPPort is the port of the SMTP server 33 | # 34 | Port = 587 35 | 36 | # SMTPUsername is the username to use to authenticate to the SMTP server 37 | # 38 | Username = "user" 39 | 40 | # SMTPPassword is the password to use to authenticate to the SMTP server 41 | # 42 | Password = "password" 43 | 44 | [Logging] 45 | 46 | # LogLevel sets the log levels for application logging using the following table: 47 | # Note that DEBUG > INFO > FATAL, so DEBUG includes all log levels 48 | # 49 | # 0 - OFF, no logging 50 | # 1 - FATAL, report fatal events 51 | # 2 - INFO, report informational events 52 | # 4 - DEBUG, report debugging events 53 | # 54 | LogLevel = 1 55 | 56 | # LogDevice determines to what device logging should be set using the following table: 57 | # Ignored if LogLevel == 0 58 | # 59 | # 0 - STDOUT (terminal) 60 | # 1 - log file 61 | # 62 | LogDevice = 0 63 | 64 | # LogFilename is the logging filename 65 | # Ignored if LogLevel == 0 or LogDevice == 0 66 | # 67 | LogFilename = "dms3mail.log" 68 | 69 | # LogLocation is the location of logfile (absolute path; must have r/w permissions) 70 | # Ignored if LogLevel == 0 or LogDevice == 0 71 | # 72 | LogLocation = "/var/log/dms3" 73 | -------------------------------------------------------------------------------- /config/dms3server.toml: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Server Component Configuration File 2 | # 1.4.4 3 | 4 | [Server] 5 | 6 | # Port is the port on which to run DMS3Server 7 | # 8 | Port = 49300 9 | 10 | # CheckInterval is the interval (in seconds) between local checks for change to motion_state 11 | # 12 | CheckInterval = 15 13 | 14 | # Enables (true) or disables (false) the DMS3Dashboard running over HTTP on this server 15 | # 16 | EnableDashboard = true 17 | 18 | [Audio] 19 | 20 | # Enables (true) or disables (false) the play-back of audio on motion detector application 21 | # start/stop 22 | # 23 | Enable = true 24 | 25 | # PlayMotionStart is the audio file played when the motion detector application is activated 26 | # By default, the value is "" (empty string), which gets set to the path of the release /media 27 | # folder and filename (e.g., /etc/distributed-motion-s3/dms3server/media/motion_start.wav) 28 | # 29 | # Any other filepath/filename will be used if valid, else set to local development folder 30 | # Ignored if Audio.Enable == false 31 | # 32 | PlayMotionStart = "" 33 | 34 | # PlayMotionStop is the audio file played when the motion detector application is deactivated 35 | # By default, the value is "" (empty string), which gets set to the path of the release /media 36 | # folder and filename (e.g., /etc/distributed-motion-s3/dms3server/media/motion_stop.wav) 37 | # 38 | # Any other filepath/filename will be used if valid, else set to local development folder 39 | # Ignored if Audio.Enable == false 40 | # 41 | PlayMotionStop = "" 42 | 43 | [AlwaysOn] 44 | 45 | # Enables (true) or disables (false) the motion detector application "Always On" feature which 46 | # starts/stops detection based on time-of-day instead of the absence/presence of user proxy 47 | # device(s) (e.g., smartphones) 48 | # 49 | Enable = true 50 | 51 | # TimeRange is the start and end times (24-hour format) for motion sensing to always be enabled, 52 | # regardless of absence/presence of user proxy device(s) 53 | # Ignored if AlwaysOn.Enable == false 54 | # 55 | TimeRange = ["2300", "0400"] 56 | 57 | [UserProxy] 58 | 59 | # IPBase is the first three address octets defining the LAN (e.g., 10.10.10.) where user 60 | # proxies (devices representing users on the network, such as a smartphone) will 61 | # be scanned for to determine when motion should be run 62 | # 63 | IPBase = "10.10.10." 64 | 65 | # IPRange is the fourth address octet defined as a range (e.g., 100..254) in which to search for 66 | # user proxies 67 | # 68 | IPRange = [100, 254] 69 | 70 | # MacsToFind are the MAC addresses (e.g., "24:da:9b:0d:53:8f") of user proxy device(s) 71 | # to search for on the LAN 72 | # 73 | # IMPORTANT: use colon delimiters only, as this is what the 'ip' command expects 74 | # 75 | MacsToFind = ["24:da:9b:0d:53:8f", "f8:cf:c5:d2:bb:9e"] 76 | 77 | [Logging] 78 | 79 | # LogLevel sets the log levels for application logging using the following table: 80 | # Note that DEBUG > INFO > FATAL, so DEBUG includes all log levels 81 | # 82 | # 0 - OFF, no logging 83 | # 1 - FATAL, report fatal events 84 | # 2 - INFO, report informational events 85 | # 4 - DEBUG, report debugging events 86 | # 87 | LogLevel = 2 88 | 89 | # LogDevice determines to what device logging should be set using the following table: 90 | # Ignored if LogLevel == 0 91 | # 92 | # 0 - STDOUT (terminal) 93 | # 1 - log file 94 | # 95 | LogDevice = 0 96 | 97 | # LogFilename is the logging filename 98 | # Ignored if LogLevel == 0 or LogDevice == 0 99 | # 100 | LogFilename = "dms3server.log" 101 | 102 | # LogLocation is the location of logfile (absolute path; must have r/w permissions) 103 | # Ignored if LogLevel == 0 or LogDevice == 0 104 | # 105 | LogLocation = "/var/log/dms3" 106 | -------------------------------------------------------------------------------- /dms3build/compiler_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3build compiler configuration structures and variables 2 | package dms3build 3 | 4 | import "github.com/richbl/go-distributed-motion-s3/dms3libs" 5 | 6 | // structComponent contains component details 7 | type structComponent struct { 8 | srcName string // component source filename 9 | exeName string // compiled filename 10 | dirName string // location for components in release folder 11 | configFilename string // relevant config (TOML) file 12 | compile bool // whether component should be compiled 13 | } 14 | 15 | var components = []structComponent{ 16 | { 17 | srcName: "cmd/dms3client/dms3client.go", 18 | exeName: dms3libs.DMS3Client, 19 | dirName: dms3libs.DMS3Client, 20 | configFilename: dms3libs.DMS3clientTOML, 21 | compile: true, 22 | }, 23 | { 24 | srcName: "cmd/dms3server/dms3server.go", 25 | exeName: dms3libs.DMS3Server, 26 | dirName: dms3libs.DMS3Server, 27 | configFilename: dms3libs.DMS3serverTOML, 28 | compile: true, 29 | }, 30 | { 31 | srcName: "cmd/dms3mail/dms3mail.go", 32 | exeName: dms3libs.DMS3Mail, 33 | dirName: dms3libs.DMS3Mail, 34 | configFilename: dms3libs.DMS3mailTOML, 35 | compile: true, 36 | }, 37 | { 38 | srcName: "cmd/install_dms3/install_dms3.go", 39 | exeName: "install_dms3", 40 | dirName: "dms3build", 41 | configFilename: dms3libs.DMS3buildTOML, 42 | compile: true, 43 | }, 44 | { 45 | srcName: "cmd/dms3client_remote_installer/dms3client_remote_installer.go", 46 | exeName: "dms3client_remote_installer", 47 | dirName: "dms3build", 48 | configFilename: "", 49 | compile: true, 50 | }, 51 | { 52 | srcName: "cmd/dms3server_remote_installer/dms3server_remote_installer.go", 53 | exeName: "dms3server_remote_installer", 54 | dirName: "dms3build", 55 | configFilename: "", 56 | compile: true, 57 | }, 58 | { 59 | srcName: "", 60 | exeName: dms3libs.DMS3Libs, 61 | dirName: dms3libs.DMS3Libs, 62 | configFilename: dms3libs.DMS3libsTOML, 63 | compile: false, 64 | }, 65 | { 66 | srcName: "", 67 | exeName: dms3libs.DMS3Dashboard, 68 | dirName: dms3libs.DMS3Dashboard, 69 | configFilename: dms3libs.DMS3dashboardTOML, 70 | compile: false, 71 | }, 72 | } 73 | 74 | // platformType represents all available platform types 75 | type platformType string 76 | 77 | // platform types 78 | const ( 79 | linuxArm6 platformType = "linuxArm6" 80 | linuxArm7 platformType = "linuxArm7" 81 | linuxArm8 platformType = "linuxArm8" 82 | linuxAMD64 platformType = "linuxAMD64" 83 | ) 84 | 85 | // structPlatform contains build environment/platform details 86 | type structPlatform struct { 87 | dirName string 88 | compileTags string 89 | } 90 | 91 | // BuildEnv contains platform build details 92 | var BuildEnv = map[platformType]structPlatform{ 93 | linuxArm6: { 94 | dirName: "linux_arm6", 95 | compileTags: "GOOS=linux GOARCH=arm GOARM=6", 96 | }, 97 | linuxArm7: { 98 | dirName: "linux_arm7", 99 | compileTags: "GOOS=linux GOARCH=arm GOARM=7", 100 | }, 101 | linuxArm8: { 102 | dirName: "linux_arm8", 103 | compileTags: "GOOS=linux GOARCH=arm64", 104 | }, 105 | linuxAMD64: { 106 | dirName: "linux_amd64", 107 | compileTags: "GOOS=linux GOARCH=amd64", 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /dms3build/installer_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3build installer configuration structures and variables 2 | package dms3build 3 | 4 | // BuildConfig contains installation configuration settings read from TOML file 5 | var BuildConfig *structSettings 6 | 7 | // client-side configuration parameters 8 | type structSettings struct { 9 | Clients map[string]structDevice 10 | Servers map[string]structDevice 11 | } 12 | 13 | type structDevice struct { 14 | User string 15 | DeviceName string 16 | SSHPassword string 17 | RemoteAdminPassword string 18 | Port int 19 | Platform platformType 20 | } 21 | -------------------------------------------------------------------------------- /dms3build/lib_build.go: -------------------------------------------------------------------------------- 1 | // Package dms3build library 2 | package dms3build 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | 11 | "github.com/mrgleam/easyssh" 12 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 13 | ) 14 | 15 | const ( 16 | command = "cmd" 17 | media = "media" 18 | ) 19 | 20 | var ( 21 | errNoReleaseFolder = errors.New("no release folder found") 22 | ) 23 | 24 | // BuildReleaseFolder creates the directory structure for each platform passed into it 25 | func BuildReleaseFolder() { 26 | 27 | dms3libs.RmDir(dms3libs.DMS3Release) 28 | 29 | for platformType := range BuildEnv { 30 | fmt.Println("Creating release folder for " + BuildEnv[platformType].dirName + " platform...") 31 | dms3libs.MkDir(filepath.Join(dms3libs.DMS3Release, command, BuildEnv[platformType].dirName)) 32 | } 33 | 34 | for itr := range components { 35 | fmt.Println("Creating release folder for " + components[itr].exeName + " component...") 36 | dirName := components[itr].dirName 37 | 38 | if components[itr].dirName == dms3libs.DMS3Server { 39 | dirName = filepath.Join(dirName, media) 40 | } 41 | 42 | dms3libs.MkDir(filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dirName)) 43 | } 44 | 45 | } 46 | 47 | // BuildComponents compiles dms3 components for each platform passed into it 48 | func BuildComponents() { 49 | 50 | for platformType, env := range BuildEnv { 51 | fmt.Println("Building dms3 components for " + env.dirName + " platform...") 52 | 53 | for _, component := range components { 54 | if component.compile { 55 | buildComponent(env, platformType, component) 56 | } 57 | } 58 | } 59 | 60 | } 61 | 62 | // buildComponent compiles individual dms3 components passed in from BuildComponents 63 | func buildComponent(env structPlatform, platformType platformType, component structComponent) { 64 | 65 | var outputDir string 66 | 67 | if component.exeName == "install_dms3" && platformType == linuxAMD64 { 68 | outputDir = filepath.Join(dms3libs.DMS3Release, command, component.exeName) 69 | } else { 70 | outputDir = filepath.Join(dms3libs.DMS3Release, command, env.dirName, component.exeName) 71 | } 72 | 73 | command := dms3libs.LibConfig.SysCommands["ENV"] + " " + env.compileTags + " go build -o " + outputDir + " " + component.srcName 74 | _, err := dms3libs.RunCommand(command) 75 | dms3libs.CheckErr(err) 76 | } 77 | 78 | // CopyServiceDaemons copies daemons into release folder 79 | func CopyServiceDaemons() { 80 | 81 | fmt.Println("Copying dms3 service daemons into dms3_release folder...") 82 | 83 | dms3libs.CopyFile(filepath.Join(dms3libs.DMS3Client, "daemons", "systemd", "dms3client.service"), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Client, "dms3client.service")) 84 | dms3libs.CopyFile(filepath.Join(dms3libs.DMS3Server, "daemons", "systemd", "dms3server.service"), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Server, "dms3server.service")) 85 | 86 | } 87 | 88 | // CopyMediaFiles copies dms3server media files into release folder 89 | func CopyMediaFiles() { 90 | 91 | fmt.Println("Copying dms3server media files (WAV) into dms3_release folder...") 92 | 93 | dms3libs.CopyFile(filepath.Join(dms3libs.DMS3Server, media, "motion_start.wav"), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Server, media, "motion_start.wav")) 94 | dms3libs.CopyFile(filepath.Join(dms3libs.DMS3Server, media, "motion_stop.wav"), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Server, media, "motion_stop.wav")) 95 | 96 | } 97 | 98 | // CopyComponents copies component html files and assets into the release folder 99 | func CopyComponents(component string) { 100 | 101 | fmt.Println("Copying " + component + " file (HTML) into dms3_release folder...") 102 | dms3libs.CopyFile(filepath.Join(component, component+".html"), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, component, component+".html")) 103 | 104 | fmt.Println("Copying " + component + " assets into dms3_release folder...") 105 | dms3libs.CopyDir(filepath.Join(component, "assets"), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, component)) 106 | 107 | } 108 | 109 | // CopyConfigFiles copies config files into release folder 110 | func CopyConfigFiles() { 111 | 112 | fmt.Println("Copying dms3 component config files (TOML) into dms3_release folder...") 113 | 114 | for itr := range components { 115 | 116 | if components[itr].configFilename != "" { 117 | dms3libs.CopyFile(filepath.Join(dms3libs.DMS3Config, components[itr].configFilename), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, components[itr].dirName, components[itr].configFilename)) 118 | } 119 | 120 | } 121 | 122 | } 123 | 124 | // ConfirmReleaseFolder checks for the existence of the release folder 125 | func ConfirmReleaseFolder(releasePath string) { 126 | 127 | if !dms3libs.IsFile(releasePath) { 128 | dms3libs.CheckErr(errNoReleaseFolder) 129 | } 130 | 131 | } 132 | 133 | // InstallClientComponents installs dms3client components onto device platforms identified in 134 | // the dms3build.toml configuration file 135 | func InstallClientComponents(releasePath string) { 136 | 137 | var ssh *easyssh.MakeConfig 138 | 139 | fmt.Println("Installing dms3client components onto remote device(s) identified in dms3build.toml...") 140 | 141 | for _, client := range BuildConfig.Clients { 142 | 143 | ssh = &easyssh.MakeConfig{ 144 | User: client.User, 145 | Server: client.DeviceName, 146 | Password: client.SSHPassword, 147 | Port: strconv.Itoa(client.Port), 148 | } 149 | 150 | // copy dms3 release folder components to remote device platform 151 | remoteMkDir(ssh, dms3libs.DMS3Release) 152 | remoteCopyDir(ssh, filepath.Join(releasePath, dms3libs.DMS3Config, dms3libs.DMS3Client), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Client)) 153 | remoteCopyDir(ssh, filepath.Join(releasePath, dms3libs.DMS3Config, dms3libs.DMS3Libs), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Libs)) 154 | remoteCopyDir(ssh, filepath.Join(releasePath, dms3libs.DMS3Config, dms3libs.DMS3Mail), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Mail)) 155 | remoteCopyDir(ssh, filepath.Join(releasePath, dms3libs.DMS3Config, dms3libs.DMS3Dashboard), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Dashboard)) 156 | 157 | // copy dms3 release file components to remote device platform 158 | remoteMkDir(ssh, filepath.Join(dms3libs.DMS3Release, command)) 159 | remoteCopyFile(ssh, filepath.Join(releasePath, command, BuildEnv[client.Platform].dirName, dms3libs.DMS3Client), filepath.Join(dms3libs.DMS3Release, command, dms3libs.DMS3Client)) 160 | remoteCopyFile(ssh, filepath.Join(releasePath, command, BuildEnv[client.Platform].dirName, dms3libs.DMS3Mail), filepath.Join(dms3libs.DMS3Release, command, dms3libs.DMS3Mail)) 161 | remoteCopyFile(ssh, filepath.Join(releasePath, command, BuildEnv[client.Platform].dirName, "dms3client_remote_installer"), "dms3client_remote_installer") 162 | 163 | // run client installer, then remove on completion 164 | remoteRunCommand(ssh, "echo "+client.RemoteAdminPassword+" | sudo -S "+"."+string(filepath.Separator)+"dms3client_remote_installer") 165 | remoteRunCommand(ssh, "rm dms3client_remote_installer") 166 | 167 | } 168 | 169 | } 170 | 171 | // InstallServerComponents installs dms3server components onto device platforms identified in 172 | // the dms3build.toml configuration file 173 | func InstallServerComponents(releasePath string) { 174 | 175 | var ssh *easyssh.MakeConfig 176 | 177 | fmt.Println("Installing dms3server components onto remote device(s) identified in dms3build.toml...") 178 | 179 | for _, server := range BuildConfig.Servers { 180 | 181 | ssh = &easyssh.MakeConfig{ 182 | User: server.User, 183 | Server: server.DeviceName, 184 | Password: server.SSHPassword, 185 | Port: strconv.Itoa(server.Port), 186 | } 187 | 188 | // copy dms3 release folder components to remote device platform 189 | remoteMkDir(ssh, dms3libs.DMS3Release) 190 | remoteCopyDir(ssh, filepath.Join(releasePath, dms3libs.DMS3Config, dms3libs.DMS3Server), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Server)) 191 | remoteCopyDir(ssh, filepath.Join(releasePath, dms3libs.DMS3Config, dms3libs.DMS3Libs), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Libs)) 192 | remoteCopyDir(ssh, filepath.Join(releasePath, dms3libs.DMS3Config, dms3libs.DMS3Dashboard), filepath.Join(dms3libs.DMS3Release, dms3libs.DMS3Config, dms3libs.DMS3Dashboard)) 193 | 194 | remoteMkDir(ssh, filepath.Join(dms3libs.DMS3Release, command)) 195 | remoteCopyFile(ssh, filepath.Join(filepath.Join(releasePath, command, BuildEnv[server.Platform].dirName), dms3libs.DMS3Server), filepath.Join(dms3libs.DMS3Release, command, dms3libs.DMS3Server)) 196 | remoteCopyFile(ssh, filepath.Join(filepath.Join(releasePath, command, BuildEnv[server.Platform].dirName), "dms3server_remote_installer"), "dms3server_remote_installer") 197 | 198 | // run server installer, then remove on completion 199 | remoteRunCommand(ssh, "echo "+server.RemoteAdminPassword+" | sudo -S "+"."+string(filepath.Separator)+"dms3server_remote_installer") 200 | remoteRunCommand(ssh, "rm dms3server_remote_installer") 201 | } 202 | 203 | } 204 | 205 | // remoteMkDir creates a new folder over SSH with permissions passed in 206 | func remoteMkDir(ssh *easyssh.MakeConfig, newPath string) { 207 | remoteRunCommand(ssh, "mkdir -p "+newPath) 208 | } 209 | 210 | // remoteCopyDir copies a directory over SSH from srcDir to destDir 211 | func remoteCopyDir(ssh *easyssh.MakeConfig, srcDir string, destDir string) { 212 | 213 | fmt.Println("Copying folder " + srcDir + " to " + ssh.User + "@" + ssh.Server + ":" + destDir + "...") 214 | 215 | dirTree := dms3libs.WalkDir(srcDir) 216 | 217 | // create directory tree... 218 | for dirName, dirType := range dirTree { 219 | 220 | if dirType == 0 { 221 | remoteMkDir(ssh, destDir+dirName[len(srcDir):]) 222 | } 223 | 224 | } 225 | 226 | // ...then copy files into directory tree 227 | for fileName, dirType := range dirTree { 228 | 229 | if dirType == 1 { 230 | remoteCopyFile(ssh, fileName, destDir+fileName[len(srcDir):]) 231 | } 232 | 233 | } 234 | 235 | } 236 | 237 | // remoteRunCommand runs a command via the SSH protocol 238 | func remoteRunCommand(ssh *easyssh.MakeConfig, command string) { 239 | 240 | fmt.Println("Running remote command " + "'" + command + "' on " + ssh.User + "@" + ssh.Server + "...") 241 | 242 | _, _, _, err := ssh.Run(command, 5) // nolint (dogsled): ssh.Run() wants what it wants... 243 | dms3libs.CheckErr(err) 244 | 245 | } 246 | 247 | // remoteCopyFile copies a file from src to a remote dest file using SCP 248 | func remoteCopyFile(ssh *easyssh.MakeConfig, srcFile string, destFile string) { 249 | 250 | fmt.Println("Copying file " + srcFile + " to " + ssh.User + "@" + ssh.Server + ":" + destFile + "...") 251 | 252 | srcAttrib, err := os.Stat(srcFile) 253 | dms3libs.CheckErr(err) 254 | 255 | err = ssh.Scp(srcFile, destFile) 256 | dms3libs.CheckErr(err) 257 | 258 | // ssh.Scp() does not set file attribs, so reset them on remote device 259 | remoteRunCommand(ssh, "chmod 0"+strconv.FormatUint(uint64(srcAttrib.Mode()), 8)+" "+destFile) 260 | 261 | } 262 | -------------------------------------------------------------------------------- /dms3client/client_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3client configuration structures and variables 2 | package dms3client 3 | 4 | import ( 5 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 6 | ) 7 | 8 | // clientConfig contains dms3Client configuration settings read from TOML file 9 | var clientConfig *structSettings 10 | 11 | // client-side configuration parameters 12 | type structSettings struct { 13 | Server *structServer 14 | Logging *dms3libs.StructLogging 15 | } 16 | 17 | // server connection details 18 | type structServer struct { 19 | IP string 20 | Port int 21 | CheckInterval uint16 22 | } 23 | -------------------------------------------------------------------------------- /dms3client/client_connector.go: -------------------------------------------------------------------------------- 1 | // Package dms3client connector initializes the dms3client device component 2 | package dms3client 3 | 4 | import ( 5 | "fmt" 6 | "net" 7 | "path/filepath" 8 | "strconv" 9 | "time" 10 | 11 | dms3dash "github.com/richbl/go-distributed-motion-s3/dms3dashboard" 12 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 13 | ) 14 | 15 | // Init configs the library, configuration, and dashboard for dms3client 16 | func Init(configPath string) { 17 | 18 | dms3libs.InitComponent(configPath, dms3libs.DMS3Client, dms3libs.DMS3clientTOML, &clientConfig, clientConfig.Logging) 19 | 20 | dms3dash.InitDashboardClient(configPath, clientConfig.Server.CheckInterval) 21 | startClient(clientConfig.Server.IP, clientConfig.Server.Port) 22 | } 23 | 24 | // startClient periodically attempts to connect to the server (based on CheckInterval) 25 | func startClient(serverIP string, serverPort int) { 26 | 27 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 28 | 29 | for { 30 | 31 | if conn, err := net.Dial("tcp", serverIP+":"+fmt.Sprint(serverPort)); err != nil { 32 | dms3libs.LogInfo(err.Error()) 33 | } else { 34 | dms3libs.LogInfo("OPEN connection from: " + conn.RemoteAddr().String()) 35 | go processClientRequest(conn) 36 | } 37 | 38 | time.Sleep(time.Duration(clientConfig.Server.CheckInterval) * time.Second) 39 | } 40 | 41 | } 42 | 43 | // processClientRequest reads from the connection and processes dashboard and motion detector 44 | // application state 45 | func processClientRequest(conn net.Conn) { 46 | 47 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 48 | 49 | dms3dash.ReceiveDashboardRequest(conn) 50 | receiveMotionDetectorState(conn) 51 | 52 | dms3libs.LogInfo("CLOSE connection from: " + conn.RemoteAddr().String()) 53 | conn.Close() 54 | } 55 | 56 | // receiveMotionDetectorState receives motion detector state from the server 57 | func receiveMotionDetectorState(conn net.Conn) { 58 | 59 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 60 | 61 | buf := make([]byte, 8) 62 | 63 | // receive motion detector application state 64 | var n int 65 | var err error 66 | 67 | if n, err = conn.Read(buf); err != nil { 68 | dms3libs.LogInfo(err.Error()) 69 | return 70 | } 71 | 72 | val, err := strconv.Atoi(string(buf[:n])) 73 | if err != nil { 74 | dms3libs.LogInfo("Error converting motion detector state to integer: " + err.Error()) 75 | } 76 | 77 | state := dms3libs.MotionDetectorState(val) 78 | 79 | if dms3libs.MotionDetector.SetState(state) { 80 | ProcessMotionDetectorState() 81 | dms3libs.LogInfo("Received motion detector state as: " + strconv.Itoa(int(state))) 82 | 83 | return 84 | } 85 | 86 | dms3libs.LogInfo("Received unanticipated motion detector state: ignored") 87 | } 88 | -------------------------------------------------------------------------------- /dms3client/client_manager.go: -------------------------------------------------------------------------------- 1 | // Package dms3client manager processes dms3client device component messages passed to it by 2 | // the dms3server device component 3 | package dms3client 4 | 5 | import ( 6 | "path/filepath" 7 | 8 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 9 | ) 10 | 11 | // ProcessMotionDetectorState starts/stops the motion detector application 12 | func ProcessMotionDetectorState() { 13 | 14 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 15 | 16 | cmdStr := "" 17 | state := dms3libs.MotionDetector.State() 18 | 19 | switch state { 20 | case dms3libs.Start: 21 | cmdStr = "started" 22 | case dms3libs.Stop: 23 | cmdStr = "stopped" 24 | default: 25 | dms3libs.LogInfo("Unanticipated motion detector state: ignored") 26 | } 27 | 28 | motionCommand := dms3libs.LibConfig.SysCommands["MOTION"] 29 | 30 | if dms3libs.StartStopApplication(state, motionCommand) { 31 | dms3libs.LogInfo(motionCommand + " " + cmdStr) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /dms3client/daemons/systemd/dms3client.service: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Client Component Systemd Service Unit 2 | # 1.4.4 3 | 4 | [Unit] 5 | Description=Distributed Motion Sense Surveillance Service (DMS3) Client 6 | After=network.target 7 | 8 | [Service] 9 | Type=simple 10 | Restart=on-failure 11 | ExecStart=/usr/local/bin/dms3client 12 | 13 | # Set this value when running the Motion application 14 | # (see dms3libs.toml for additional details) 15 | # 16 | WorkingDirectory=/usr/local/etc/motion 17 | 18 | [Install] 19 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /dms3dashboard/assets/css/icomoon-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon', sans-serif; 3 | src: url('../fonts/icomoon.eot?trtp26'); 4 | src: url('../fonts/icomoon.eot?trtp26#iefix') format('embedded-opentype'), 5 | url('../fonts/icomoon.ttf?trtp26') format('truetype'), 6 | url('../fonts/icomoon.woff?trtp26') format('woff'), 7 | url('../fonts/icomoon.svg?trtp26#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'icomoon' !important; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .icon-television:before { 27 | content: "\e903"; 28 | } 29 | .icon-server2:before { 30 | content: "\e902"; 31 | } 32 | .icon-server:before { 33 | content: "\e901"; 34 | } 35 | .icon-raspberry-pi:before { 36 | content: "\e900"; 37 | } 38 | .icon-stopwatch:before { 39 | content: "\e952"; 40 | } 41 | .icon-display:before { 42 | content: "\e956"; 43 | } 44 | .icon-hour-glass:before { 45 | content: "\e979"; 46 | } 47 | .icon-eye:before { 48 | content: "\e9ce"; 49 | } 50 | .icon-loop2:before { 51 | content: "\ea2e"; 52 | } -------------------------------------------------------------------------------- /dms3dashboard/assets/css/paper-dashboard.css: -------------------------------------------------------------------------------- 1 | /*! 2 | ========================================================= 3 | * Paper Dashboard - v1.1.2 4 | ========================================================= 5 | 6 | * Product Page: http://www.creative-tim.com/product/paper-dashboard 7 | * Copyright 2017 Creative Tim (http://www.creative-tim.com) 8 | * Licensed under MIT (https://github.com/creativetimofficial/paper-dashboard/blob/master/LICENSE.md) 9 | 10 | ========================================================= 11 | 12 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | */ 14 | 15 | /* light colors - used for select dropdown */ 16 | 17 | p, 18 | .navbar, 19 | a { 20 | -moz-osx-font-smoothing: grayscale; 21 | -webkit-font-smoothing: antialiased; 22 | font-family: 'Muli', "Helvetica", Arial, sans-serif; 23 | } 24 | 25 | p { 26 | font-size: 18px; 27 | font-weight: bold; 28 | line-height: 1.4em; 29 | } 30 | 31 | .icon-info { 32 | fill: #68B3C8; 33 | color: #68B3C8; 34 | } 35 | 36 | .icon-success { 37 | fill: #4caf50; 38 | color: #4caf50; 39 | } 40 | 41 | .icon-warning { 42 | fill: #ff9800; 43 | color: #ff9800; 44 | } 45 | 46 | .icon-danger { 47 | fill: #f44336; 48 | color: #f44336; 49 | } 50 | 51 | 52 | /* General overwrite */ 53 | 54 | body { 55 | color: #66615b; 56 | font-size: 14px; 57 | font-family: 'Muli', Arial, sans-serif; 58 | } 59 | 60 | img { 61 | max-height: 70px; 62 | max-width: 100%; 63 | background-color: #ffffff; 64 | padding:10px 0; 65 | } 66 | 67 | body .wrapper { 68 | min-height: 100vh; 69 | position: relative; 70 | } 71 | 72 | a { 73 | color: #68B3C8; 74 | } 75 | 76 | a:hover, 77 | a:focus { 78 | color: #3091B2; 79 | text-decoration: none; 80 | } 81 | 82 | a:focus, 83 | a:active { 84 | outline: 0 !important; 85 | } 86 | 87 | 88 | /* Animations */ 89 | 90 | hr { 91 | border-color: #F1EAE0; 92 | } 93 | 94 | .wrapper { 95 | position: relative; 96 | top: 0; 97 | height: 100vh; 98 | } 99 | 100 | .main-panel { 101 | background-color: #f4f3ef; 102 | position: relative; 103 | z-index: 2; 104 | min-height: 100%; 105 | overflow: auto; 106 | max-height: 100%; 107 | height: 100%; 108 | -webkit-transition-property: top, bottom; 109 | transition-property: top, bottom; 110 | -webkit-transition-duration: .2s, .2s; 111 | transition-duration: .2s, .2s; 112 | -webkit-transition-timing-function: linear, linear; 113 | transition-timing-function: linear, linear; 114 | -webkit-overflow-scrolling: touch; 115 | } 116 | 117 | .main-panel>.content { 118 | padding: 30px 15px; 119 | min-height: calc(100% - 123px); 120 | } 121 | 122 | .main-panel .navbar { 123 | margin-bottom: 0; 124 | } 125 | 126 | /* Checkbox and radio */ 127 | 128 | .navbar { 129 | border: 0; 130 | border-radius: 0; 131 | font-size: 16px; 132 | z-index: 3; 133 | -webkit-transition: all 300ms linear; 134 | -moz-transition: all 300ms linear; 135 | -o-transition: all 300ms linear; 136 | -ms-transition: all 300ms linear; 137 | transition: all 300ms linear; 138 | } 139 | 140 | .navbar .navbar-brand { 141 | font-weight: 600; 142 | margin: 5px 0px; 143 | padding: 20px 15px; 144 | font-size: 20px; 145 | } 146 | 147 | .navbar-default { 148 | background-color: #ffffff; 149 | border-bottom: 1px solid #DDDDDD; 150 | } 151 | 152 | .container-fluid > .navbar-header, 153 | .container > .navbar-header { 154 | margin-right: 0px; 155 | margin-left: 0px; 156 | } 157 | 158 | .footer { 159 | background-attachment: fixed; 160 | position: relative; 161 | line-height: 20px; 162 | } 163 | 164 | .card { 165 | border-radius: 6px; 166 | box-shadow: 0 2px 2px rgba(204, 197, 185, 0.5); 167 | background-color: #FFFFFF; 168 | color: #252422; 169 | margin-bottom: 20px; 170 | position: relative; 171 | z-index: 1; 172 | } 173 | 174 | .card .content { 175 | padding: 15px 15px 10px 15px; 176 | } 177 | 178 | .card .footer { 179 | padding: 0; 180 | } 181 | 182 | .card .footer hr { 183 | margin-top: 5px; 184 | margin-bottom: 5px; 185 | } 186 | 187 | .card .stats { 188 | color: #a9a9a9; 189 | font-weight: 300; 190 | } 191 | 192 | .card .stats i { 193 | margin-right: 2px; 194 | min-width: 15px; 195 | display: inline-block; 196 | } 197 | 198 | .card .footer div { 199 | display: inline-block; 200 | } 201 | 202 | .card .icon-big { 203 | font-size: 4.2em; 204 | min-height: 64px; 205 | } 206 | 207 | .card .numbers { 208 | font-size: 16px; 209 | line-height: 1.4em; 210 | text-align: right; 211 | } 212 | 213 | .card .numbers p { 214 | margin: 0; 215 | } 216 | 217 | @media (min-width: 992px) { 218 | .navbar { 219 | min-height: 75px; 220 | } 221 | .navbar .navbar-header { 222 | margin-left: 10px; 223 | } 224 | } 225 | 226 | 227 | /* Changes for small display */ 228 | 229 | @media (max-width: 991px) { 230 | .main-panel { 231 | width: 100%; 232 | } 233 | body { 234 | position: relative; 235 | } 236 | .wrapper { 237 | -webkit-transform: translate3d(0px, 0, 0); 238 | -moz-transform: translate3d(0px, 0, 0); 239 | -o-transform: translate3d(0px, 0, 0); 240 | -ms-transform: translate3d(0px, 0, 0); 241 | transform: translate3d(0px, 0, 0); 242 | -webkit-transition: all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1); 243 | -moz-transition: all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1); 244 | -o-transition: all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1); 245 | -ms-transition: all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1); 246 | transition: all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1); 247 | left: 0; 248 | background-color: white; 249 | } 250 | .navbar-header { 251 | float: none; 252 | } 253 | .main-panel>.content { 254 | padding-left: 0; 255 | padding-right: 0; 256 | } 257 | } 258 | 259 | .logo { 260 | display: block; 261 | text-indent: -9999px; 262 | width: 100px; 263 | height: 82px; 264 | background: url(kiwi.svg); 265 | background-size: 100px 82px; 266 | } -------------------------------------------------------------------------------- /dms3dashboard/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3dashboard/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /dms3dashboard/assets/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /dms3dashboard/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3dashboard/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /dms3dashboard/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3dashboard/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /dms3dashboard/assets/img/dms3logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3dashboard/assets/img/dms3logo.png -------------------------------------------------------------------------------- /dms3dashboard/assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3dashboard/assets/img/favicon.png -------------------------------------------------------------------------------- /dms3dashboard/assets/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 37 | 43 | 47 | 51 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /dms3dashboard/dashboard_client.go: -------------------------------------------------------------------------------- 1 | // Package dms3dash client implements client services for a dms3server-based metrics dashboard 2 | package dms3dash 3 | 4 | import ( 5 | "bytes" 6 | "encoding/gob" 7 | "net" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 12 | ) 13 | 14 | var dashboardClientMetrics *DeviceMetrics 15 | 16 | // InitDashboardClient loads configuration and assigns the dashboard client profile (sets static client metrics) 17 | func InitDashboardClient(configPath string, checkInterval uint16) { 18 | 19 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 20 | 21 | dashboardConfig = new(tomlTables) 22 | dms3libs.LoadComponentConfig(&dashboardConfig, filepath.Join(configPath, dms3libs.DMS3Dashboard, dms3libs.DMS3dashboardTOML)) 23 | 24 | dashboardClientMetrics = initializeDeviceMetrics(Client, checkInterval) 25 | dashboardClientMetrics.checkImagesFolder() 26 | } 27 | 28 | // ReceiveDashboardRequest receives server requests and returns data 29 | func ReceiveDashboardRequest(conn net.Conn) { 30 | 31 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 32 | 33 | if receiveDashboardEnableState(conn) { 34 | sendDashboardData(conn) 35 | } 36 | 37 | } 38 | 39 | // receiveDashboardEnableState parses the server dashboard state notification, returning true 40 | // if the dashboard state is enabled 41 | func receiveDashboardEnableState(conn net.Conn) bool { 42 | 43 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 44 | 45 | buf := make([]byte, 16) 46 | var n int 47 | var err error 48 | 49 | if n, err = conn.Read(buf); err != nil { 50 | dms3libs.LogFatal(err.Error()) 51 | return false 52 | } 53 | 54 | val := string(buf[:n]) 55 | dms3libs.LogInfo("Received dashboard enable state as: " + val) 56 | 57 | return (val == "1") 58 | } 59 | 60 | // sendDashboardData sends dashboard info to server 61 | func sendDashboardData(conn net.Conn) { 62 | 63 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 64 | 65 | // update client metrics 66 | dashboardClientMetrics.Period.LastReport = time.Now() 67 | dashboardClientMetrics.Period.Uptime = dms3libs.Uptime(dashboardClientMetrics.Period.StartTime) 68 | 69 | if dashboardClientMetrics.ShowEventCount { 70 | dashboardClientMetrics.EventCount = dms3libs.CountFilesInDir(dashboardConfig.Client.ImagesFolder) 71 | } 72 | 73 | // gob encoding of client metrics 74 | encBuf := new(bytes.Buffer) 75 | 76 | if err := gob.NewEncoder(encBuf).Encode(dashboardClientMetrics); err != nil { 77 | dms3libs.LogFatal(err.Error()) 78 | } 79 | 80 | if _, err := conn.Write(encBuf.Bytes()); err != nil { 81 | dms3libs.LogFatal(err.Error()) 82 | } else { 83 | dms3libs.LogInfo("Sent client dashboard data") 84 | } 85 | 86 | } 87 | 88 | // checkImagesFolder confirms the location of the motion-triggered image/movie files managed by 89 | // the motion detector application (if installed), and used in displaying client metrics in the 90 | // dashboard 91 | func (dash *DeviceMetrics) checkImagesFolder() { 92 | 93 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 94 | 95 | if dms3libs.IsFile(dashboardConfig.Client.ImagesFolder) { 96 | dashboardClientMetrics.ShowEventCount = true 97 | } else { 98 | 99 | if dashboardConfig.Client.ImagesFolder == "" { 100 | dashboardClientMetrics.ShowEventCount = false 101 | } else { 102 | dms3libs.LogFatal("Unable to find motion detector application images folder... check TOML configuration file") 103 | } 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /dms3dashboard/dashboard_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3dash dashboard configuration structures and variables 2 | package dms3dash 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 8 | ) 9 | 10 | var DashboardEnable bool 11 | 12 | var dashboardConfig *tomlTables 13 | var dashboardData *deviceData 14 | 15 | // tomlTables represents the TOML table(s) 16 | type tomlTables struct { 17 | Client *clientKeyValues 18 | Server *serverKeyValues 19 | } 20 | 21 | // clientKeyValues represents the k-v pairs in the TOML file 22 | type clientKeyValues struct { 23 | ImagesFolder string 24 | } 25 | 26 | // serverKeyValues represents the k-v pairs in the TOML file 27 | type serverKeyValues struct { 28 | Port int 29 | Filename string 30 | FileLocation string 31 | Title string 32 | ReSort bool 33 | ServerFirst bool 34 | DeviceStatus *serverDeviceStatus 35 | } 36 | 37 | // serverDeviceStatus represents the device status cycle in the TOML file 38 | type serverDeviceStatus struct { 39 | Caution uint32 40 | Danger uint32 41 | Missing uint32 42 | } 43 | 44 | // deviceData represents dashboard elements from all devices 45 | type deviceData struct { 46 | Title string 47 | Devices []DeviceMetrics 48 | } 49 | 50 | // DeviceMetrics represents device data presented on the dashboard 51 | type DeviceMetrics struct { 52 | Platform DevicePlatform 53 | Period DeviceTime 54 | ShowEventCount bool 55 | EventCount int 56 | } 57 | 58 | // DevicePlatform represents the physical device platform environment 59 | type DevicePlatform struct { 60 | Type dashboardDeviceType 61 | Hostname string 62 | OSName string 63 | Environment string 64 | Kernel string 65 | } 66 | 67 | // DeviceTime represents device time/duration metrics 68 | type DeviceTime struct { 69 | CheckInterval uint16 70 | StartTime time.Time 71 | Uptime string 72 | LastReport time.Time 73 | } 74 | 75 | // dashboardDeviceType defines the dashboard device type 76 | type dashboardDeviceType int 77 | 78 | // types of DMS3 devices 79 | const ( 80 | Client dashboardDeviceType = iota 81 | Server 82 | ) 83 | 84 | // initializeDeviceMetrics is a helper function to initialize a DeviceMetrics struct. 85 | func initializeDeviceMetrics(deviceType dashboardDeviceType, checkInterval uint16) *DeviceMetrics { 86 | return &DeviceMetrics{ 87 | Platform: DevicePlatform{ 88 | Type: deviceType, 89 | Hostname: dms3libs.GetDeviceHostname(), 90 | OSName: dms3libs.GetDeviceOSName(), 91 | Environment: dms3libs.GetDeviceDetails(dms3libs.Sysname) + " " + dms3libs.GetDeviceDetails(dms3libs.Machine), 92 | Kernel: dms3libs.GetDeviceDetails(dms3libs.Release), 93 | }, 94 | Period: DeviceTime{ 95 | CheckInterval: checkInterval, 96 | StartTime: time.Now(), 97 | Uptime: "", 98 | LastReport: time.Now(), 99 | }, 100 | ShowEventCount: false, 101 | EventCount: 0, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /dms3dashboard/dashboard_server.go: -------------------------------------------------------------------------------- 1 | // Package dms3dash server implements a dms3server-based metrics dashboard for all dms3clients 2 | package dms3dash 3 | 4 | import ( 5 | "bytes" 6 | "encoding/gob" 7 | "fmt" 8 | "html/template" 9 | "net" 10 | "net/http" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | "time" 15 | 16 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 17 | ) 18 | 19 | // InitDashboardServer configs the library and server configuration for the dashboard 20 | func InitDashboardServer(configPath string, checkInterval uint16) { 21 | 22 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 23 | 24 | dashboardConfig = new(tomlTables) 25 | dms3libs.LoadComponentConfig(&dashboardConfig, filepath.Join(configPath, dms3libs.DMS3Dashboard, dms3libs.DMS3dashboardTOML)) 26 | dms3libs.CheckFileLocation(configPath, dms3libs.DMS3Dashboard, &dashboardConfig.Server.FileLocation, dashboardConfig.Server.Filename) 27 | 28 | dashboardData = &deviceData{ 29 | Title: "", 30 | Devices: []DeviceMetrics{}, 31 | } 32 | 33 | // create initial server device entry in set of all dashboard devices 34 | serverData := initializeDeviceMetrics(Server, checkInterval) 35 | 36 | dashboardData.Devices = append(dashboardData.Devices, *serverData) 37 | go dashboardConfig.Server.startDashboard(configPath) 38 | } 39 | 40 | // SendDashboardRequest manages dashboard requests and receipt of client device data 41 | func SendDashboardRequest(conn net.Conn) { 42 | 43 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 44 | 45 | if DashboardEnable { 46 | sendDashboardEnableState(conn, "1") 47 | receiveDashboardData(conn) 48 | } else { 49 | sendDashboardEnableState(conn, "0") 50 | } 51 | 52 | } 53 | 54 | // startDashboard initializes and starts an HTTP server, serving the client dash on the server 55 | func (dash *serverKeyValues) startDashboard(configPath string) { 56 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 57 | 58 | // Template functions 59 | funcs := template.FuncMap{ 60 | "ModVal": dms3libs.ModVal, 61 | "FormatDateTime": dms3libs.FormatDateTime, 62 | "iconStatus": iconStatus, 63 | "iconType": iconType, 64 | "clientCount": clientCount, 65 | "showEventCount": showEventCount, 66 | } 67 | 68 | // Parse template 69 | tmpl := template.Must(template.New(dash.Filename).Funcs(funcs).ParseFiles(filepath.Join(dash.FileLocation, dash.Filename))) 70 | 71 | // File server for static assets 72 | fs := http.FileServer(http.Dir(filepath.Join(configPath, dms3libs.DMS3Dashboard, "assets"))) 73 | http.Handle("/assets/", http.StripPrefix("/assets/", fs)) 74 | 75 | // Dashboard handler 76 | http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { 77 | handleDashboardRequest(w, tmpl, dash.Title) 78 | }) 79 | 80 | // Configure HTTP server with timeouts 81 | server := &http.Server{ 82 | Addr: ":" + fmt.Sprint(dash.Port), 83 | ReadTimeout: 15 * time.Second, 84 | WriteTimeout: 15 * time.Second, 85 | IdleTimeout: 60 * time.Second, 86 | } 87 | 88 | // Start server 89 | dms3libs.LogInfo(fmt.Sprintf("Starting dashboard server on port %d", dash.Port)) 90 | if err := server.ListenAndServe(); err != nil { 91 | dms3libs.LogFatal(err.Error()) 92 | } 93 | } 94 | 95 | // handleDashboardRequest processes requests for the dashboard 96 | func handleDashboardRequest(w http.ResponseWriter, tmpl *template.Template, title string) { 97 | 98 | dashboardData := &deviceData{ 99 | Title: title, 100 | Devices: dashboardData.Devices, 101 | } 102 | 103 | dashboardData.updateServerMetrics() 104 | 105 | if err := tmpl.Execute(w, dashboardData); err != nil { 106 | dms3libs.LogFatal(err.Error()) 107 | } 108 | 109 | } 110 | 111 | // updateServerMetrics updates dynamic dashboard data of the server, triggered 112 | // initially on dashboard start and subsequent webpage refreshes 113 | func (dd *deviceData) updateServerMetrics() { 114 | 115 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 116 | 117 | for i := range dd.Devices { 118 | 119 | if dd.Devices[i].Platform.Type == Server { 120 | dd.Devices[i].Period.LastReport = time.Now() 121 | dd.Devices[i].Period.Uptime = dms3libs.Uptime(dd.Devices[i].Period.StartTime) 122 | } else { 123 | // check for and remove dead (non-reporting) client devices 124 | lastUpdate := dms3libs.SecondsSince(dd.Devices[i].Period.LastReport) 125 | missingDeviceLimit := uint32(dd.Devices[i].Period.CheckInterval) * dashboardConfig.Server.DeviceStatus.Missing 126 | 127 | if lastUpdate > missingDeviceLimit { 128 | dms3libs.LogInfo("Non-reporting remote device timeout reached: removing " + dd.Devices[i].Platform.Hostname + " client") 129 | dd.Devices = append(dd.Devices[:i], dd.Devices[i+1:]...) 130 | 131 | break 132 | } 133 | 134 | } 135 | } 136 | 137 | } 138 | 139 | // sendDashboardEnableState asks clients to send client info based on dashboard state 140 | func sendDashboardEnableState(conn net.Conn, enableState string) { 141 | 142 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 143 | 144 | if _, err := conn.Write([]byte(enableState)); err != nil { 145 | dms3libs.LogFatal(err.Error()) 146 | } else { 147 | dms3libs.LogInfo("Sent dashboard enable state as: " + enableState) 148 | } 149 | 150 | } 151 | 152 | // receiveDashboardData receives and parses client dashboard metrics 153 | func receiveDashboardData(conn net.Conn) { 154 | 155 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 156 | 157 | updatedDeviceMetrics := new(DeviceMetrics) 158 | buf := make([]byte, 1024) 159 | 160 | if n, err := conn.Read(buf); err != nil { 161 | dms3libs.LogFatal(err.Error()) 162 | } else { 163 | 164 | decBuf := bytes.NewBuffer(buf[:n]) // gob decoding of client metrics 165 | 166 | if err := gob.NewDecoder(decBuf).Decode(updatedDeviceMetrics); err != nil { 167 | dms3libs.LogFatal(err.Error()) 168 | } 169 | 170 | updatedDeviceMetrics.updateDeviceMetrics() 171 | } 172 | 173 | } 174 | 175 | // updateDeviceMetrics adds new devices to the dashboard list, or updates existing device 176 | // metrics, where Hostname is the unique key 177 | func (udm *DeviceMetrics) updateDeviceMetrics() { 178 | 179 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 180 | 181 | // scan for existing client device 182 | for i := range dashboardData.Devices { 183 | 184 | if dashboardData.Devices[i].Platform.Hostname == udm.Platform.Hostname { 185 | 186 | if dashboardData.Devices[i].Platform.Type == Client { 187 | dashboardData.Devices[i].EventCount = udm.EventCount 188 | dashboardData.Devices[i].Period.LastReport = udm.Period.LastReport 189 | dashboardData.Devices[i].Period.Uptime = udm.Period.Uptime 190 | dashboardData.Devices[i].Platform.OSName = udm.Platform.OSName 191 | dashboardData.Devices[i].Platform.Environment = udm.Platform.Environment 192 | dashboardData.Devices[i].Platform.Kernel = udm.Platform.Kernel 193 | 194 | return 195 | } 196 | 197 | } 198 | 199 | } 200 | 201 | // add new client device and (optionally) resort device order 202 | dashboardData.Devices = append(dashboardData.Devices, *udm) 203 | 204 | if dashboardConfig.Server.ReSort { 205 | resortDashboardDevices() 206 | } 207 | 208 | } 209 | 210 | // resortDashboardDevices re-sorts all dashboard devices alphabetically 211 | func resortDashboardDevices() { 212 | 213 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 214 | 215 | sort.Slice(dashboardData.Devices, func(i, j int) bool { 216 | 217 | switch strings.Compare(dashboardData.Devices[i].Platform.Hostname, dashboardData.Devices[j].Platform.Hostname) { 218 | case -1: 219 | return true 220 | case 1: 221 | return false 222 | } 223 | 224 | return dashboardData.Devices[i].Platform.Hostname > dashboardData.Devices[j].Platform.Hostname 225 | 226 | }) 227 | 228 | if dashboardConfig.Server.ServerFirst { 229 | 230 | // place server in first dashboard position 231 | for i := range dashboardData.Devices { 232 | 233 | if dashboardData.Devices[i].Platform.Type == Server { 234 | server := dashboardData.Devices[i] 235 | dashboardData.Devices = append(dashboardData.Devices[:i], dashboardData.Devices[i+1:]...) 236 | dashboardData.Devices = append([]DeviceMetrics{server}, dashboardData.Devices...) 237 | } 238 | 239 | } 240 | 241 | } 242 | 243 | } 244 | 245 | // iconStatus is an HTML template function that returns the CSS string representing icon color, 246 | // depending on the last time the client reported status to the server, relative to the client's 247 | // CheckInterval 248 | func iconStatus(index int) string { 249 | 250 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 251 | 252 | lastUpdate := dms3libs.SecondsSince(dashboardData.Devices[index].Period.LastReport) 253 | checkInterval := dashboardData.Devices[index].Period.CheckInterval 254 | warningLimit := uint32(checkInterval) * dashboardConfig.Server.DeviceStatus.Caution 255 | dangerLimit := uint32(checkInterval) * dashboardConfig.Server.DeviceStatus.Danger 256 | 257 | switch { 258 | case lastUpdate < warningLimit: 259 | return "icon-success" 260 | case (lastUpdate >= warningLimit) && (lastUpdate < dangerLimit): 261 | return "icon-warning" 262 | case lastUpdate >= dangerLimit: 263 | return "icon-danger" 264 | default: 265 | return "" 266 | } 267 | 268 | } 269 | 270 | // iconType is an HTML template function that returns an icon based on device type 271 | func iconType(index int) string { 272 | 273 | dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 274 | 275 | switch dashboardData.Devices[index].Platform.Type { 276 | case Client: 277 | return "icon-raspberry-pi" 278 | case Server: 279 | return "icon-server2" 280 | default: 281 | return "" 282 | } 283 | 284 | } 285 | 286 | // deviceType is an HTML template function that returns a string based on device type 287 | // func deviceType(index int) string { 288 | 289 | // dms3libs.LogDebug(filepath.Base((dms3libs.GetFunctionName()))) 290 | 291 | // switch dashboardData.Devices[index].Platform.Type { 292 | // case Client: 293 | // return "client" 294 | // case Server: 295 | // return "server" 296 | // default: 297 | // return "" 298 | // } 299 | 300 | // } 301 | 302 | // clientCount is an HTML template function that returns the current count of dms3clients 303 | // reporting to the server 304 | func clientCount() int { 305 | return len(dashboardData.Devices) - 1 306 | } 307 | 308 | // showEventCount is an HTML template function that returns whether to display client event count 309 | func showEventCount(index int) bool { 310 | return dashboardData.Devices[index].ShowEventCount 311 | } 312 | -------------------------------------------------------------------------------- /dms3dashboard/dms3dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{.Title}} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 | 39 | 40 |
41 |
42 | 43 | {{range $index, $_ := .Devices}} 44 | 45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 |

{{.Platform.Hostname}}

57 | {{.Platform.OSName}} 58 |
59 | {{.Platform.Environment}} 60 |
61 | {{.Platform.Kernel}} 62 |
63 |
64 |
65 | 85 | 86 |
87 |
88 |
89 | 90 | {{end}} 91 | 92 |
93 |
94 | 95 |
96 |
97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /dms3libs/lib_audio.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs audio provides audio services for dms3 device components 2 | package dms3libs 3 | 4 | // PlayAudio uses the shell aplay command (system default) to play audioFile 5 | func PlayAudio(audioFile string) { 6 | 7 | if _, err := RunCommand(LibConfig.SysCommands["APLAY"] + " -q " + audioFile); err != nil { 8 | LogInfo(err.Error()) 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /dms3libs/lib_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs configuration structures and variables 2 | package dms3libs 3 | 4 | import ( 5 | "os" 6 | "path" 7 | "path/filepath" 8 | 9 | "github.com/BurntSushi/toml" 10 | ) 11 | 12 | // Configuration file names 13 | const ( 14 | DMS3Mail = "dms3mail" 15 | DMS3Client = "dms3client" 16 | DMS3Server = "dms3server" 17 | DMS3Libs = "dms3libs" 18 | DMS3Release = "dms3_release" 19 | DMS3Dashboard = "dms3dashboard" 20 | DMS3Config = "config" 21 | 22 | DMS3clientTOML = "dms3client.toml" 23 | DMS3serverTOML = "dms3server.toml" 24 | DMS3libsTOML = "dms3libs.toml" 25 | DMS3buildTOML = "dms3build.toml" 26 | DMS3dashboardTOML = "dms3dashboard.toml" 27 | DMS3mailTOML = "dms3mail.toml" 28 | ) 29 | 30 | // LibConfig contains dms3Libs configuration settings read from TOML file 31 | var LibConfig *structConfig 32 | 33 | type structConfig struct { 34 | SysCommands mapSysCommands 35 | } 36 | 37 | // mapSysCommands provides a location mapping of required system commands 38 | type mapSysCommands map[string]string 39 | 40 | // LoadLibConfig loads a TOML configuration file of system commands into parameter values 41 | func LoadLibConfig(configFile string) { 42 | 43 | if IsFile(configFile) { 44 | 45 | if _, err := toml.DecodeFile(configFile, &LibConfig); err != nil { 46 | LogFatal(err.Error()) 47 | } 48 | 49 | } else { 50 | LogFatal(configFile + " file not found") 51 | } 52 | 53 | } 54 | 55 | // LoadComponentConfig loads a TOML configuration file of client/server configs into parameter values 56 | func LoadComponentConfig(structConfig interface{}, configFile string) { 57 | 58 | // check if config file exists 59 | if _, err := os.Stat(configFile); err != nil { 60 | LogFatal(err.Error()) 61 | } 62 | 63 | if _, err := toml.DecodeFile(configFile, structConfig); err != nil { 64 | LogFatal(err.Error()) 65 | } 66 | 67 | } 68 | 69 | // SetLogFileLocation sets the location of the log file based on TOML configuration 70 | func SetLogFileLocation(config *StructLogging) { 71 | 72 | projectDir := path.Dir(GetPackageDir()) 73 | 74 | if !IsFile(config.LogLocation) { 75 | 76 | if config.LogLocation == "" && IsFile(projectDir) { // if no config location set, set to development project folder 77 | config.LogLocation = projectDir 78 | } else { 79 | LogFatal("unable to set log location... check TOML configuration file") 80 | } 81 | 82 | } 83 | 84 | } 85 | 86 | // Setup performs common installer tasks and takes a list of binary and config files to copy 87 | func DeviceInstaller(binFiles, configDirs []string) { 88 | 89 | // Load libs config file from dms3_release folder on remote device 90 | configPath := filepath.Join(DMS3Release, DMS3Config, DMS3Libs, DMS3clientTOML) 91 | LoadLibConfig(configPath) 92 | 93 | binaryInstallDir := filepath.Join(string(filepath.Separator), "usr", "local", "bin") 94 | configInstallDir := filepath.Join(string(filepath.Separator), "etc", "distributed-motion-s3") 95 | logDir := filepath.Join(string(filepath.Separator), "var", "log", "dms3") 96 | 97 | // Create log folder 98 | MkDir(logDir) 99 | 100 | // Copy binary files into binaryInstallDir 101 | for _, binFile := range binFiles { 102 | src := filepath.Join(DMS3Release, "cmd", binFile) 103 | dst := filepath.Join(binaryInstallDir, binFile) 104 | CopyFile(src, dst) 105 | } 106 | 107 | // Create config folder and copy configuration files 108 | MkDir(configInstallDir) 109 | for _, configDir := range configDirs { 110 | src := filepath.Join(DMS3Release, DMS3Config, configDir) 111 | CopyDir(src, configInstallDir) 112 | } 113 | 114 | // Remove the release directory 115 | RmDir(DMS3Release) 116 | } 117 | 118 | // InitComponent initializes the common configuration and logging for a dms3 component. 119 | func InitComponent(configPath, componentName, componentTOML string, config interface{}, logging *StructLogging) { 120 | 121 | LogDebug(filepath.Base(GetFunctionName())) 122 | 123 | LoadLibConfig(filepath.Join(configPath, DMS3Libs, DMS3libsTOML)) 124 | LoadComponentConfig(config, filepath.Join(configPath, componentName, componentTOML)) 125 | 126 | SetLogFileLocation(logging) 127 | CreateLogger(logging) 128 | 129 | LogInfo(componentName + " " + GetProjectVersion() + " started") 130 | } 131 | -------------------------------------------------------------------------------- /dms3libs/lib_detector_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs provides motion detector application services for dms3 device components 2 | package dms3libs 3 | 4 | // MotionDetector represents the motion detector application running on the clients (e.g., motion) 5 | var MotionDetector = motionDetectorStruct{ 6 | state: Stop, 7 | } 8 | 9 | // states of the motion detector application 10 | const ( 11 | Stop MotionDetectorState = iota 12 | Start 13 | ) 14 | 15 | // MotionDetectorStruct encapsulates the state of the motion detector 16 | type motionDetectorStruct struct { 17 | state MotionDetectorState 18 | } 19 | 20 | // MotionDetectorState defines the motion detector application state type 21 | type MotionDetectorState int 22 | 23 | // State returns the motion detector application state 24 | func (s motionDetectorStruct) State() MotionDetectorState { 25 | return s.state 26 | } 27 | 28 | // SetState sets the motion detector application state 29 | func (s *motionDetectorStruct) SetState(state MotionDetectorState) bool { 30 | 31 | switch state { 32 | case Start, Stop: 33 | { 34 | s.state = state 35 | return true 36 | } 37 | default: 38 | return false 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /dms3libs/lib_file.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs file provides file services for dms3 device components 2 | package dms3libs 3 | 4 | import ( 5 | "errors" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | // IsFile returns true/false on existence of file/folder passed in 13 | func IsFile(filename string) bool { 14 | 15 | _, err := os.Stat(filename) 16 | return (!errors.Is(err, fs.ErrNotExist)) 17 | 18 | } 19 | 20 | // MkDir creates a new folder with permissions passed in 21 | func MkDir(newPath string) { 22 | 23 | err := os.MkdirAll(newPath, os.ModePerm) 24 | CheckErr(err) 25 | 26 | } 27 | 28 | // RmDir removes the folder passed in 29 | func RmDir(dir string) { 30 | 31 | if IsFile(dir) { 32 | err := os.RemoveAll(dir) 33 | CheckErr(err) 34 | } 35 | 36 | } 37 | 38 | // WalkDir generates a map of directories (0) and files (1) 39 | func WalkDir(dirname string) map[string]int { 40 | 41 | fileList := map[string]int{} 42 | err := filepath.WalkDir(dirname, func(path string, f os.DirEntry, _ error) error { 43 | 44 | // exclude root directory 45 | if f.IsDir() && f.Name() == dirname { 46 | return nil 47 | } 48 | 49 | if f.IsDir() { 50 | fileList[path] = 0 // directory 51 | } else { 52 | fileList[path] = 1 // file 53 | } 54 | 55 | return nil 56 | }) 57 | 58 | CheckErr(err) 59 | 60 | return fileList 61 | 62 | } 63 | 64 | // CopyFile copies a file from src to dest 65 | func CopyFile(src string, dest string) { 66 | 67 | srcFile, err := os.Open(src) 68 | CheckErr(err) 69 | defer srcFile.Close() 70 | 71 | destFile, err := os.Create(dest) 72 | CheckErr(err) 73 | defer destFile.Close() 74 | 75 | _, err = io.Copy(destFile, srcFile) 76 | CheckErr(err) 77 | 78 | err = destFile.Sync() 79 | CheckErr(err) 80 | 81 | srcAttrib, err := os.Stat(src) 82 | CheckErr(err) 83 | 84 | err = os.Chmod(dest, srcAttrib.Mode()) 85 | CheckErr(err) 86 | 87 | } 88 | 89 | // CopyDir copies a directory from srcDir to destDir 90 | func CopyDir(srcDir string, destDir string) { 91 | 92 | pathRoot := filepath.Dir(srcDir) 93 | 94 | if pathRoot == "." { 95 | pathRoot = "" 96 | } 97 | 98 | dirTree := WalkDir(srcDir) 99 | 100 | // create directory tree... 101 | for dirName, dirType := range dirTree { 102 | 103 | if dirType == 0 { 104 | MkDir(filepath.Join(destDir, dirName[len(pathRoot):])) 105 | } 106 | 107 | } 108 | 109 | // ...then copy files into directory tree 110 | for dirName, dirType := range dirTree { 111 | 112 | if dirType == 1 { 113 | CopyFile(dirName, filepath.Join(destDir, dirName[len(pathRoot):])) 114 | } 115 | 116 | } 117 | 118 | } 119 | 120 | // CountFilesInDir recursively counts the files in the dir passed in 121 | func CountFilesInDir(srcDir string) int { 122 | 123 | fileCount := 0 124 | dirTree := WalkDir(srcDir) 125 | 126 | for _, dirType := range dirTree { 127 | 128 | if dirType == 1 { 129 | fileCount++ 130 | } 131 | 132 | } 133 | 134 | return fileCount 135 | 136 | } 137 | 138 | // CheckFileLocation checks/sets the location of file and pathname passed in as defined in various 139 | // TOML config files, returning a fully qualified path 140 | func CheckFileLocation(configPath string, fileDir string, fileLocation *string, filename string) { 141 | 142 | // set default template location 143 | if *fileLocation == "" { 144 | *fileLocation = filepath.Join(configPath, fileDir) 145 | } 146 | 147 | if filename == "" || !IsFile(filepath.Join(*fileLocation, filename)) { 148 | LogFatal("unable to set file location... check TOML configuration file") 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /dms3libs/lib_log.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs log provides logging services for dms3 device components 2 | package dms3libs 3 | 4 | import ( 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // global logging objects 11 | var ( 12 | Fatal *log.Logger 13 | Info *log.Logger 14 | Debug *log.Logger 15 | loggingLevel int 16 | ) 17 | 18 | // StructLogging is used for dms3 logging 19 | type StructLogging struct { 20 | LogLevel int 21 | LogDevice int 22 | LogFilename string 23 | LogLocation string 24 | } 25 | 26 | func init() { 27 | 28 | var f *os.File 29 | Fatal = log.New(f, "FATAL: ", log.Lshortfile|log.LstdFlags) 30 | Info = log.New(f, "INFO: ", log.Lshortfile|log.LstdFlags) 31 | Debug = log.New(f, "DEBUG: ", log.Lshortfile|log.LstdFlags) 32 | 33 | } 34 | 35 | // CreateLogger creates an application log file (1) or redirects to STDOUT (2) based on logDevice 36 | func CreateLogger(logger *StructLogging) { 37 | 38 | var ( 39 | f *os.File 40 | err error 41 | ) 42 | 43 | if logger.LogLevel > 0 { 44 | loggingLevel = logger.LogLevel 45 | 46 | switch logger.LogDevice { 47 | case 0: 48 | f = os.Stdout 49 | case 1: 50 | { 51 | if f, err = os.OpenFile(filepath.Join(logger.LogLocation, logger.LogFilename), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644); err != nil { 52 | LogFatal(err.Error()) 53 | } 54 | } 55 | } 56 | 57 | } 58 | 59 | Fatal.SetOutput(f) 60 | Info.SetOutput(f) 61 | Debug.SetOutput(f) 62 | 63 | } 64 | 65 | // LogFatal generates a fatal log message based on loggingLevel 66 | func LogFatal(msg string) { 67 | 68 | if loggingLevel >= 1 { 69 | Fatal.Fatalln(msg) 70 | } 71 | 72 | os.Exit(1) 73 | 74 | } 75 | 76 | // LogInfo generates an informational log message based on loggingLevel 77 | func LogInfo(msg string) { 78 | 79 | if loggingLevel >= 2 { 80 | Info.Println(msg) 81 | } 82 | 83 | } 84 | 85 | // LogDebug generates a debug log message based on loggingLevel 86 | func LogDebug(msg string) { 87 | 88 | if loggingLevel >= 4 { 89 | Debug.Println(msg) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /dms3libs/lib_network.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs network provides networking services for dms3 device components 2 | package dms3libs 3 | 4 | import ( 5 | "os/exec" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | // PingHosts uses 'ping' to ping a range of IP addresses, returning an error if the command fails 11 | func PingHosts(ipBase string, ipRange []int) error { 12 | 13 | var wg sync.WaitGroup 14 | cmd := LibConfig.SysCommands["PING"] + " -q -W 1 -c 1 " + ipBase 15 | 16 | for i := ipRange[0]; i <= ipRange[1]; i++ { 17 | wg.Add(1) 18 | 19 | // allow threaded system command calls to finish asynchronously 20 | go func(i int, w *sync.WaitGroup) { 21 | defer w.Done() 22 | 23 | if _, err := RunCommand(cmd + strconv.Itoa(i)); err != nil { 24 | LogInfo("Failed to run command: " + err.Error()) 25 | } 26 | 27 | }(i, &wg) 28 | } 29 | wg.Wait() 30 | 31 | return nil 32 | } 33 | 34 | // FindMacs uses 'ip neigh' to find mac address(es) passed in, returning true if any mac passed in is found 35 | // (e.g., mac1|mac2|mac3) 36 | func FindMacs(macsToFind []string) bool { 37 | 38 | macListRegex := "" 39 | 40 | for i := 0; i < len(macsToFind); i++ { 41 | macListRegex += macsToFind[i] 42 | 43 | if i < len(macsToFind)-1 { 44 | macListRegex += "|" 45 | } 46 | 47 | } 48 | 49 | cmd := LibConfig.SysCommands["IP"] + " neigh | " + LibConfig.SysCommands["GREP"] + " -iE '" + macListRegex + "'" 50 | 51 | if _, err := RunCommand(cmd); err != nil { 52 | 53 | switch err.(type) { 54 | case *exec.ExitError: 55 | LogInfo(LibConfig.SysCommands["IP"] + " command: no device mac address found") 56 | default: 57 | LogFatal("Failed to run '" + LibConfig.SysCommands["IP"] + " neigh | " + LibConfig.SysCommands["GREP"] + " -iE '" + macListRegex + "': " + err.Error()) 58 | } 59 | 60 | return false 61 | } 62 | 63 | LogInfo(LibConfig.SysCommands["IP"] + " command: device mac address found") 64 | 65 | return true 66 | } 67 | -------------------------------------------------------------------------------- /dms3libs/lib_os.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs OS provides operating system information services for dms3 device components 2 | package dms3libs 3 | 4 | import ( 5 | "bufio" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | "syscall" 11 | ) 12 | 13 | // DeviceDetails defines the set of available device details available in GetDeviceDetails 14 | type DeviceDetails int 15 | 16 | // types of DMS3 devices 17 | const ( 18 | Sysname DeviceDetails = iota 19 | Machine 20 | Release 21 | ) 22 | 23 | // GetDeviceHostname returns the name of the local machine 24 | func GetDeviceHostname() string { 25 | 26 | name, err := os.Hostname() 27 | CheckErr(err) 28 | 29 | return name 30 | } 31 | 32 | // GetDeviceOSName returns the OS release name (NAME) and version ID (VERSION_ID) from a parse of 33 | // the /etc/os-release file found in most Linux-based distributions 34 | func GetDeviceOSName() string { 35 | 36 | result := "OS unknown" 37 | 38 | if file, err := os.Open(filepath.Join(string(filepath.Separator), "etc", "os-release")); err == nil { 39 | 40 | defer file.Close() 41 | scanner := bufio.NewScanner(file) 42 | 43 | nameRegx := regexp.MustCompile(`^NAME=(.*)$`) 44 | versionIDRegx := regexp.MustCompile(`^VERSION_ID=(.*)$`) 45 | osName := "" 46 | osVersion := "" 47 | 48 | for scanner.Scan() { 49 | 50 | if res := nameRegx.FindStringSubmatch(scanner.Text()); res != nil { 51 | osName = strings.Trim(res[1], `"`) 52 | } else if res := versionIDRegx.FindStringSubmatch(scanner.Text()); res != nil { 53 | osVersion = strings.Trim(res[1], `"`) 54 | } 55 | 56 | } 57 | 58 | if osName != "" && osVersion != "" { 59 | result = strings.ToLower(osName + " " + osVersion) 60 | } 61 | 62 | } 63 | 64 | return result 65 | } 66 | 67 | // GetDeviceDetails returns device details of the local machine 68 | func GetDeviceDetails(element DeviceDetails) string { 69 | 70 | utsName, err := uname() 71 | CheckErr(err) 72 | 73 | var length int 74 | var buf [65]byte 75 | 76 | switch element { 77 | case Sysname: 78 | for ; utsName.Sysname[length] != 0; length++ { 79 | buf[length] = byte(utsName.Sysname[length]) 80 | } 81 | case Machine: 82 | for ; utsName.Machine[length] != 0; length++ { 83 | buf[length] = byte(utsName.Machine[length]) 84 | } 85 | case Release: 86 | for ; utsName.Release[length] != 0; length++ { 87 | buf[length] = byte(utsName.Release[length]) 88 | } 89 | default: 90 | LogFatal("invalid DeviceDetails element passed in") 91 | } 92 | 93 | return strings.ToLower(string(buf[:length])) 94 | } 95 | 96 | // uname returns the Utsname struct used to query system settings 97 | func uname() (*syscall.Utsname, error) { 98 | 99 | uts := &syscall.Utsname{} 100 | 101 | if err := syscall.Uname(uts); err != nil { 102 | return nil, err 103 | } 104 | 105 | return uts, nil 106 | 107 | } 108 | -------------------------------------------------------------------------------- /dms3libs/lib_process.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs process provides process-related services for dms3 device components 2 | package dms3libs 3 | 4 | import ( 5 | "errors" 6 | "os/exec" 7 | ) 8 | 9 | var ( 10 | ErrInvalidConfiguration = errors.New("invalid configuration") 11 | ErrNoCommand = errors.New("empty command provided") 12 | ) 13 | 14 | // isRunning checks if application is currently running (has PID > 0) 15 | func isRunning(application string) bool { 16 | 17 | LogInfo("Check if already running: " + application) 18 | cmd := LibConfig.SysCommands["PGREP"] + " -if " + "'" + application + "'" 19 | 20 | var err error 21 | 22 | if _, err = RunCommand(cmd); err == nil { 23 | LogInfo("Already running: " + application) 24 | return true 25 | } 26 | 27 | handleCommandErrors(err, cmd) 28 | 29 | return false 30 | } 31 | 32 | // handleCommandErrors handles errors encountered while running commands 33 | func handleCommandErrors(err error, cmd string) { 34 | 35 | switch err.(type) { 36 | case *exec.ExitError: 37 | LogInfo("Process not found when running " + cmd) 38 | default: 39 | LogFatal("Failed to run " + cmd + ": " + err.Error()) 40 | } 41 | 42 | } 43 | 44 | // startApplication starts the specified application if it is not already running 45 | func startApplication(application string) bool { 46 | 47 | // check already running 48 | if isRunning(application) { 49 | return false 50 | } 51 | 52 | LogInfo("Attempting to start: " + application) 53 | 54 | var err error 55 | 56 | if _, err = RunCommand(application); err == nil { 57 | LogInfo("Successfully started: " + application) 58 | return true 59 | } 60 | 61 | handleCommandErrors(err, application) 62 | 63 | return false 64 | } 65 | 66 | // stopApplication stops the specified application if it is currently running 67 | func stopApplication(application string) bool { 68 | 69 | // check already stopped 70 | if !isRunning(application) { 71 | return false 72 | } 73 | 74 | LogInfo("Attempting to stop: " + application) 75 | cmd := LibConfig.SysCommands["PKILL"] + " -if " + "'" + application + "'" 76 | 77 | var err error 78 | 79 | if _, err = RunCommand(cmd); err == nil { 80 | LogInfo("Successfully stopped: " + application) 81 | return true 82 | } 83 | 84 | handleCommandErrors(err, cmd) 85 | 86 | return false 87 | } 88 | 89 | // RunCommand is a simple wrapper for the exec.Command() call 90 | // 91 | // NOTE: this call is blocking (non-threaded) 92 | func RunCommand(cmd string) (res []byte, err error) { 93 | 94 | LogInfo("Command to be run: " + LibConfig.SysCommands["BASH"] + " -c " + cmd) 95 | 96 | // nolint (gosec): this is a closed application (not a package/library) 97 | return exec.Command(LibConfig.SysCommands["BASH"], "-c", cmd).Output() 98 | } 99 | 100 | // StartStopApplication enables/disables the application passed in based on state 101 | func StartStopApplication(state MotionDetectorState, application string) bool { 102 | 103 | switch state { 104 | case Start: 105 | return startApplication(application) 106 | case Stop: 107 | return stopApplication(application) 108 | default: 109 | LogInfo("Unanticipated application state: ignored") 110 | return false 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /dms3libs/lib_util.go: -------------------------------------------------------------------------------- 1 | // Package dms3libs util provides utility services for dms3 device components 2 | package dms3libs 3 | 4 | import ( 5 | "fmt" 6 | "image" 7 | _ "image/jpeg" // Import JPEG decoder 8 | "log" 9 | "os" 10 | "path" 11 | "runtime" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // GetFunctionName uses reflection (runtime) to return current function name 17 | func GetFunctionName() string { 18 | 19 | pc := make([]uintptr, 10) 20 | 21 | // get program counter index (call stack) 22 | runtime.Callers(2, pc) 23 | fn := runtime.FuncForPC(pc[0]) 24 | 25 | return fn.Name() 26 | } 27 | 28 | // GetPackageDir returns the absolute path of the calling package 29 | func GetPackageDir() string { 30 | 31 | _, filename, _, ok := runtime.Caller(1) 32 | if !ok { 33 | log.Fatal() 34 | } 35 | 36 | return path.Dir(filename) 37 | } 38 | 39 | // StripRet strips the rightmost byte from the byte array 40 | func StripRet(value []byte) []byte { 41 | 42 | if len(value) <= 1 { 43 | return value 44 | } 45 | 46 | return value[:len(value)-1] 47 | } 48 | 49 | // Uptime returns uptime for the application process 50 | func Uptime(startTime time.Time) string { 51 | return fmtDuration(time.Since(startTime)) 52 | } 53 | 54 | // SecondsSince returns seconds passed since value passed 55 | func SecondsSince(value time.Time) uint32 { 56 | return uint32(time.Since(value).Seconds()) 57 | } 58 | 59 | // To24H converts 12-hour time to 24-hour time, returning a string (e.g., "231305") 60 | func To24H(value time.Time) string { 61 | return value.Format("150405") 62 | } 63 | 64 | // Format24H formats 24-hour time to six places (HHMMSS) 65 | func Format24H(time string) string { 66 | return rightPadToLen(time, "0", 6) 67 | } 68 | 69 | // FormatDateTime formats time to "date at time" 70 | func FormatDateTime(value time.Time) string { 71 | return value.Format("2006-01-02 at 15:04:05") 72 | } 73 | 74 | // ModVal returns the remainder of number/val passed in 75 | func ModVal(number int, val int) int { 76 | return number % val 77 | } 78 | 79 | // CheckErr does simple error management (no logging dependencies) 80 | func CheckErr(err error) { 81 | 82 | if err != nil { 83 | fmt.Println(err.Error()) 84 | os.Exit(1) 85 | } 86 | 87 | } 88 | 89 | // GetProjectVersion returns the current project version string to the caller 90 | func GetProjectVersion() string { 91 | return "1.4.4" 92 | } 93 | 94 | // GetImageDimensions returns the (width, height) of an image passed in 95 | func GetImageDimensions(imagePath string) (int, int) { 96 | 97 | var file *os.File 98 | var err error 99 | var img image.Config 100 | 101 | if file, err = os.Open(imagePath); err != nil { 102 | LogFatal(err.Error()) 103 | } 104 | 105 | defer file.Close() 106 | 107 | if img, _, err = image.DecodeConfig(file); err != nil { 108 | LogFatal(err.Error()) 109 | } 110 | 111 | return img.Width, img.Height 112 | 113 | } 114 | 115 | // rightPadToLen pads a string to pLen places with padStr 116 | func rightPadToLen(s string, padStr string, pLen int) string { 117 | return s + strings.Repeat(padStr, pLen-len(s)) 118 | } 119 | 120 | // fmtDuration formats a duration value to show days/hours/minutes/seconds 121 | func fmtDuration(d time.Duration) string { 122 | 123 | days := (d / time.Hour) / 24 124 | d -= days * (time.Hour * 24) 125 | hours := d / time.Hour 126 | d -= hours * time.Hour 127 | minutes := d / time.Minute 128 | d -= minutes * time.Minute 129 | seconds := d / time.Second 130 | 131 | return fmt.Sprintf("%03dd:%02dh:%02dm:%02ds", days, hours, minutes, seconds) 132 | } 133 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_audio_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 8 | "github.com/richbl/go-distributed-motion-s3/dms3server" 9 | ) 10 | 11 | const ( 12 | successfulPlayback = "successful playback" 13 | errNotFound = "not found" 14 | audioFile = "audio file" 15 | testFile = "Test file" 16 | ) 17 | 18 | func init() { 19 | dms3libs.LoadLibConfig(filepath.Join("..", "..", dms3libs.DMS3Config, dms3libs.DMS3libsTOML)) 20 | } 21 | 22 | func TestPlayAudio(t *testing.T) { 23 | 24 | testFile := "lib_audio_test.wav" 25 | 26 | if dms3libs.IsFile(testFile) { 27 | dms3libs.PlayAudio(testFile) 28 | t.Log(testFile, testFile, successfulPlayback) 29 | } else { 30 | t.Error(testFile, testFile, errNotFound) 31 | } 32 | 33 | } 34 | 35 | func TestAudioConfig(t *testing.T) { 36 | 37 | configPath := dms3libs.GetPackageDir() 38 | 39 | dms3libs.LoadComponentConfig(&dms3server.ServerConfig, filepath.Join(configPath, "..", "..", dms3libs.DMS3Config, dms3libs.DMS3serverTOML)) 40 | 41 | mediaFileStart := dms3server.ServerConfig.Audio.PlayMotionStart 42 | mediaFileStop := dms3server.ServerConfig.Audio.PlayMotionStop 43 | 44 | if mediaFileStart == "" { 45 | mediaFileStart = filepath.Join("..", "..", dms3libs.DMS3Server, "media", "motion_start.wav") 46 | } 47 | 48 | if dms3libs.IsFile(mediaFileStart) { 49 | dms3libs.PlayAudio(mediaFileStart) 50 | t.Log(audioFile, mediaFileStart, successfulPlayback) 51 | } else { 52 | t.Error(audioFile, mediaFileStart, errNotFound) 53 | } 54 | 55 | if mediaFileStop == "" { 56 | mediaFileStop = filepath.Join("..", "..", dms3libs.DMS3Server, "media", "motion_stop.wav") 57 | } 58 | 59 | if dms3libs.IsFile(mediaFileStop) { 60 | dms3libs.PlayAudio(mediaFileStop) 61 | t.Log(audioFile, mediaFileStop, successfulPlayback) 62 | } else { 63 | t.Error(audioFile, mediaFileStop, errNotFound) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_audio_test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3libs/tests/lib_audio_test.wav -------------------------------------------------------------------------------- /dms3libs/tests/lib_config_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 8 | ) 9 | 10 | func TestLoadLibConfig(t *testing.T) { 11 | 12 | libsLocation := filepath.Join("..", "..", dms3libs.DMS3Config, dms3libs.DMS3libsTOML) 13 | 14 | dms3libs.LoadLibConfig(libsLocation) 15 | t.Log("libs configuration file loaded from", libsLocation, "successfully") 16 | 17 | } 18 | 19 | func TestConfiguration(t *testing.T) { 20 | 21 | for k, v := range dms3libs.LibConfig.SysCommands { 22 | 23 | if dms3libs.IsFile(v) { 24 | t.Log(k, "confirmed at", v) 25 | } else { 26 | t.Error(k, "not found at", v) 27 | } 28 | 29 | } 30 | 31 | } 32 | 33 | func TestLoadComponentConfig(t *testing.T) { 34 | 35 | type structServer struct { 36 | Port int 37 | CheckInterval int 38 | Logging *dms3libs.StructLogging 39 | } 40 | 41 | type structSettings struct { 42 | Server *structServer 43 | } 44 | 45 | testSettings := new(structSettings) 46 | configPath := dms3libs.GetPackageDir() 47 | configLocation := filepath.Join("..", "..", dms3libs.DMS3Config, dms3libs.DMS3serverTOML) 48 | 49 | dms3libs.LoadComponentConfig(&testSettings, filepath.Join(configPath, configLocation)) 50 | t.Log("component configuration file loaded from", configLocation, "successfully") 51 | 52 | } 53 | 54 | func TestSetLogFileLocation(t *testing.T) { 55 | 56 | testSettings := new(dms3libs.StructLogging) 57 | testSettings.LogLocation = "" 58 | 59 | dms3libs.SetLogFileLocation(testSettings) 60 | t.Log("log location set to", testSettings.LogLocation, "successfully") 61 | 62 | } 63 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_detector_config_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 7 | ) 8 | 9 | const ( 10 | errFunctionFailed = "function failed" 11 | ) 12 | 13 | func TestCommand(t *testing.T) { 14 | 15 | if dms3libs.LibConfig.SysCommands["MOTION"] == "" { 16 | t.Error(errFunctionFailed) 17 | } 18 | 19 | } 20 | 21 | func TestState(t *testing.T) { 22 | 23 | dms3libs.MotionDetector.SetState(dms3libs.Start) 24 | 25 | if dms3libs.MotionDetector.State() != dms3libs.Start { 26 | t.Error(errFunctionFailed) 27 | } 28 | 29 | } 30 | 31 | func TestSetState(t *testing.T) { 32 | 33 | if !dms3libs.MotionDetector.SetState(dms3libs.Start) { 34 | t.Error(errFunctionFailed) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_file_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 9 | ) 10 | 11 | const ( 12 | tmpDir = "tmpDir" 13 | tmpFile = "tmpFile" 14 | wavFile = "lib_audio_test.wav" 15 | ) 16 | 17 | func TestIsFile(t *testing.T) { 18 | 19 | testFile := "lib_file_test.go" 20 | 21 | if !dms3libs.IsFile(testFile) { 22 | t.Error(testFile + " file not found, but should have been") 23 | } 24 | 25 | } 26 | 27 | func TestMkDir(t *testing.T) { 28 | 29 | newDir := filepath.Join(dms3libs.GetPackageDir(), tmpDir) 30 | dms3libs.MkDir(newDir) 31 | 32 | if dms3libs.IsFile(newDir) { 33 | dms3libs.CheckErr(os.RemoveAll(newDir)) 34 | } else { 35 | t.Error("directory " + newDir + " not found, but should have been") 36 | } 37 | 38 | } 39 | 40 | func TestRmDir(t *testing.T) { 41 | 42 | newDir := filepath.Join(dms3libs.GetPackageDir(), tmpDir) 43 | 44 | dms3libs.MkDir(newDir) 45 | dms3libs.RmDir(newDir) 46 | 47 | if dms3libs.IsFile(newDir) { 48 | t.Error("directory not removed, but should have been") 49 | } 50 | 51 | } 52 | 53 | func TestWalkDir(t *testing.T) { 54 | 55 | dirCount := 0 56 | fileCount := 0 57 | 58 | newDir := filepath.Join(dms3libs.GetPackageDir(), tmpDir) 59 | newFile := tmpFile 60 | 61 | dms3libs.MkDir(newDir) 62 | dms3libs.CopyFile(filepath.Join(dms3libs.GetPackageDir(), wavFile), filepath.Join(newDir, newFile)) 63 | 64 | for _, dirType := range dms3libs.WalkDir(newDir) { 65 | 66 | if dirType == 0 { 67 | dirCount++ 68 | } else { 69 | fileCount++ 70 | } 71 | 72 | } 73 | 74 | if dirCount != 1 { 75 | t.Error("wrong directory count in", newDir) 76 | } 77 | 78 | if fileCount != 1 { 79 | t.Error("wrong file count in", newDir) 80 | } 81 | 82 | // avoid using os.RemoveAll() in the event of an error with newDir creation 83 | os.Remove(newDir + "/" + newFile) 84 | os.Remove(newDir) 85 | 86 | } 87 | 88 | func TestCopyFile(t *testing.T) { 89 | 90 | dms3libs.CopyFile(filepath.Join(dms3libs.GetPackageDir(), wavFile), tmpFile) 91 | 92 | if dms3libs.IsFile(tmpFile) { 93 | os.Remove(tmpFile) 94 | } else { 95 | t.Error("file not found, but should have been") 96 | } 97 | 98 | } 99 | 100 | func TestCopyDir(t *testing.T) { 101 | 102 | dms3libs.CopyDir(dms3libs.GetPackageDir(), tmpDir) 103 | 104 | if dms3libs.IsFile(tmpDir) { 105 | dms3libs.RmDir(filepath.Join(dms3libs.GetPackageDir(), tmpDir)) 106 | } else { 107 | t.Error("directory not found, but should have been") 108 | } 109 | 110 | } 111 | 112 | func TestCountFilesInDir(t *testing.T) { 113 | 114 | dms3libs.MkDir(filepath.Join(dms3libs.GetPackageDir(), tmpDir)) 115 | dms3libs.CopyFile(filepath.Join(dms3libs.GetPackageDir(), wavFile), filepath.Join(dms3libs.GetPackageDir(), tmpDir, tmpFile)) 116 | currentDir := filepath.Join(dms3libs.GetPackageDir(), tmpDir) 117 | 118 | if dms3libs.CountFilesInDir(currentDir) != 1 { 119 | t.Error("incorrect file count") 120 | } 121 | 122 | dms3libs.RmDir(currentDir) 123 | 124 | } 125 | 126 | func TestCheckFileLocation(t *testing.T) { 127 | 128 | dms3libs.MkDir(filepath.Join(dms3libs.GetPackageDir(), tmpDir)) 129 | dms3libs.CopyFile(filepath.Join(dms3libs.GetPackageDir(), wavFile), filepath.Join(dms3libs.GetPackageDir(), tmpDir, tmpFile)) 130 | currentDir := filepath.Join(dms3libs.GetPackageDir(), tmpDir) 131 | 132 | configPath := dms3libs.GetPackageDir() 133 | fileDir := tmpDir 134 | fileLocation := "" 135 | filename := tmpFile 136 | 137 | dms3libs.CheckFileLocation(configPath, fileDir, &fileLocation, filename) 138 | 139 | if fileLocation == "" { 140 | t.Error("fileLocation not set") 141 | } 142 | 143 | dms3libs.RmDir(currentDir) 144 | 145 | } 146 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_log_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 8 | ) 9 | 10 | const ( 11 | logFile = "tmpFile" 12 | ) 13 | 14 | func TestCreateLogger(t *testing.T) { 15 | 16 | logger := dms3libs.StructLogging{ 17 | LogLevel: 2, 18 | LogDevice: 1, 19 | LogFilename: "lib_log_test.log", 20 | LogLocation: dms3libs.GetPackageDir(), 21 | } 22 | 23 | dms3libs.CreateLogger(&logger) 24 | 25 | if !dms3libs.IsFile(logger.LogFilename) { 26 | t.Error(logFile, logger.LogFilename, "not created") 27 | } else { 28 | t.Log(logFile, logger.LogFilename, "created") 29 | os.Remove(logger.LogFilename) 30 | t.Log(logFile, logger.LogFilename, "removed") 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_network_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 11 | ) 12 | 13 | var ( 14 | ErrNoMacFound = errors.New("no MAC address found") 15 | ) 16 | 17 | func init() { 18 | dms3libs.LoadLibConfig(filepath.Join("..", "..", dms3libs.DMS3Config, dms3libs.DMS3libsTOML)) 19 | } 20 | 21 | func TestPingHosts(t *testing.T) { 22 | 23 | // ACTION: set active IP net address details 24 | testIPBase := "10.10.10." 25 | testIPRange := []int{100, 150} 26 | 27 | if err := dms3libs.PingHosts(testIPBase, testIPRange); err != nil { 28 | t.Error("PingHosts failed to ping hosts", err) 29 | } 30 | 31 | } 32 | 33 | // TestFindMacs tests the FindMacs function by first finding a local MAC address to establish 34 | // a true case, and then passing it into the FindMacs() function 35 | func TestFindMacs(t *testing.T) { 36 | 37 | var localMAC []string 38 | 39 | // Get a local MAC address for testing 40 | if res, err := getMACAddress(); err != nil { 41 | t.Error("Unable to determine local MAC address for testing") 42 | } else { 43 | localMAC = append(localMAC, res) 44 | } 45 | 46 | // Test the FindMacs function 47 | if !dms3libs.FindMacs(localMAC) { 48 | t.Error("FindMacs failed to find local MAC address") 49 | } 50 | 51 | } 52 | 53 | // getMACAddress returns the MAC address of the first device found on the network 54 | func getMACAddress() (string, error) { 55 | 56 | // Execute the `ip neigh` command 57 | var out []byte 58 | var err error 59 | out, err = dms3libs.RunCommand(dms3libs.LibConfig.SysCommands["IP"] + " neigh") 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | // Define a regex pattern to match MAC addresses 65 | macRegex := regexp.MustCompile(`([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})`) 66 | matches := macRegex.FindAllString(string(out), -1) 67 | 68 | // Check if any MAC addresses were found 69 | if len(matches) == 0 { 70 | return "", ErrNoMacFound 71 | } 72 | 73 | return strings.TrimSpace(matches[0]), nil 74 | } 75 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_os_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 7 | ) 8 | 9 | func TestDeviceHostname(t *testing.T) { 10 | 11 | val := dms3libs.GetDeviceHostname() 12 | 13 | if val != "" { 14 | t.Log("Success, devicehost is", val) 15 | } else { 16 | t.Error("Failure. Unable to find devicehost") 17 | } 18 | 19 | } 20 | 21 | func TestDeviceOSName(t *testing.T) { 22 | 23 | val := dms3libs.GetDeviceOSName() 24 | 25 | if val != "" { 26 | t.Log("Success, device OS name is", val) 27 | } else { 28 | t.Error("Failure. Unable to find device OS name") 29 | } 30 | 31 | } 32 | 33 | func TestGetDeviceDetails(t *testing.T) { 34 | 35 | val := dms3libs.GetDeviceDetails(dms3libs.Sysname) 36 | 37 | if val != "" { 38 | t.Log("Success, device platform is", val) 39 | } else { 40 | t.Error("Failure. Unable to find device details") 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_process_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 8 | ) 9 | 10 | func init() { 11 | dms3libs.LoadLibConfig(filepath.Join("..", "..", dms3libs.DMS3Config, dms3libs.DMS3libsTOML)) 12 | } 13 | 14 | func TestRunCommand(t *testing.T) { 15 | 16 | testCommand := dms3libs.LibConfig.SysCommands["ENV"] 17 | 18 | if res, err := dms3libs.RunCommand(testCommand); err != nil { 19 | t.Error("Command " + testCommand + " failed") 20 | } else if len(res) == 0 { 21 | t.Error("Output from command " + testCommand + " failed") 22 | } 23 | 24 | } 25 | 26 | func TestStartStopApplication(t *testing.T) { 27 | 28 | // NOTE: assumes Motion application (https://motion-project.github.io/) is installed 29 | // and properly configured 30 | testApplication := dms3libs.LibConfig.SysCommands["MOTION"] 31 | 32 | if !dms3libs.StartStopApplication(dms3libs.Start, testApplication) { 33 | t.Error("Start failed") 34 | } 35 | 36 | if !dms3libs.StartStopApplication(dms3libs.Stop, testApplication) { 37 | t.Error("Stop failed") 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_util_test.go: -------------------------------------------------------------------------------- 1 | package dms3libs_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | "time" 7 | 8 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 9 | ) 10 | 11 | func TestGetFunctionName(t *testing.T) { 12 | 13 | val := filepath.Base(dms3libs.GetFunctionName()) 14 | 15 | if val != "" { 16 | t.Log("Success, function name is", val) 17 | } else { 18 | t.Error("Failure. Unable to get function name") 19 | } 20 | 21 | } 22 | 23 | func TestGetPackageDir(t *testing.T) { 24 | 25 | val := dms3libs.GetPackageDir() 26 | 27 | if val != "" { 28 | t.Log("Success, package dir is", val) 29 | } else { 30 | t.Error("Failure. Unable to get package dir") 31 | } 32 | 33 | } 34 | 35 | func TestStripRet(t *testing.T) { 36 | 37 | testArray := []byte{50, 40, 30, 20, 10} 38 | res := dms3libs.StripRet(testArray) 39 | 40 | if len(res) != len(testArray)-1 { 41 | t.Error("function failed") 42 | } 43 | 44 | } 45 | 46 | func TestUptime(t *testing.T) { 47 | 48 | testTime := time.Now() 49 | time.Sleep(1000 * time.Millisecond) // force uptime value 50 | val := dms3libs.Uptime(testTime) 51 | 52 | if val != "000d:00h:00m:00s" { 53 | t.Log("Success, uptime is", val) 54 | } else { 55 | t.Error("Failure. Unable to get uptime") 56 | } 57 | 58 | } 59 | 60 | func TestSecondsSince(t *testing.T) { 61 | 62 | testTime := time.Now() 63 | time.Sleep(1000 * time.Millisecond) 64 | val := dms3libs.SecondsSince(testTime) 65 | 66 | if val >= 1 { 67 | t.Log("Success, seconds since", testTime, "is", val) 68 | } else { 69 | t.Error("Failure. Unable to get seconds since", testTime) 70 | } 71 | 72 | } 73 | 74 | func TestTo24H(t *testing.T) { 75 | 76 | testTime := time.Now() 77 | val := dms3libs.To24H(testTime) 78 | 79 | if val == testTime.Format("150405") { 80 | t.Log("Success, current time to 24H format is", val) 81 | } else { 82 | t.Error("Failure. Unable to convert current time") 83 | } 84 | 85 | } 86 | 87 | func TestFormatDateTime(t *testing.T) { 88 | 89 | testTime := time.Now() 90 | val := dms3libs.FormatDateTime(testTime) 91 | 92 | if val == testTime.Format("2006-01-02 at 15:04:05") { 93 | t.Log("Success, current time formatted is", val) 94 | } else { 95 | t.Error("Failure. Unable to format time") 96 | } 97 | 98 | } 99 | 100 | func TestModVal(t *testing.T) { 101 | 102 | dividend := 30 103 | divisor := 10 104 | val := dms3libs.ModVal(dividend, divisor) 105 | 106 | if val == 0 { 107 | t.Log("Success, ModVal(", dividend, ",", divisor, ") is", val) 108 | } else { 109 | t.Error("Failure. ModVal(", dividend, ",", divisor, ") yielded", val) 110 | } 111 | 112 | } 113 | 114 | func TestGetImageDimensions(t *testing.T) { 115 | 116 | imageFile := filepath.Join(dms3libs.GetPackageDir(), "lib_util_test.jpg") 117 | w, h := dms3libs.GetImageDimensions(imageFile) 118 | 119 | if w != 100 && h != 100 { 120 | t.Error("Failure. Dimensions do not match image") 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /dms3libs/tests/lib_util_test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3libs/tests/lib_util_test.jpg -------------------------------------------------------------------------------- /dms3mail/assets/img/dms3github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3mail/assets/img/dms3github.png -------------------------------------------------------------------------------- /dms3mail/assets/img/dms3logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3mail/assets/img/dms3logo.png -------------------------------------------------------------------------------- /dms3mail/dms3mail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | DMS3 Motion Detected 15 | 16 | 24 | 25 | 44 | 45 | 52 | 53 | 54 | 55 | 56 |
57 | 62 | 63 |
64 | 65 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 84 | 85 | 86 | 87 | 88 | 91 | 92 | 93 | 94 | 95 | 114 | 115 | 116 | 117 | 118 | 129 | 130 | 131 |
78 | 79 | 82 |
80 | distributed-motion-s3 81 |
83 |
89 | security image capture 90 |
96 | 97 | 98 | 111 | 112 |
99 |

100 | A DMS3 Motion 101 | Surveillance Event was Detected

102 |
    103 |
  • 104 | {{.Change}}% of image pixels changed in the event
  • 105 |
  • Event occurred at {{.Date}}
  • 106 |
  • Event reported by DMS3 device client 107 | {{.Client}} 108 |
  • 109 |
110 |
113 |
119 | 120 | 121 | 126 | 127 |
122 | 123 | GitHub Logo 124 | Contribute to the DMS3 Project on Github 125 |
128 |
132 | 133 | 138 | 139 |
140 | 141 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /dms3mail/mail_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3mail configuration structures and variables 2 | package dms3mail 3 | 4 | import "github.com/richbl/go-distributed-motion-s3/dms3libs" 5 | 6 | // mailConfig contains dms3mail configuration settings read from TOML file 7 | var mailConfig *structEmailSettings 8 | 9 | // motion mail configuration parameters 10 | type structEmailSettings struct { 11 | Filename string 12 | FileLocation string 13 | Email *structEmail 14 | SMTP *structSMTP 15 | Logging *dms3libs.StructLogging 16 | } 17 | 18 | // email composition parameters 19 | type structEmail struct { 20 | To string 21 | From string 22 | } 23 | 24 | // SMTP mailer parameters 25 | type structSMTP struct { 26 | Address string 27 | Port int 28 | Username string 29 | Password string 30 | } 31 | 32 | // event details to be sent via email 33 | type structEventDetails struct { 34 | eventMedia string 35 | eventDate string 36 | eventChange string 37 | clientName string 38 | } 39 | 40 | // HTML tokens used in email template 41 | type emailTemplateElements struct { 42 | Header string 43 | Event string 44 | Date string 45 | Client string 46 | Change string 47 | Footer string 48 | } 49 | -------------------------------------------------------------------------------- /dms3mail/motion_mail.go: -------------------------------------------------------------------------------- 1 | // Package dms3mail implements a mailer service for dms3clients 2 | package dms3mail 3 | 4 | import ( 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "html/template" 9 | "math" 10 | "path" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 17 | "golang.org/x/text/cases" 18 | "golang.org/x/text/language" 19 | "gopkg.in/gomail.v2" 20 | ) 21 | 22 | // Init configs the library and configuration for dms3mail 23 | func Init(configPath string) { 24 | 25 | dms3libs.LoadLibConfig(filepath.Join(configPath, dms3libs.DMS3Libs, dms3libs.DMS3libsTOML)) 26 | dms3libs.LoadComponentConfig(&mailConfig, filepath.Join(configPath, dms3libs.DMS3Mail, dms3libs.DMS3mailTOML)) 27 | 28 | dms3libs.SetLogFileLocation(mailConfig.Logging) 29 | dms3libs.CreateLogger(mailConfig.Logging) 30 | dms3libs.LogInfo("dms3mail " + dms3libs.GetProjectVersion() + " started") 31 | 32 | dms3libs.CheckFileLocation(configPath, dms3libs.DMS3Mail, &mailConfig.FileLocation, mailConfig.Filename) 33 | 34 | GenerateEventEmail() 35 | 36 | } 37 | 38 | // GenerateEventEmail is the entry point for this package, first calling parseEventArgs to interpret 39 | // the Motion event, and then calling generateSMTPEmail to create and send the email 40 | func GenerateEventEmail() { 41 | 42 | var eventDetails structEventDetails 43 | 44 | eventDetails.parseEventArgs() 45 | eventDetails.generateSMTPEmail() 46 | 47 | } 48 | 49 | // parseEventArgs creates an event by parsing the following command line arguments passed in via the 50 | // Motion on_picture_save or the on_movie_end command: 51 | // ARGV[0] count of pixels changed 52 | // ARGV[1] media filename 53 | func (eventDetails *structEventDetails) parseEventArgs() { 54 | 55 | // parse command line arguments passed from Motion command 56 | pixels := flag.Int("pixels", 0, "count of pixels detected in the event") 57 | filename := flag.String("filename", "", "fullpath filename of the event media file") 58 | 59 | flag.Parse() 60 | 61 | if flag.NFlag() != 2 { 62 | dms3libs.LogFatal("only " + strconv.Itoa(flag.NFlag()) + " argument(s) passed... exiting") 63 | } else if !dms3libs.IsFile(*filename) { 64 | dms3libs.LogFatal("filename not found... exiting") 65 | } 66 | 67 | eventDetails.eventMedia = *filename 68 | 69 | // get image dimensions and calculate percent of image change 70 | width, height := dms3libs.GetImageDimensions(eventDetails.eventMedia) 71 | eventDetails.eventChange = fmt.Sprintf("%d", int(math.Ceil((float64(*pixels) / float64(width*height) * 100)))) 72 | 73 | eventDetails.eventDate = getEventDetails(eventDetails.eventMedia) 74 | eventDetails.clientName = cases.Title(language.English, cases.NoLower).String(dms3libs.GetDeviceHostname()) 75 | 76 | } 77 | 78 | // getEventDetails creates the following event details based on filename: 79 | // 80 | // eventNumber - Motion-generated event number 81 | // eventDate - Motion-generated event datetime 82 | // 83 | // This method assumes that filename follows the default Motion file-naming convention of 84 | // [%v-]%Y%m%d%H%M%S (for movies) or [%v-]%Y%m%d%H%M%S-%q (for pictures), where: 85 | // 86 | // [%v] - event number (as of Motion 4.3.2, no longer included in filename by default) 87 | // %Y%m%d%H%M%S - ISO 8601 date, with hours, minutes, seconds notion 88 | // %q - frame number (value ignored) 89 | func getEventDetails(filename string) (eventDate string) { 90 | 91 | var index int 92 | file := path.Base(filename) 93 | sepCount := strings.Count(file, "-") 94 | 95 | if sepCount > 0 && sepCount < 3 { 96 | index = sepCount - 1 97 | } else { 98 | dms3libs.LogFatal("unexpected Motion filenaming convention: missing separators") 99 | } 100 | 101 | res := strings.Split(file, "-") 102 | 103 | if len(res[index]) != 14 { 104 | dms3libs.LogFatal("unexpected Motion filenaming convention: incorrect string length") 105 | } 106 | 107 | year := atoi(res[index][0:4]) 108 | month := atoi(res[index][4:6]) 109 | day := atoi(res[index][6:8]) 110 | hour := atoi(res[index][8:10]) 111 | minute := atoi(res[index][10:12]) 112 | sec := atoi(res[index][12:14]) 113 | 114 | return time.Date(year, time.Month(month), day, hour, minute, sec, 0, time.UTC).Format("15:04:05 on 2006-01-02") 115 | 116 | } 117 | 118 | // atoi converts a string to an integer 119 | func atoi(s string) int { 120 | 121 | i, err := strconv.Atoi(s) 122 | if err != nil { 123 | dms3libs.LogFatal("failed to convert string to integer: " + err.Error()) 124 | } 125 | 126 | return i 127 | } 128 | 129 | // createEmailBody loads the email template (HTML) and parses elements 130 | func (elements emailTemplateElements) createEmailBody() string { 131 | 132 | t := template.Must(template.New(mailConfig.Filename).ParseFiles(filepath.Join(mailConfig.FileLocation, mailConfig.Filename))) 133 | 134 | var tpl bytes.Buffer 135 | 136 | if err := t.Execute(&tpl, elements); err != nil { 137 | dms3libs.LogFatal(err.Error()) 138 | } 139 | 140 | return tpl.String() 141 | 142 | } 143 | 144 | // generateSMTPEmail generates and mails an email message based on configuration options 145 | // See https://github.com/go-gomail/gomail for mail package options 146 | func (eventDetails *structEventDetails) generateSMTPEmail() { 147 | 148 | mail := gomail.NewMessage() 149 | mail.SetHeader("From", mailConfig.Email.From) 150 | mail.SetHeader("To", mailConfig.Email.To) 151 | mail.SetHeader("Subject", "DMS3 Client "+eventDetails.clientName+": Event Detected at "+eventDetails.eventDate) 152 | 153 | headerImage := filepath.Join(mailConfig.FileLocation, "assets", "img", "dms3logo.png") 154 | footerImage := filepath.Join(mailConfig.FileLocation, "assets", "img", "dms3github.png") 155 | 156 | elements := &emailTemplateElements{ 157 | Header: filepath.Base(headerImage), 158 | Event: filepath.Base(eventDetails.eventMedia), 159 | Date: eventDetails.eventDate, 160 | Client: eventDetails.clientName, 161 | Change: eventDetails.eventChange, 162 | Footer: filepath.Base(footerImage), 163 | } 164 | 165 | mail.SetBody("text/html", elements.createEmailBody()) 166 | 167 | mail.Embed(headerImage) 168 | mail.Embed(eventDetails.eventMedia) 169 | mail.Embed(footerImage) 170 | 171 | dialer := gomail.NewDialer(mailConfig.SMTP.Address, mailConfig.SMTP.Port, mailConfig.SMTP.Username, mailConfig.SMTP.Password) 172 | 173 | if err := dialer.DialAndSend(mail); err != nil { 174 | dms3libs.LogFatal(err.Error()) 175 | } else { 176 | dms3libs.LogInfo("successfully processed and sent email") 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /dms3server/daemons/systemd/dms3server.service: -------------------------------------------------------------------------------- 1 | # Distributed-Motion-S3 DMS3Server Component Systemd Service Unit 2 | # 1.4.4 3 | 4 | [Unit] 5 | Description=Distributed Motion Sense Surveillance Service (DMS3) Server 6 | After=network.target 7 | 8 | [Service] 9 | Type=simple 10 | Restart=on-failure 11 | ExecStart=/usr/local/bin/dms3server 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /dms3server/media/motion_start.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3server/media/motion_start.wav -------------------------------------------------------------------------------- /dms3server/media/motion_stop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richbl/go-distributed-motion-s3/5525385b6b2db385fb68bc7984b6ba0355bf1e5b/dms3server/media/motion_stop.wav -------------------------------------------------------------------------------- /dms3server/server_config.go: -------------------------------------------------------------------------------- 1 | // Package dms3server configuration structures and variables 2 | package dms3server 3 | 4 | import ( 5 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 6 | ) 7 | 8 | // ServerConfig contains dms3Server configuration settings read from TOML file 9 | var ServerConfig *structSettings 10 | 11 | // server-side configuration parameters 12 | type structSettings struct { 13 | Server *structServer 14 | Audio *structAudio 15 | AlwaysOn *structAlwaysOn 16 | UserProxy *structUserProxy 17 | Logging *dms3libs.StructLogging 18 | } 19 | 20 | // server details 21 | type structServer struct { 22 | Port int 23 | CheckInterval uint16 24 | EnableDashboard bool 25 | } 26 | 27 | // audio parameters used when the motion detector application starts/stops 28 | type structAudio struct { 29 | Enable bool 30 | PlayMotionStart string 31 | PlayMotionStop string 32 | } 33 | 34 | // Always On feature parameters (enable the motion detector application based on time of day) 35 | type structAlwaysOn struct { 36 | Enable bool 37 | TimeRange []string 38 | } 39 | 40 | // User proxy parameters (representing the user's existence on the LAN) 41 | type structUserProxy struct { 42 | IPBase string 43 | IPRange []int 44 | MacsToFind []string 45 | } 46 | 47 | // Media file locations (for audio) 48 | type mediaPath struct { 49 | configLocation *string 50 | mediaLocation string 51 | } 52 | -------------------------------------------------------------------------------- /dms3server/server_connector.go: -------------------------------------------------------------------------------- 1 | // Package dms3server connector initializes the dms3server device component 2 | package dms3server 3 | 4 | import ( 5 | "fmt" 6 | "net" 7 | "path" 8 | "path/filepath" 9 | "strconv" 10 | 11 | dms3dash "github.com/richbl/go-distributed-motion-s3/dms3dashboard" 12 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 13 | ) 14 | 15 | // Init configs the library and configuration for dms3server 16 | func Init(configPath string) { 17 | 18 | dms3libs.InitComponent(configPath, dms3libs.DMS3Server, dms3libs.DMS3serverTOML, &ServerConfig, ServerConfig.Logging) 19 | 20 | setMediaLocation(configPath, ServerConfig) 21 | dms3dash.DashboardEnable = ServerConfig.Server.EnableDashboard 22 | 23 | if dms3dash.DashboardEnable { 24 | dms3dash.InitDashboardServer(configPath, ServerConfig.Server.CheckInterval) 25 | } 26 | 27 | startServer(ServerConfig.Server.Port) 28 | } 29 | 30 | // startServer starts the TCP server 31 | func startServer(serverPort int) { 32 | 33 | if listener, err := net.Listen("tcp", ":"+fmt.Sprint(serverPort)); err != nil { 34 | dms3libs.LogFatal(err.Error()) 35 | } else { 36 | dms3libs.LogInfo("TCP server started") 37 | defer listener.Close() 38 | serverLoop(listener) 39 | } 40 | 41 | } 42 | 43 | // serverLoop starts a loop to listen for clients, spawning a separate processing thread on 44 | // dms3client connect 45 | func serverLoop(listener net.Listener) { 46 | 47 | for { 48 | 49 | if conn, err := listener.Accept(); err != nil { 50 | dms3libs.LogFatal(err.Error()) 51 | } else { 52 | dms3libs.LogInfo("OPEN connection from: " + conn.RemoteAddr().String()) 53 | go processClient(conn) 54 | } 55 | 56 | } 57 | } 58 | 59 | // processClient passes motion detector application state to all dms3client listeners 60 | func processClient(conn net.Conn) { 61 | 62 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 63 | 64 | dms3dash.SendDashboardRequest(conn) 65 | sendMotionDetectorState(conn) 66 | 67 | dms3libs.LogInfo("CLOSE connection from: " + conn.RemoteAddr().String()) 68 | conn.Close() 69 | } 70 | 71 | // sendMotionDetectorState sends detector state to clients 72 | func sendMotionDetectorState(conn net.Conn) { 73 | 74 | state := strconv.Itoa(int(DetermineMotionDetectorState())) 75 | 76 | if _, err := conn.Write([]byte(state)); err != nil { 77 | dms3libs.LogFatal(err.Error()) 78 | } else { 79 | dms3libs.LogInfo("Sent motion detector state as: " + state) 80 | } 81 | 82 | } 83 | 84 | // setMediaLocation sets the location where audio files are located for motion detection 85 | // application start/stop events 86 | func setMediaLocation(configPath string, config *structSettings) { 87 | 88 | media := []mediaPath{ 89 | {&config.Audio.PlayMotionStart, filepath.Join(dms3libs.DMS3Server, "media", "motion_start.wav")}, 90 | {&config.Audio.PlayMotionStop, filepath.Join(dms3libs.DMS3Server, "media", "motion_stop.wav")}, 91 | } 92 | 93 | for _, mp := range media { 94 | checkMediaLocations(configPath, mp) 95 | } 96 | 97 | } 98 | 99 | // checkMediaLocations identifies the possible location of media (audio) files (release or 100 | // development depending on the runtime environment) 101 | func checkMediaLocations(configPath string, mp mediaPath) { 102 | 103 | relPath := filepath.Join(configPath, mp.mediaLocation) 104 | devPath := filepath.Join(path.Dir(dms3libs.GetPackageDir()), mp.mediaLocation) 105 | 106 | if mp.configLocation != nil && *mp.configLocation != "" { 107 | return // already set 108 | } 109 | 110 | var possiblePaths = []string{relPath, devPath} 111 | for _, path := range possiblePaths { 112 | 113 | if dms3libs.IsFile(path) { 114 | *mp.configLocation = path 115 | return 116 | } 117 | 118 | } 119 | 120 | dms3libs.LogFatal("unable to set media location... check TOML configuration file") 121 | } 122 | -------------------------------------------------------------------------------- /dms3server/server_manager.go: -------------------------------------------------------------------------------- 1 | // Package dms3server manager processes dms3server device component messages received by 2 | // dms3client device components 3 | package dms3server 4 | 5 | import ( 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/richbl/go-distributed-motion-s3/dms3libs" 10 | ) 11 | 12 | var checkIntervalTimestamp = time.Now() 13 | 14 | // DetermineMotionDetectorState determines whether to start the motion detector application based 15 | // device presence/time logic 16 | func DetermineMotionDetectorState() dms3libs.MotionDetectorState { 17 | 18 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 19 | 20 | if checkIntervalExpired() { 21 | 22 | if timeInRange() || !deviceOnLAN() { 23 | return setMotionDetectorState(dms3libs.Start) 24 | } 25 | 26 | return setMotionDetectorState(dms3libs.Stop) 27 | } 28 | 29 | return dms3libs.MotionDetector.State() 30 | } 31 | 32 | // setMotionDetectorState sets the state read by device clients to starts/stop the motion detector 33 | // applications 34 | func setMotionDetectorState(state dms3libs.MotionDetectorState) dms3libs.MotionDetectorState { 35 | 36 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 37 | 38 | if dms3libs.MotionDetector.State() == state { 39 | return state 40 | } 41 | 42 | dms3libs.MotionDetector.SetState(state) 43 | 44 | if ServerConfig.Audio.Enable { 45 | 46 | switch state { 47 | case dms3libs.Start: 48 | dms3libs.PlayAudio(ServerConfig.Audio.PlayMotionStart) 49 | case dms3libs.Stop: 50 | dms3libs.PlayAudio(ServerConfig.Audio.PlayMotionStop) 51 | } 52 | 53 | } 54 | 55 | return state 56 | } 57 | 58 | // checkIntervalExpired determines if last check interval (in seconds) has expired 59 | func checkIntervalExpired() bool { 60 | 61 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 62 | 63 | if time.Since(checkIntervalTimestamp).Seconds() >= float64(ServerConfig.Server.CheckInterval) { 64 | checkIntervalTimestamp = time.Now() 65 | return true 66 | } 67 | 68 | return false 69 | } 70 | 71 | // timeInRange checks to see if the current time is within the bounds of the 'always on' range 72 | // (if the AlwaysOn option is enabled) 73 | func timeInRange() bool { 74 | 75 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 76 | 77 | if ServerConfig.AlwaysOn.Enable { 78 | return calcTimeRange() 79 | } 80 | 81 | return false 82 | } 83 | 84 | // calcTimeRange checks to see if the configured time range crosses into the next day, and 85 | // determines time range accordingly 86 | func calcTimeRange() bool { 87 | 88 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 89 | 90 | curTime := dms3libs.To24H(time.Now()) 91 | 92 | startTime := dms3libs.Format24H(ServerConfig.AlwaysOn.TimeRange[0]) 93 | endTime := dms3libs.Format24H(ServerConfig.AlwaysOn.TimeRange[1]) 94 | 95 | if startTime > endTime { 96 | return (curTime >= startTime) || (curTime < endTime) 97 | } 98 | 99 | return (curTime >= startTime) && (curTime < endTime) 100 | } 101 | 102 | // deviceOnLAN checks to see if device MACs exist on LAN (first freshens local arp cache to 103 | // guarantee good results) 104 | func deviceOnLAN() bool { 105 | 106 | dms3libs.LogDebug(filepath.Base(dms3libs.GetFunctionName())) 107 | dms3libs.PingHosts(ServerConfig.UserProxy.IPBase, ServerConfig.UserProxy.IPRange) 108 | 109 | return dms3libs.FindMacs(ServerConfig.UserProxy.MacsToFind) 110 | } 111 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/richbl/go-distributed-motion-s3 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/BurntSushi/toml v0.4.1 9 | github.com/mrgleam/easyssh v0.0.0-20170611150909-933e069a250a 10 | golang.org/x/text v0.22.0 11 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 12 | ) 13 | 14 | require ( 15 | golang.org/x/crypto v0.35.0 // indirect 16 | golang.org/x/sys v0.30.0 // indirect 17 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= 2 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/mrgleam/easyssh v0.0.0-20170611150909-933e069a250a h1:xB0uQ78Pxb3UQ+A9cG0RsdyBxINcRfKeexlf5AHp4Dc= 4 | github.com/mrgleam/easyssh v0.0.0-20170611150909-933e069a250a/go.mod h1:PEFDTl2WSHEVdE4ERbP0mu74cH7SBB/D5BFc09pOYcM= 5 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 6 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 7 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 8 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 9 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 10 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 11 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 12 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 13 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 14 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 15 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= 16 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= 17 | --------------------------------------------------------------------------------