├── .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 |  [](https://goreportcard.com/report/github.com/richbl/go-distributed-motion-s3) [](https://app.codacy.com/gh/richbl/go-distributed-motion-s3/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://sonarcloud.io/summary/new_code?id=richbl_go-distributed-motion-s3)
4 |
5 | ## What Is **DMS3**?
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |