| @@ -0,0 +1,22 @@ | |||
| root = true | |||
| [*] | |||
| charset = utf-8 | |||
| end_of_line = lf | |||
| insert_final_newline = true | |||
| trim_trailing_whitespace = true | |||
| [{*.go,Makefile,.gitmodules,go.mod,go.sum}] | |||
| indent_style = tab | |||
| [*.md] | |||
| indent_style = tab | |||
| trim_trailing_whitespace = false | |||
| [*.{yml,yaml,json}] | |||
| indent_style = space | |||
| indent_size = 2 | |||
| [*.{js,jsx,ts,tsx,css,less,sass,scss,vue,py}] | |||
| indent_style = space | |||
| indent_size = 4 | |||
| @@ -0,0 +1 @@ | |||
| * -text | |||
| @@ -0,0 +1,25 @@ | |||
| # Mac OS X files | |||
| .DS_Store | |||
| # Binaries for programs and plugins | |||
| *.exe | |||
| *.exe~ | |||
| *.dll | |||
| *.so | |||
| *.dylib | |||
| # Test binary, build with `go test -c` | |||
| *.test | |||
| # Output of the go coverage tool, specifically when used with LiteIDE | |||
| *.out | |||
| # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 | |||
| .glide/ | |||
| copy_files/ | |||
| presence.db | |||
| cmd/presenSe/presence.db | |||
| # Dependency directories (remove the comment below to include it) | |||
| vendor/ | |||
| @@ -0,0 +1 @@ | |||
| Hacker license! | |||
| @@ -0,0 +1 @@ | |||
| # note: call scripts from /scripts | |||
| @@ -1 +1,196 @@ | |||
| update | |||
| # Standard Go Project Layout | |||
| ## Overview | |||
| This is a basic layout for Go application projects. Note that it's basic in terms of content because it's focusing only on the general layout and not what you have inside. It's also basic because it's very high level and it doesn't go into great details in terms of how you can structure your project even further. For example, it doesn't try to cover the project structure you'd have with something like Clean Architecture. | |||
| This is **`NOT an official standard defined by the core Go dev team`**. This is a set of common historical and emerging project layout patterns in the Go ecosystem. Some of these patterns are more popular than others. It also has a number of small enhancements along with several supporting directories common to any large enough real world application. Note that the **core Go team provides a great set of general guidelines about structuring Go projects** and what it means for your project when it's imported and when it's installed. See the [`Organizing a Go module`](https://go.dev/doc/modules/layout) page in the official Go docs for more details. It includes the `internal` and `cmd` directory patterns (described below) and other useful information. | |||
| **`If you are trying to learn Go or if you are building a PoC or a simple project for yourself this project layout is an overkill. Start with something really simple instead (a single `main.go` file and `go.mod` is more than enough).`** As your project grows keep in mind that it'll be important to make sure your code is well structured otherwise you'll end up with a messy code with lots of hidden dependencies and global state. When you have more people working on the project you'll need even more structure. That's when it's important to introduce a common way to manage packages/libraries. When you have an open source project or when you know other projects import the code from your project repository that's when it's important to have private (aka `internal`) packages and code. Clone the repository, keep what you need and delete everything else! Just because it's there it doesn't mean you have to use it all. None of these patterns are used in every single project. Even the `vendor` pattern is not universal. | |||
| With Go 1.14 [`Go Modules`](https://go.dev/wiki/Modules) are finally ready for production. Use [`Go Modules`](https://blog.golang.org/using-go-modules) unless you have a specific reason not to use them and if you do then you don’t need to worry about $GOPATH and where you put your project. The basic `go.mod` file in the repo assumes your project is hosted on GitHub, but it's not a requirement. The module path can be anything though the first module path component should have a dot in its name (the current version of Go doesn't enforce it anymore, but if you are using slightly older versions don't be surprised if your builds fail without it). See Issues [`37554`](https://github.com/golang/go/issues/37554) and [`32819`](https://github.com/golang/go/issues/32819) if you want to know more about it. | |||
| This project layout is intentionally generic and it doesn't try to impose a specific Go package structure. | |||
| This is a community effort. Open an issue if you see a new pattern or if you think one of the existing patterns needs to be updated. | |||
| If you need help with naming, formatting and style start by running [`gofmt`](https://golang.org/cmd/gofmt/) and [`staticcheck`](https://github.com/dominikh/go-tools/tree/master/cmd/staticcheck). The previous standard linter, golint, is now deprecated and not maintained; use of a maintained linter such as staticcheck is recommended. Also make sure to read these Go code style guidelines and recommendations: | |||
| * https://talks.golang.org/2014/names.slide | |||
| * https://golang.org/doc/effective_go.html#names | |||
| * https://blog.golang.org/package-names | |||
| * https://go.dev/wiki/CodeReviewComments | |||
| * [Style guideline for Go packages](https://rakyll.org/style-packages) (rakyll/JBD) | |||
| See [`Go Project Layout`](https://medium.com/golang-learn/go-project-layout-e5213cdcfaa2) for additional background information. | |||
| More about naming and organizing packages as well as other code structure recommendations: | |||
| * [GopherCon EU 2018: Peter Bourgon - Best Practices for Industrial Programming](https://www.youtube.com/watch?v=PTE4VJIdHPg) | |||
| * [GopherCon Russia 2018: Ashley McNamara + Brian Ketelsen - Go best practices.](https://www.youtube.com/watch?v=MzTcsI6tn-0) | |||
| * [GopherCon 2017: Edward Muller - Go Anti-Patterns](https://www.youtube.com/watch?v=ltqV6pDKZD8) | |||
| * [GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps](https://www.youtube.com/watch?v=oL6JBUk6tj0) | |||
| A Chinese post about Package-Oriented-Design guidelines and Architecture layer | |||
| * [面向包的设计和架构分层](https://github.com/danceyoung/paper-code/blob/master/package-oriented-design/packageorienteddesign.md) | |||
| ## Go Directories | |||
| ### `/cmd` | |||
| Main applications for this project. | |||
| The directory name for each application should match the name of the executable you want to have (e.g., `/cmd/myapp`). | |||
| Don't put a lot of code in the application directory. If you think the code can be imported and used in other projects, then it should live in the `/pkg` directory. If the code is not reusable or if you don't want others to reuse it, put that code in the `/internal` directory. You'll be surprised what others will do, so be explicit about your intentions! | |||
| It's common to have a small `main` function that imports and invokes the code from the `/internal` and `/pkg` directories and nothing else. | |||
| See the [`/cmd`](cmd/README.md) directory for examples. | |||
| ### `/internal` | |||
| Private application and library code. This is the code you don't want others importing in their applications or libraries. Note that this layout pattern is enforced by the Go compiler itself. See the Go 1.4 [`release notes`](https://golang.org/doc/go1.4#internalpackages) for more details. Note that you are not limited to the top level `internal` directory. You can have more than one `internal` directory at any level of your project tree. | |||
| You can optionally add a bit of extra structure to your internal packages to separate your shared and non-shared internal code. It's not required (especially for smaller projects), but it's nice to have visual clues showing the intended package use. Your actual application code can go in the `/internal/app` directory (e.g., `/internal/app/myapp`) and the code shared by those apps in the `/internal/pkg` directory (e.g., `/internal/pkg/myprivlib`). | |||
| You use internal directories to make packages private. If you put a package inside an internal directory, then other packages can’t import it unless they share a common ancestor. And it’s the only directory named in Go’s documentation and has special compiler treatment. | |||
| ### `/pkg` | |||
| Library code that's ok to use by external applications (e.g., `/pkg/mypubliclib`). Other projects will import these libraries expecting them to work, so think twice before you put something here :-) Note that the `internal` directory is a better way to ensure your private packages are not importable because it's enforced by Go. The `/pkg` directory is still a good way to explicitly communicate that the code in that directory is safe for use by others. The [`I'll take pkg over internal`](https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/) blog post by Travis Jeffery provides a good overview of the `pkg` and `internal` directories and when it might make sense to use them. | |||
| It's also a way to group Go code in one place when your root directory contains lots of non-Go components and directories making it easier to run various Go tools (as mentioned in these talks: [`Best Practices for Industrial Programming`](https://www.youtube.com/watch?v=PTE4VJIdHPg) from GopherCon EU 2018, [GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps](https://www.youtube.com/watch?v=oL6JBUk6tj0) and [GoLab 2018 - Massimiliano Pippi - Project layout patterns in Go](https://www.youtube.com/watch?v=3gQa1LWwuzk)). | |||
| See the [`/pkg`](pkg/README.md) directory if you want to see which popular Go repos use this project layout pattern. This is a common layout pattern, but it's not universally accepted and some in the Go community don't recommend it. | |||
| It's ok not to use it if your app project is really small and where an extra level of nesting doesn't add much value (unless you really want to :-)). Think about it when it's getting big enough and your root directory gets pretty busy (especially if you have a lot of non-Go app components). | |||
| The `pkg` directory origins: The old Go source code used to use `pkg` for its packages and then various Go projects in the community started copying the pattern (see [`this`](https://twitter.com/bradfitz/status/1039512487538970624) Brad Fitzpatrick's tweet for more context). | |||
| ### `/vendor` | |||
| Application dependencies (managed manually or by your favorite dependency management tool like the new built-in [`Go Modules`](https://go.dev/wiki/Modules) feature). The `go mod vendor` command will create the `/vendor` directory for you. Note that you might need to add the `-mod=vendor` flag to your `go build` command if you are not using Go 1.14 where it's on by default. | |||
| Don't commit your application dependencies if you are building a library. | |||
| Note that since [`1.13`](https://golang.org/doc/go1.13#modules) Go also enabled the module proxy feature (using [`https://proxy.golang.org`](https://proxy.golang.org) as their module proxy server by default). Read more about it [`here`](https://blog.golang.org/module-mirror-launch) to see if it fits all of your requirements and constraints. If it does, then you won't need the `vendor` directory at all. | |||
| ## Service Application Directories | |||
| ### `/api` | |||
| OpenAPI/Swagger specs, JSON schema files, protocol definition files. | |||
| See the [`/api`](api/README.md) directory for examples. | |||
| ## Web Application Directories | |||
| ### `/web` | |||
| Web application specific components: static web assets, server side templates and SPAs. | |||
| ## Common Application Directories | |||
| ### `/configs` | |||
| Configuration file templates or default configs. | |||
| Put your `confd` or `consul-template` template files here. | |||
| ### `/init` | |||
| System init (systemd, upstart, sysv) and process manager/supervisor (runit, supervisord) configs. | |||
| ### `/scripts` | |||
| Scripts to perform various build, install, analysis, etc operations. | |||
| These scripts keep the root level Makefile small and simple (e.g., [`https://github.com/hashicorp/terraform/blob/main/Makefile`](https://github.com/hashicorp/terraform/blob/main/Makefile)). | |||
| See the [`/scripts`](scripts/README.md) directory for examples. | |||
| ### `/build` | |||
| Packaging and Continuous Integration. | |||
| Put your cloud (AMI), container (Docker), OS (deb, rpm, pkg) package configurations and scripts in the `/build/package` directory. | |||
| Put your CI (travis, circle, drone) configurations and scripts in the `/build/ci` directory. Note that some of the CI tools (e.g., Travis CI) are very picky about the location of their config files. Try putting the config files in the `/build/ci` directory linking them to the location where the CI tools expect them (when possible). | |||
| ### `/deployments` | |||
| IaaS, PaaS, system and container orchestration deployment configurations and templates (docker-compose, kubernetes/helm, terraform). Note that in some repos (especially apps deployed with kubernetes) this directory is called `/deploy`. | |||
| ### `/test` | |||
| Additional external test apps and test data. Feel free to structure the `/test` directory anyway you want. For bigger projects it makes sense to have a data subdirectory. For example, you can have `/test/data` or `/test/testdata` if you need Go to ignore what's in that directory. Note that Go will also ignore directories or files that begin with "." or "_", so you have more flexibility in terms of how you name your test data directory. | |||
| See the [`/test`](test/README.md) directory for examples. | |||
| ## Other Directories | |||
| ### `/docs` | |||
| Design and user documents (in addition to your godoc generated documentation). | |||
| See the [`/docs`](docs/README.md) directory for examples. | |||
| ### `/tools` | |||
| Supporting tools for this project. Note that these tools can import code from the `/pkg` and `/internal` directories. | |||
| See the [`/tools`](tools/README.md) directory for examples. | |||
| ### `/examples` | |||
| Examples for your applications and/or public libraries. | |||
| See the [`/examples`](examples/README.md) directory for examples. | |||
| ### `/third_party` | |||
| External helper tools, forked code and other 3rd party utilities (e.g., Swagger UI). | |||
| ### `/githooks` | |||
| Git hooks. | |||
| ### `/assets` | |||
| Other assets to go along with your repository (images, logos, etc). | |||
| ### `/website` | |||
| This is the place to put your project's website data if you are not using GitHub pages. | |||
| See the [`/website`](website/README.md) directory for examples. | |||
| ## Directories You Shouldn't Have | |||
| ### `/src` | |||
| Some Go projects do have a `src` folder, but it usually happens when the devs came from the Java world where it's a common pattern. If you can help yourself try not to adopt this Java pattern. You really don't want your Go code or Go projects to look like Java :-) | |||
| Don't confuse the project level `/src` directory with the `/src` directory Go uses for its workspaces as described in [`How to Write Go Code`](https://golang.org/doc/code.html). The `$GOPATH` environment variable points to your (current) workspace (by default it points to `$HOME/go` on non-windows systems). This workspace includes the top level `/pkg`, `/bin` and `/src` directories. Your actual project ends up being a sub-directory under `/src`, so if you have the `/src` directory in your project the project path will look like this: `/some/path/to/workspace/src/your_project/src/your_code.go`. Note that with Go 1.11 it's possible to have your project outside of your `GOPATH`, but it still doesn't mean it's a good idea to use this layout pattern. | |||
| ## Badges | |||
| * [Go Report Card](https://goreportcard.com/) - It will scan your code with `gofmt`, `go vet`, `gocyclo`, `golint`, `ineffassign`, `license` and `misspell`. Replace `github.com/golang-standards/project-layout` with your project reference. | |||
| [](https://goreportcard.com/report/github.com/golang-standards/project-layout) | |||
| * ~~[GoDoc](http://godoc.org) - It will provide online version of your GoDoc generated documentation. Change the link to point to your project.~~ | |||
| [](http://godoc.org/github.com/golang-standards/project-layout) | |||
| * [Pkg.go.dev](https://pkg.go.dev) - Pkg.go.dev is a new destination for Go discovery & docs. You can create a badge using the [badge generation tool](https://pkg.go.dev/badge). | |||
| [](https://pkg.go.dev/github.com/golang-standards/project-layout) | |||
| * Release - It will show the latest release number for your project. Change the github link to point to your project. | |||
| [](https://github.com/golang-standards/project-layout/releases/latest) | |||
| ## Notes | |||
| A more opinionated project template with sample/reusable configs, scripts and code is a WIP. | |||
| @@ -0,0 +1,320 @@ | |||
| <mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/28.2.5 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36" version="28.2.5"> | |||
| <diagram name="Pagina-1" id="Fpn7J8KQqI0Mq3TqwKSc"> | |||
| <mxGraphModel dx="1028" dy="611" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0"> | |||
| <root> | |||
| <mxCell id="0" /> | |||
| <mxCell id="1" parent="0" /> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-9" value="localization algorithm" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||
| <mxGeometry x="149" y="334" width="70" height="70" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-11" value="API Call From API Server Logic" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="302" y="439" width="190" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-1" value="API Server<br>(FastAPI)<div><br></div><div><br><br><br>+<br><br>Logic Connector</div>" style="whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||
| <mxGeometry x="613" y="330" width="204" height="204" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-2" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.02;exitY=0.704;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;exitPerimeter=0;startArrow=none;startFill=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-18" target="krLT08NrNyH7hn3MfuL1-19" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="680" y="240" as="sourcePoint" /> | |||
| <mxPoint x="680" y="40" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-59" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-2" vertex="1" connectable="0"> | |||
| <mxGeometry x="0.1209" y="1" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-3" value="BackEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="508" y="770" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-4" value="FrontEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="975" y="770" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-5" value="" style="endArrow=none;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0;exitY=0;exitDx=50;exitDy=0;exitPerimeter=0;endFill=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-24" target="krLT08NrNyH7hn3MfuL1-1" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="680" y="700" as="sourcePoint" /> | |||
| <mxPoint x="585" y="595" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-7" value="WebGui<br>Config" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||
| <mxGeometry x="950" y="312" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-8" value="WebGui<br>Status" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||
| <mxGeometry x="950" y="399" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-9" value="WebGui<br>Alarm" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||
| <mxGeometry x="950" y="585" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-10" value="WebGui<br>QueryLog" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||
| <mxGeometry x="950" y="677" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-11" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-7" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="750" y="160" as="sourcePoint" /> | |||
| <mxPoint x="800" y="110" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-12" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-8" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="840" y="428" as="sourcePoint" /> | |||
| <mxPoint x="1060" y="280" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-13" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-9" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="840" y="395" as="sourcePoint" /> | |||
| <mxPoint x="1060" y="360" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-14" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-10" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="820" y="450" as="sourcePoint" /> | |||
| <mxPoint x="1020" y="531" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-15" value="WebGui<br>QueryStatus" style="rounded=1;whiteSpace=wrap;html=1;" parent="1" vertex="1"> | |||
| <mxGeometry x="950" y="487" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-16" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-1" target="krLT08NrNyH7hn3MfuL1-15" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="850" y="309" as="sourcePoint" /> | |||
| <mxPoint x="1050" y="300" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-17" value="Operator" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="1114" y="560" width="30" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-18" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||
| <mxGeometry x="1012" y="60" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-19" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" parent="1" vertex="1"> | |||
| <mxGeometry x="644" y="79" width="90" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-20" value="MQTT<br>Broker" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" parent="1" vertex="1"> | |||
| <mxGeometry x="86" y="79" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-21" value="LOG" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" parent="1" vertex="1"> | |||
| <mxGeometry x="115" y="613" width="60" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-23" value="<br>Tracker<br>LOCALIZATION<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;" parent="1" vertex="1"> | |||
| <mxGeometry x="318" y="240" width="141" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-24" value="USER<br>CONFIG" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" parent="1" vertex="1"> | |||
| <mxGeometry x="665" y="676" width="120" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-25" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||
| <mxGeometry x="1012" y="120" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-26" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||
| <mxGeometry x="1012" y="180" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-27" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||
| <mxGeometry x="1012" y="240" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-28" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" parent="1" vertex="1"> | |||
| <mxGeometry x="650" y="193" width="87" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-29" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-25" target="krLT08NrNyH7hn3MfuL1-19" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="990" y="150" as="sourcePoint" /> | |||
| <mxPoint x="170" y="120" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-60" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-29" vertex="1" connectable="0"> | |||
| <mxGeometry x="0.038" y="3" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-30" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-26" target="krLT08NrNyH7hn3MfuL1-28" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="140" y="160" as="sourcePoint" /> | |||
| <mxPoint x="220" y="165" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-61" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-30" vertex="1" connectable="0"> | |||
| <mxGeometry x="0.0103" y="2" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-31" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-27" target="krLT08NrNyH7hn3MfuL1-28" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="130" y="180" as="sourcePoint" /> | |||
| <mxPoint x="773" y="220" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-62" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-31" vertex="1" connectable="0"> | |||
| <mxGeometry x="0.0203" y="-2" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-32" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1.017;entryY=0.38;entryDx=0;entryDy=0;entryPerimeter=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-19" target="krLT08NrNyH7hn3MfuL1-20" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="210" y="160" as="sourcePoint" /> | |||
| <mxPoint x="290" y="165" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-63" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-32" vertex="1" connectable="0"> | |||
| <mxGeometry x="-0.2888" y="-1" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-33" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-28" target="krLT08NrNyH7hn3MfuL1-20" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="280" y="190" as="sourcePoint" /> | |||
| <mxPoint x="370" y="240" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-64" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="krLT08NrNyH7hn3MfuL1-33" vertex="1" connectable="0"> | |||
| <mxGeometry x="-0.2558" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-34" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-20" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="400" y="281" as="sourcePoint" /> | |||
| <mxPoint x="400" y="190" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-36" value="<br>Tracker Sensors:<br>Alarm, Temp, <br>battery level<br>...<br>Heartbest<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;dx=20;dy=10;" parent="1" vertex="1"> | |||
| <mxGeometry x="318" y="486" width="140" height="104" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-37" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.358;exitDx=0;exitDy=0;startArrow=none;startFill=0;endFill=1;exitPerimeter=0;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="196.3699999999999" y="466.95000000000005" as="sourcePoint" /> | |||
| <mxPoint x="610" y="430" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-39" value="" style="endArrow=none;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endFill=0;exitX=0.949;exitY=0.3;exitDx=0;exitDy=0;exitPerimeter=0;startArrow=classic;startFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-23" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="310" y="350" as="sourcePoint" /> | |||
| <mxPoint x="385" y="640" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-40" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=1;exitDx=0;exitDy=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-36" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="350" y="360" as="sourcePoint" /> | |||
| <mxPoint x="350" y="460" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-41" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryPerimeter=0;endFill=1;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" target="krLT08NrNyH7hn3MfuL1-21" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="450" y="460" as="sourcePoint" /> | |||
| <mxPoint x="360" y="560" as="targetPoint" /> | |||
| <Array as="points" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-46" value="Presense<br>CORE" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1"> | |||
| <mxGeometry x="90" y="390" width="110" height="110" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-52" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="1101" y="70" width="30" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-56" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="1102" y="181" width="30" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-69" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-71" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="10" y="810" as="sourcePoint" /> | |||
| <mxPoint x="1160" y="810" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-72" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="1160" y="810" as="sourcePoint" /> | |||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-73" value="" style="endArrow=none;html=1;rounded=0;" parent="1" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||
| <mxPoint x="10" y="810" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-74" value="<b><font style="font-size: 27px;">ResLevis DIAGRAM</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="398" y="30" width="370" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="krLT08NrNyH7hn3MfuL1-75" value="<b style=""><font style="font-size: 13px;">Ver 2.0 2025/10/09</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="410" y="49" width="370" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="uKMjvkko2EQDpS27TLVR-1" value="<br>GW <br>and Tracker definition<br>model<br>supported<br>Decode-lib<br>Raw-Data<div><br></div>" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" parent="1" vertex="1"> | |||
| <mxGeometry x="334" y="613" width="110" height="126" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="uKMjvkko2EQDpS27TLVR-4" value="topic<br>Subscribe<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="72" y="331" width="76" height="47" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="uKMjvkko2EQDpS27TLVR-5" value="topic<br>Publish<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="532" y="133" width="76" height="47" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-1" value="" style="endArrow=classic;html=1;rounded=0;endFill=1;exitX=0;exitY=0;exitDx=0;exitDy=53;exitPerimeter=0;entryX=0.687;entryY=0.962;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="uKMjvkko2EQDpS27TLVR-1" target="krLT08NrNyH7hn3MfuL1-46" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="416" y="695" as="sourcePoint" /> | |||
| <mxPoint x="250" y="620" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-2" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="100" y="560" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-3" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="668" y="591" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-4" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="230" y="560" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-6" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="210" y="290" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-7" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="230" y="478" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-8" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="100" y="273" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-9" value="API Call From CORE" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1"> | |||
| <mxGeometry x="316" y="403" width="142" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="Xrb94kopVgDRZLuWBJpc-10" value="" style="endArrow=none;html=1;rounded=0;exitX=0.964;exitY=0.656;exitDx=0;exitDy=0;startArrow=classic;startFill=1;endFill=0;exitPerimeter=0;" parent="1" source="krLT08NrNyH7hn3MfuL1-46" edge="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="206" y="463.5" as="sourcePoint" /> | |||
| <mxPoint x="610" y="462" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-2" value="" style="rounded=0;whiteSpace=wrap;html=1;opacity=40;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"> | |||
| <mxGeometry x="60" y="220" width="460" height="550" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-3" value="<b>Developed by SenLab</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="68" y="734" width="84" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-4" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;opacity=40;" vertex="1" parent="1"> | |||
| <mxGeometry x="560" y="300" width="280" height="470" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-5" value="<b>Developed by AFA Systems</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="568" y="734" width="84" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-6" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||
| <mxGeometry x="870" y="300" width="230" height="470" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-7" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||
| <mxGeometry x="560" y="300" width="310" height="100" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-8" value="<b>Developed Maestry&nbsp;</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="867" y="737" width="84" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-11" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endFill=1;exitX=1;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="CsGlrJDhfrg_2RR0sWjT-9" target="krLT08NrNyH7hn3MfuL1-23"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="350" y="513" as="sourcePoint" /> | |||
| <mxPoint x="474" y="370" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="CsGlrJDhfrg_2RR0sWjT-13" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="235" y="342.5" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| </root> | |||
| </mxGraphModel> | |||
| </diagram> | |||
| </mxfile> | |||
| @@ -0,0 +1,354 @@ | |||
| <mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0" version="28.2.7"> | |||
| <diagram name="Pagina-1" id="Q54yxviGp2ibMrAPitEd"> | |||
| <mxGraphModel dx="1426" dy="799" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"> | |||
| <root> | |||
| <mxCell id="0" /> | |||
| <mxCell id="1" parent="0" /> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-2" value="API Call From API Server Logic" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;rotation=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="360" y="460" width="190" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-3" value="API Server<br>(FastAPI)<div><br></div><div><br><br><br>+<br><br>Logic Connector</div>" style="whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||
| <mxGeometry x="613" y="330" width="204" height="204" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-4" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.02;exitY=0.704;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;exitPerimeter=0;startArrow=none;startFill=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-20" target="9h2_EveRN_1U4OZwnWZC-21"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="680" y="240" as="sourcePoint" /> | |||
| <mxPoint x="680" y="40" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-5" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-4"> | |||
| <mxGeometry x="0.1209" y="1" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-6" value="BackEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="508" y="770" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-7" value="FrontEnd" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="975" y="770" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-8" value="" style="endArrow=none;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0;exitY=0;exitDx=50;exitDy=0;exitPerimeter=0;endFill=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-25" target="9h2_EveRN_1U4OZwnWZC-3"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="680" y="700" as="sourcePoint" /> | |||
| <mxPoint x="585" y="595" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-9" value="WebGui<br>Config" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="950" y="312" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-10" value="WebGui<br>Status" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="950" y="399" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-11" value="WebGui<br>Alarm" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="950" y="585" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-12" value="WebGui<br>QueryLog" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="950" y="677" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-13" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-9"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="750" y="160" as="sourcePoint" /> | |||
| <mxPoint x="800" y="110" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-14" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-10"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="840" y="428" as="sourcePoint" /> | |||
| <mxPoint x="1060" y="280" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-15" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-11"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="840" y="395" as="sourcePoint" /> | |||
| <mxPoint x="1060" y="360" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-16" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-12"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="820" y="450" as="sourcePoint" /> | |||
| <mxPoint x="1020" y="531" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-17" value="WebGui<br>QueryStatus" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="950" y="487" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-18" value="" style="endArrow=none;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-3" target="9h2_EveRN_1U4OZwnWZC-17"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="850" y="309" as="sourcePoint" /> | |||
| <mxPoint x="1050" y="300" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-19" value="Operator" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="1114" y="560" width="30" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-20" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||
| <mxGeometry x="1012" y="60" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-21" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" vertex="1" parent="1"> | |||
| <mxGeometry x="644" y="79" width="90" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-22" value="MQTT<br>Broker" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="62" y="79" width="120" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-23" value="LOG" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="1"> | |||
| <mxGeometry x="91" y="610" width="60" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-24" value="<br>Tracker<br>LOCALIZATION<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="269" y="241" width="141" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-25" value="USER<br>CONFIG" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" vertex="1" parent="1"> | |||
| <mxGeometry x="665" y="676" width="120" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-26" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||
| <mxGeometry x="1012" y="120" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-27" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||
| <mxGeometry x="1012" y="180" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-28" value="Beacon" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1"> | |||
| <mxGeometry x="1012" y="240" width="50" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-29" value="Gateway&nbsp;" style="triangle;whiteSpace=wrap;html=1;direction=west;" vertex="1" parent="1"> | |||
| <mxGeometry x="650" y="193" width="87" height="80" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-30" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-26" target="9h2_EveRN_1U4OZwnWZC-21"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="990" y="150" as="sourcePoint" /> | |||
| <mxPoint x="170" y="120" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-31" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-30"> | |||
| <mxGeometry x="0.038" y="3" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-32" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-27" target="9h2_EveRN_1U4OZwnWZC-29"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="140" y="160" as="sourcePoint" /> | |||
| <mxPoint x="220" y="165" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-33" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-32"> | |||
| <mxGeometry x="0.0103" y="2" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-34" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-28" target="9h2_EveRN_1U4OZwnWZC-29"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="130" y="180" as="sourcePoint" /> | |||
| <mxPoint x="773" y="220" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-35" value="BLE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-34"> | |||
| <mxGeometry x="0.0203" y="-2" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-36" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1.017;entryY=0.38;entryDx=0;entryDy=0;entryPerimeter=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-21" target="9h2_EveRN_1U4OZwnWZC-22"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="210" y="160" as="sourcePoint" /> | |||
| <mxPoint x="290" y="165" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-37" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-36"> | |||
| <mxGeometry x="-0.2888" y="-1" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-38" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-29" target="9h2_EveRN_1U4OZwnWZC-22"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="280" y="190" as="sourcePoint" /> | |||
| <mxPoint x="370" y="240" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-39" value="IP MQTT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9h2_EveRN_1U4OZwnWZC-38"> | |||
| <mxGeometry x="-0.2558" relative="1" as="geometry"> | |||
| <mxPoint as="offset" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-40" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endFill=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-22"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="146" y="260" as="sourcePoint" /> | |||
| <mxPoint x="400" y="190" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-41" value="<br>Tracker Sensors:<br>Alarm, Temp, <br>battery level<br>...<br>Heartbest<br>(state)" style="shape=internalStorage;whiteSpace=wrap;html=1;backgroundOutline=1;dx=20;dy=10;" vertex="1" parent="1"> | |||
| <mxGeometry x="420" y="541" width="140" height="104" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-42" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.25;exitDx=0;exitDy=0;startArrow=none;startFill=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="200" y="429.3800000000001" as="sourcePoint" /> | |||
| <mxPoint x="610" y="430" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-44" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endFill=1;exitX=1;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84" target="9h2_EveRN_1U4OZwnWZC-41"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="300" y="500" as="sourcePoint" /> | |||
| <mxPoint x="350" y="460" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-45" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryPerimeter=0;endFill=1;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-23"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="145" y="500" as="sourcePoint" /> | |||
| <mxPoint x="360" y="560" as="targetPoint" /> | |||
| <Array as="points" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-47" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="1101" y="70" width="30" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-48" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="1102" y="181" width="30" height="60" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-49" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-50" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="10" y="810" as="sourcePoint" /> | |||
| <mxPoint x="1160" y="810" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-51" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="1160" y="810" as="sourcePoint" /> | |||
| <mxPoint x="1160" y="10" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-52" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="10" y="10" as="sourcePoint" /> | |||
| <mxPoint x="10" y="810" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-53" value="<b><font style="font-size: 27px;">ResLevis DIAGRAM</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="398" y="30" width="370" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-54" value="<b style=""><font style="font-size: 13px;">Ver 2.0 2025/10/09</font></b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="410" y="49" width="370" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-56" value="topic<br>Subscribe<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="54" y="170" width="76" height="47" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-57" value="topic<br>Publish<br>publish_out" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="532" y="133" width="76" height="47" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-59" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="100" y="560" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-60" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="668" y="591" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-61" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="152" y="498" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-62" value="R/W" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="206" y="312" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-63" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="330" y="530" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-64" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="100" y="273" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-65" value="API Call From CORE" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="384" y="410" width="142" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-66" value="" style="endArrow=none;html=1;rounded=0;exitX=1.017;exitY=0.606;exitDx=0;exitDy=0;startArrow=classic;startFill=1;endFill=0;exitPerimeter=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="196.03999999999996" y="462.1600000000001" as="sourcePoint" /> | |||
| <mxPoint x="610" y="462" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-68" value="<b>Developed by SenLab</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="68" y="734" width="84" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-69" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;opacity=40;" vertex="1" parent="1"> | |||
| <mxGeometry x="560" y="300" width="280" height="470" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-70" value="<b>Developed by AFA Systems</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="568" y="734" width="84" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-71" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||
| <mxGeometry x="870" y="300" width="230" height="470" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-72" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;opacity=40;" vertex="1" parent="1"> | |||
| <mxGeometry x="560" y="300" width="310" height="100" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-73" value="<b>Developed Maestry&nbsp;</b>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="867" y="737" width="84" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-76" value="Kafka" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="72" y="420" width="98" height="70" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-77" value="Decoder" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="18" y="350" width="68" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-78" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0.691;exitY=0.867;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-77" target="9h2_EveRN_1U4OZwnWZC-76"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="50" y="410" as="sourcePoint" /> | |||
| <mxPoint x="100" y="360" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-79" value="R/W" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="62" y="390" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-80" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;entryX=0;entryY=1;entryDx=0;entryDy=0;exitX=1;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-24"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="190" y="360" as="sourcePoint" /> | |||
| <mxPoint x="250" y="310" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-84" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="230" y="412.5" width="120" height="85" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-89" value="Scorpio broker" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="260" y="440.5" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-91" value="" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-76" target="9h2_EveRN_1U4OZwnWZC-84"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="180" y="530" as="sourcePoint" /> | |||
| <mxPoint x="230" y="480" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-92" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.518;entryY=0.008;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84" target="9h2_EveRN_1U4OZwnWZC-98"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="300" y="640" as="sourcePoint" /> | |||
| <mxPoint x="284.5" y="621" as="targetPoint" /> | |||
| <Array as="points" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-93" value="R" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1"> | |||
| <mxGeometry x="212" y="561" width="60" height="30" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-97" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-89" target="9h2_EveRN_1U4OZwnWZC-89"> | |||
| <mxGeometry relative="1" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-98" value="<br>GW <br>and Tracker definition<br>model<br>supported<br>Decode-lib<br>Raw-Data<div><br></div>" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" vertex="1" parent="1"> | |||
| <mxGeometry x="235" y="621" width="110" height="126" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-99" value="Redis cache" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> | |||
| <mxGeometry x="254.5" y="342" width="90.5" height="50" as="geometry" /> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-101" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.403;entryY=0.98;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="9h2_EveRN_1U4OZwnWZC-84" target="9h2_EveRN_1U4OZwnWZC-99"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="260" y="440" as="sourcePoint" /> | |||
| <mxPoint x="310" y="390" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-102" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1"> | |||
| <mxGeometry width="50" height="50" relative="1" as="geometry"> | |||
| <mxPoint x="170" y="420" as="sourcePoint" /> | |||
| <mxPoint x="260" y="390" as="targetPoint" /> | |||
| </mxGeometry> | |||
| </mxCell> | |||
| <mxCell id="9h2_EveRN_1U4OZwnWZC-104" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;opacity=40;" vertex="1" parent="1"> | |||
| <mxGeometry x="18" y="230" width="392" height="540" as="geometry" /> | |||
| </mxCell> | |||
| </root> | |||
| </mxGraphModel> | |||
| </diagram> | |||
| </mxfile> | |||
| @@ -0,0 +1,8 @@ | |||
| # `/api` | |||
| OpenAPI/Swagger specs, JSON schema files, protocol definition files. | |||
| Examples: | |||
| * https://github.com/kubernetes/kubernetes/tree/master/api | |||
| * https://github.com/moby/moby/tree/master/api | |||
| @@ -0,0 +1,3 @@ | |||
| # `/assets` | |||
| Other assets to go along with your repository (images, logos, etc). | |||
| @@ -1 +0,0 @@ | |||
| This folder is dedicated to backend sourcecode | |||
| @@ -0,0 +1,11 @@ | |||
| # `/build` | |||
| Packaging and Continuous Integration. | |||
| Put your cloud (AMI), container (Docker), OS (deb, rpm, pkg) package configurations and scripts in the `/build/package` directory. | |||
| Put your CI (travis, circle, drone) configurations and scripts in the `/build/ci` directory. Note that some of the CI tools (e.g., Travis CI) are very picky about the location of their config files. Try putting the config files in the `/build/ci` directory linking them to the location where the CI tools expect them when possible (don't worry if it's not and if keeping those files in the root directory makes your life easier :-)). | |||
| Examples: | |||
| * https://github.com/cockroachdb/cockroach/tree/master/build | |||
| @@ -0,0 +1,84 @@ | |||
| services: | |||
| emqx: | |||
| image: emqx/emqx:5.8.8 | |||
| container_name: emqx | |||
| environment: | |||
| - EMQX_DASHBOARD__DEFAULT_USERNAME=user | |||
| - EMQX_DASHBOARD__DEFAULT_PASSWORD=pass | |||
| ports: | |||
| - "127.0.0.1:1883:1883" | |||
| healthcheck: | |||
| test: ["CMD", "curl", "-f", "http://localhost:18083/api/v5/status"] | |||
| interval: 10s | |||
| timeout: 5s | |||
| retries: 10 | |||
| start_period: 20s | |||
| kafka: | |||
| image: apache/kafka:3.9.0 | |||
| container_name: kafka | |||
| environment: | |||
| - KAFKA_NODE_ID=1 | |||
| - KAFKA_PROCESS_ROLES=broker,controller | |||
| - KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 | |||
| - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092 | |||
| - KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER | |||
| - KAFKA_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 | |||
| - KAFKA_LOG_DIRS=/tmp/kraft-combined-logs | |||
| - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 | |||
| ports: | |||
| - "127.0.0.1:9092:9092" | |||
| valkey: | |||
| image: valkey/valkey:9.0.0 | |||
| container_name: valkey | |||
| ports: | |||
| - "127.0.0.1:6379:6379" | |||
| presense-decoder: | |||
| build: | |||
| context: ../ | |||
| dockerfile: build/package/Dockerfile.decoder | |||
| image: presense-decoder | |||
| container_name: presense-decoder | |||
| environment: | |||
| - REDIS_URL=valkey:6379 | |||
| - KAFKA_URL=kafka:9092 | |||
| depends_on: | |||
| - kafka | |||
| - valkey | |||
| restart: always | |||
| presense-server: | |||
| build: | |||
| context: ../ | |||
| dockerfile: build/package/Dockerfile.server | |||
| image: presense-server | |||
| container_name: presense-server | |||
| environment: | |||
| - REDIS_URL=valkey:6379 | |||
| - KAFKA_URL=kafka:9092 | |||
| depends_on: | |||
| - kafka | |||
| - emqx | |||
| ports: | |||
| - "127.0.0.1:1902:1902" | |||
| restart: always | |||
| presense-bridge: | |||
| build: | |||
| context: ../ | |||
| dockerfile: build/package/Dockerfile.bridge | |||
| image: presense-bridge | |||
| container_name: presense-bridge | |||
| environment: | |||
| - KAFKA_URL=kafka:9092 | |||
| - MQTT_HOST=emqx:1883 | |||
| - MQTT_USERNAME=user | |||
| - MQTT_PASSWORD=pass | |||
| depends_on: | |||
| kafka: | |||
| condition: service_started | |||
| emqx: | |||
| condition: service_healthy | |||
| restart: always | |||
| @@ -0,0 +1,16 @@ | |||
| # syntax=docker/dockerfile:1 | |||
| FROM golang:1.24.0 | |||
| WORKDIR /app | |||
| COPY go.mod go.sum ./ | |||
| RUN go mod download | |||
| COPY . . | |||
| RUN go build -o app ./cmd/presenSe | |||
| EXPOSE 8080 | |||
| ENTRYPOINT ["./app"] | |||
| @@ -0,0 +1,17 @@ | |||
| # syntax=docker/dockerfile:1 | |||
| FROM golang:1.24.0 AS builder | |||
| WORKDIR /app | |||
| COPY go.mod go.sum ./ | |||
| RUN go mod download | |||
| COPY . . | |||
| RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o bridge ./cmd/bridge | |||
| FROM alpine:latest | |||
| RUN apk add --no-cache ca-certificates | |||
| WORKDIR /app | |||
| COPY --from=builder /app/bridge . | |||
| ENTRYPOINT ["./bridge"] | |||
| @@ -0,0 +1,17 @@ | |||
| # syntax=docker/dockerfile:1 | |||
| FROM golang:1.24.0 AS builder | |||
| WORKDIR /app | |||
| COPY go.mod go.sum ./ | |||
| RUN go mod download | |||
| COPY . . | |||
| RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o decoder ./cmd/decoder | |||
| FROM alpine:latest | |||
| RUN apk add --no-cache ca-certificates | |||
| WORKDIR /app | |||
| COPY --from=builder /app/decoder . | |||
| ENTRYPOINT ["./decoder"] | |||
| @@ -0,0 +1,18 @@ | |||
| # syntax=docker/dockerfile:1 | |||
| FROM golang:1.24.0 AS builder | |||
| WORKDIR /app | |||
| COPY go.mod go.sum ./ | |||
| RUN go mod download | |||
| COPY . . | |||
| RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/server | |||
| FROM alpine:latest | |||
| RUN apk add --no-cache ca-certificates | |||
| WORKDIR /app | |||
| COPY --from=builder /app/server . | |||
| EXPOSE 1902 | |||
| ENTRYPOINT ["./server"] | |||
| @@ -0,0 +1,19 @@ | |||
| # `/cmd` | |||
| Main applications for this project. | |||
| The directory name for each application should match the name of the executable you want to have (e.g., `/cmd/myapp`). | |||
| Don't put a lot of code in the application directory. If you think the code can be imported and used in other projects, then it should live in the `/pkg` directory. If the code is not reusable or if you don't want others to reuse it, put that code in the `/internal` directory. You'll be surprised what others will do, so be explicit about your intentions! | |||
| It's common to have a small `main` function that imports and invokes the code from the `/internal` and `/pkg` directories and nothing else. | |||
| Examples: | |||
| * https://github.com/vmware-tanzu/velero/tree/main/cmd (just a really small `main` function with everything else in packages) | |||
| * https://github.com/moby/moby/tree/master/cmd | |||
| * https://github.com/prometheus/prometheus/tree/main/cmd | |||
| * https://github.com/influxdata/influxdb/tree/master/cmd | |||
| * https://github.com/kubernetes/kubernetes/tree/master/cmd | |||
| * https://github.com/dapr/dapr/tree/master/cmd | |||
| * https://github.com/ethereum/go-ethereum/tree/master/cmd | |||
| @@ -0,0 +1,57 @@ | |||
| package main | |||
| import ( | |||
| "fmt" | |||
| "github.com/AFASystems/presence/internal/pkg/bridge/mqtthandler" | |||
| "github.com/AFASystems/presence/internal/pkg/config" | |||
| "github.com/AFASystems/presence/internal/pkg/kafka" | |||
| "github.com/yosssi/gmq/mqtt" | |||
| "github.com/yosssi/gmq/mqtt/client" | |||
| ) | |||
| func main() { | |||
| cfg := config.Load() | |||
| cli := client.New(&client.Options{ | |||
| ErrorHandler: func(err error) { | |||
| fmt.Println("Error in initiating MQTT client: ", err) | |||
| }, | |||
| }) | |||
| defer cli.Terminate() | |||
| err := cli.Connect(&client.ConnectOptions{ | |||
| Network: "tcp", | |||
| Address: cfg.MQTTHost, | |||
| ClientID: []byte(cfg.MQTTClientID), | |||
| UserName: []byte(cfg.MQTTUser), | |||
| Password: []byte(cfg.MQTTPass), | |||
| }) | |||
| if err != nil { | |||
| fmt.Println("Could not connect to MQTT broker") | |||
| panic(err) | |||
| } | |||
| fmt.Println("Successfuly connected to MQTT broker") | |||
| writer := kafka.KafkaWriter(cfg.KafkaURL, "rawbeacons") | |||
| defer writer.Close() | |||
| err = cli.Subscribe(&client.SubscribeOptions{ | |||
| SubReqs: []*client.SubReq{ | |||
| { | |||
| TopicFilter: []byte("publish_out/#"), | |||
| QoS: mqtt.QoS0, | |||
| Handler: func(topicName, message []byte) { | |||
| mqtthandler.MqttHandler(writer, topicName, message) | |||
| }, | |||
| }, | |||
| }, | |||
| }) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| select {} | |||
| } | |||
| @@ -0,0 +1,206 @@ | |||
| package main | |||
| import ( | |||
| "context" | |||
| "fmt" | |||
| "math" | |||
| "strconv" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/config" | |||
| "github.com/AFASystems/presence/internal/pkg/kafka" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/AFASystems/presence/internal/pkg/mqttclient" | |||
| presenseredis "github.com/AFASystems/presence/internal/pkg/redis" | |||
| "github.com/redis/go-redis/v9" | |||
| ) | |||
| // Move Kafka topics, Redis keys, intervals to env config | |||
| // Replace hardcoded IPs with env vars | |||
| // avoid defers -> lock and unlock right before and after usage | |||
| // Distance formula uses twos_comp incorrectly should parse signed int not hex string | |||
| // Use buffered log instead of fmt.Println ??? | |||
| // Limit metrics slice size with ring buffer ?? | |||
| // handle go routine exit signals with context.WithCancel() ?? | |||
| // Make internal package for Kafka and Redis | |||
| // Make internal package for processor: | |||
| // Helper functions: twos_comp, getBeaconId | |||
| func main() { | |||
| // Load global context to init beacons and latest list | |||
| appCtx := model.AppContext{ | |||
| Beacons: model.BeaconsList{ | |||
| Beacons: make(map[string]model.Beacon), | |||
| }, | |||
| LatestList: model.LatestBeaconsList{ | |||
| LatestList: make(map[string]model.Beacon), | |||
| }, | |||
| } | |||
| cfg := config.Load() | |||
| // Kafka writer idk why yet | |||
| writer := kafka.KafkaWriter(cfg.KafkaURL, "beacons") | |||
| // Kafka reader for Raw MQTT beacons | |||
| rawReader := kafka.KafkaReader(cfg.KafkaURL, "rawbeacons", "someID") | |||
| defer rawReader.Close() | |||
| // Kafka reader for API server updates | |||
| apiReader := kafka.KafkaReader(cfg.KafkaURL, "apibeacons", "someID") | |||
| defer apiReader.Close() | |||
| // Kafka reader for latest list updates | |||
| latestReader := kafka.KafkaReader(cfg.KafkaURL, "latestbeacons", "someID") | |||
| defer latestReader.Close() | |||
| defer writer.Close() | |||
| ctx := context.Background() | |||
| // Init Redis Client | |||
| client := redis.NewClient(&redis.Options{ | |||
| Addr: cfg.RedisURL, | |||
| Password: "", | |||
| }) | |||
| beaconsList := presenseredis.LoadBeaconsList(client, ctx) | |||
| appCtx.Beacons.Beacons = beaconsList | |||
| latestList := presenseredis.LoadLatestList(client, ctx) | |||
| appCtx.LatestList.LatestList = latestList | |||
| // declare channel for collecting Kafka messages | |||
| chRaw := make(chan model.Incoming_json, 2000) | |||
| chApi := make(chan model.ApiUpdate, 2000) | |||
| chLatest := make(chan model.Incoming_json, 2000) | |||
| go kafka.Consume(rawReader, chRaw) | |||
| go kafka.Consume(apiReader, chApi) | |||
| go kafka.Consume(latestReader, chLatest) | |||
| go func() { | |||
| // Syncing Redis cache every 1s with 2 lists: beacons, latest list | |||
| ticker := time.NewTicker(1 * time.Second) | |||
| defer ticker.Stop() | |||
| for range ticker.C { | |||
| presenseredis.SaveBeaconsList(&appCtx, client, ctx) | |||
| presenseredis.SaveLatestList(&appCtx, client, ctx) | |||
| } | |||
| }() | |||
| for { | |||
| select { | |||
| case msg := <-chRaw: | |||
| processIncoming(msg, &appCtx) | |||
| case msg := <-chApi: | |||
| switch msg.Method { | |||
| case "POST": | |||
| appCtx.Beacons.Lock.Lock() | |||
| appCtx.Beacons.Beacons[msg.Beacon.Beacon_id] = msg.Beacon | |||
| case "DELETE": | |||
| _, exists := appCtx.Beacons.Beacons[msg.ID] | |||
| if exists { | |||
| appCtx.Beacons.Lock.Lock() | |||
| delete(appCtx.Beacons.Beacons, msg.ID) | |||
| } | |||
| default: | |||
| fmt.Println("unknown method: ", msg.Method) | |||
| } | |||
| appCtx.Beacons.Lock.Unlock() | |||
| case msg := <-chLatest: | |||
| fmt.Println("latest msg: ", msg) | |||
| } | |||
| } | |||
| } | |||
| func processIncoming(incoming model.Incoming_json, ctx *model.AppContext) { | |||
| defer func() { | |||
| if err := recover(); err != nil { | |||
| fmt.Println("work failed:", err) | |||
| } | |||
| }() | |||
| incoming = mqttclient.IncomingBeaconFilter(incoming) | |||
| id := mqttclient.GetBeaconID(incoming) | |||
| now := time.Now().Unix() | |||
| beacons := &ctx.Beacons | |||
| beacons.Lock.Lock() | |||
| defer beacons.Lock.Unlock() | |||
| latestList := &ctx.LatestList | |||
| latestList.Lock.Lock() | |||
| defer latestList.Lock.Unlock() | |||
| beacon, exists := beacons.Beacons[id] | |||
| if !exists { | |||
| x, exists := latestList.LatestList[id] | |||
| if exists { | |||
| x.Last_seen = now | |||
| x.Incoming_JSON = incoming | |||
| x.Distance = getBeaconDistance(incoming) | |||
| latestList.LatestList[id] = x | |||
| } else { | |||
| latestList.LatestList[id] = model.Beacon{Beacon_id: id, Beacon_type: incoming.Beacon_type, Last_seen: now, Incoming_JSON: incoming, Beacon_location: incoming.Hostname, Distance: getBeaconDistance(incoming)} | |||
| } | |||
| // Move this to seperate routine? | |||
| for k, v := range latestList.LatestList { | |||
| if (now - v.Last_seen) > 10 { | |||
| delete(latestList.LatestList, k) | |||
| } | |||
| } | |||
| return | |||
| } | |||
| updateBeacon(&beacon, incoming) | |||
| beacons.Beacons[id] = beacon | |||
| } | |||
| func getBeaconDistance(incoming model.Incoming_json) float64 { | |||
| rssi := incoming.RSSI | |||
| power := incoming.TX_power | |||
| distance := 100.0 | |||
| ratio := float64(rssi) * (1.0 / float64(twos_comp(power))) | |||
| if ratio < 1.0 { | |||
| distance = math.Pow(ratio, 10) | |||
| } else { | |||
| distance = (0.89976)*math.Pow(ratio, 7.7095) + 0.111 | |||
| } | |||
| return distance | |||
| } | |||
| func updateBeacon(beacon *model.Beacon, incoming model.Incoming_json) { | |||
| now := time.Now().Unix() | |||
| beacon.Incoming_JSON = incoming | |||
| beacon.Last_seen = now | |||
| beacon.Beacon_type = incoming.Beacon_type | |||
| beacon.HB_ButtonCounter = incoming.HB_ButtonCounter | |||
| beacon.HB_Battery = incoming.HB_Battery | |||
| beacon.HB_RandomNonce = incoming.HB_RandomNonce | |||
| beacon.HB_ButtonMode = incoming.HB_ButtonMode | |||
| if beacon.Beacon_metrics == nil { | |||
| beacon.Beacon_metrics = make([]model.BeaconMetric, 10) // 10 is a placeholder for now | |||
| } | |||
| metric := model.BeaconMetric{} | |||
| metric.Distance = getBeaconDistance(incoming) | |||
| metric.Timestamp = now | |||
| metric.Rssi = int64(incoming.RSSI) | |||
| metric.Location = incoming.Hostname | |||
| beacon.Beacon_metrics = append(beacon.Beacon_metrics, metric) | |||
| // Leave the HB button implementation for now | |||
| } | |||
| func twos_comp(inp string) int64 { | |||
| i, _ := strconv.ParseInt("0x"+inp, 0, 64) | |||
| return i - 256 | |||
| } | |||
| @@ -0,0 +1,71 @@ | |||
| package main | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "fmt" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/redis/go-redis/v9" | |||
| ) | |||
| func main() { | |||
| ctx := context.Background() | |||
| client := redis.NewClient(&redis.Options{ | |||
| Addr: "127.0.0.1:6379", | |||
| Password: "", | |||
| }) | |||
| } | |||
| func getLikelyLocations(client *redis.Client, ctx context.Context) { | |||
| beaconsList, err := client.Get(ctx, "beaconsList").Result() | |||
| var beacons = make(map[string]model.Beacon) | |||
| if err == redis.Nil { | |||
| fmt.Println("no beacons list, starting empty") | |||
| } else if err != nil { | |||
| panic(err) | |||
| } else { | |||
| json.Unmarshal([]byte(beaconsList), &beacons) | |||
| } | |||
| for id, beacon := range beacons { | |||
| if len(beacon.Beacon_metrics) == 0 { | |||
| continue | |||
| } | |||
| if isExpired(&beacon, settings) { | |||
| handleExpiredBeacon(&beacon, cl, ctx) | |||
| continue | |||
| } | |||
| best := calculateBestLocation(&beacon) | |||
| updateBeaconState(&beacon, best, settings, ctx, cl) | |||
| appendHTTPResult(ctx, beacon, best) | |||
| ctx.Beacons.Beacons[id] = beacon | |||
| } | |||
| } | |||
| // get likely locations: | |||
| /* | |||
| 1. Locks the http_results list | |||
| 2. inits list to empty struct type -> TODO: what is this list used for | |||
| 3. loops through beacons list -> should be locked? | |||
| 4. check for beacon metrics -> how do you get beacon metrics, I guess it has an array of timestamps | |||
| 5. check for threshold value in the settings | |||
| 5.1. check for property expired location | |||
| 5.2. if location is not expired -> mark it as expired, generate message and send to all clients, | |||
| if clients do not respond close the connection | |||
| 6. Init best location with type Best_location{} -> what is this type | |||
| 7. make locations list -> key: string, val: float64 | |||
| 7.1 set weight for seen and rssi | |||
| 7.2 loop over metrics of the beacon -> some alogirthm based on location value | |||
| I think the algorithm is recording names of different gateways and their rssi's and then from | |||
| that it checks gateway name and makes decisions based on calculated values | |||
| 7.3 writes result in best location and updates list location history with this name if the list | |||
| is longer than 10 elements it removes the first element | |||
| */ | |||
| @@ -0,0 +1,188 @@ | |||
| package main | |||
| import ( | |||
| "encoding/json" | |||
| "fmt" | |||
| "log" | |||
| "os" | |||
| "os/signal" | |||
| "strconv" | |||
| "strings" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/config" | |||
| "github.com/AFASystems/presence/internal/pkg/httpserver" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/AFASystems/presence/internal/pkg/mqttclient" | |||
| "github.com/AFASystems/presence/internal/pkg/persistence" | |||
| "github.com/boltdb/bolt" | |||
| "github.com/gorilla/websocket" | |||
| "github.com/yosssi/gmq/mqtt" | |||
| "github.com/yosssi/gmq/mqtt/client" | |||
| ) | |||
| func main() { | |||
| sigc := make(chan os.Signal, 1) | |||
| signal.Notify(sigc, os.Interrupt) | |||
| cfg := config.Load() | |||
| fmt.Println("hello world") | |||
| db, err := bolt.Open("presence.db", 0644, nil) | |||
| if err != nil { | |||
| log.Fatal(err) | |||
| } | |||
| defer db.Close() | |||
| model.Db = db | |||
| cli := client.New(&client.Options{ | |||
| ErrorHandler: func(err error) { | |||
| fmt.Println(err) | |||
| }, | |||
| }) | |||
| defer cli.Terminate() | |||
| fmt.Println("host: ", cfg.MQTTHost, " Client ID: ", cfg.MQTTClientID, "user: ", cfg.MQTTUser) | |||
| err = cli.Connect(&client.ConnectOptions{ | |||
| Network: "tcp", | |||
| Address: cfg.MQTTHost, | |||
| ClientID: []byte(cfg.MQTTClientID), | |||
| UserName: []byte(cfg.MQTTUser), | |||
| Password: []byte(cfg.MQTTPass), | |||
| }) | |||
| if err != nil { | |||
| fmt.Println("Error comes from here") | |||
| panic(err) | |||
| } | |||
| ctx := &model.AppContext{ | |||
| HTTPResults: model.HTTPResultsList{ | |||
| HTTPResults: model.HTTPLocationsList{Beacons: []model.HTTPLocation{}}, | |||
| }, | |||
| Beacons: model.BeaconsList{ | |||
| Beacons: make(map[string]model.Beacon), | |||
| }, | |||
| ButtonsList: make(map[string]model.Button), | |||
| Settings: model.Settings{ | |||
| Location_confidence: 4, | |||
| Last_seen_threshold: 15, | |||
| Beacon_metrics_size: 30, | |||
| HA_send_interval: 5, | |||
| HA_send_changes_only: false, | |||
| }, | |||
| Clients: make(map[*websocket.Conn]bool), | |||
| Broadcast: make(chan model.Message, 100), | |||
| Locations: model.LocationsList{Locations: make(map[string]model.Location)}, | |||
| LatestList: model.LatestBeaconsList{LatestList: make(map[string]model.Beacon)}, | |||
| } | |||
| persistence.LoadState(model.Db, ctx) | |||
| incomingChan := mqttclient.IncomingMQTTProcessor(1*time.Second, cli, model.Db, ctx) | |||
| err = cli.Subscribe(&client.SubscribeOptions{ | |||
| SubReqs: []*client.SubReq{ | |||
| &client.SubReq{ | |||
| TopicFilter: []byte("publish_out/#"), | |||
| QoS: mqtt.QoS0, | |||
| Handler: func(topicName, message []byte) { | |||
| msgStr := string(message) | |||
| t := strings.Split(string(topicName), "/") | |||
| hostname := t[1] | |||
| fmt.Println("hostname: ", hostname) | |||
| if strings.HasPrefix(msgStr, "[") { | |||
| var readings []model.RawReading | |||
| err := json.Unmarshal(message, &readings) | |||
| if err != nil { | |||
| log.Printf("Error parsing JSON: %v", err) | |||
| return | |||
| } | |||
| for _, reading := range readings { | |||
| if reading.Type == "Gateway" { | |||
| continue | |||
| } | |||
| incoming := model.Incoming_json{ | |||
| Hostname: hostname, | |||
| MAC: reading.MAC, | |||
| RSSI: int64(reading.RSSI), | |||
| Data: reading.RawData, | |||
| HB_ButtonCounter: parseButtonState(reading.RawData), | |||
| } | |||
| incomingChan <- incoming | |||
| } | |||
| } else { | |||
| s := strings.Split(string(message), ",") | |||
| if len(s) < 6 { | |||
| log.Printf("Messaggio CSV non valido: %s", msgStr) | |||
| return | |||
| } | |||
| rawdata := s[4] | |||
| buttonCounter := parseButtonState(rawdata) | |||
| if buttonCounter > 0 { | |||
| incoming := model.Incoming_json{} | |||
| i, _ := strconv.ParseInt(s[3], 10, 64) | |||
| incoming.Hostname = hostname | |||
| incoming.Beacon_type = "hb_button" | |||
| incoming.MAC = s[1] | |||
| incoming.RSSI = i | |||
| incoming.Data = rawdata | |||
| incoming.HB_ButtonCounter = buttonCounter | |||
| read_line := strings.TrimRight(string(s[5]), "\r\n") | |||
| it, err33 := strconv.Atoi(read_line) | |||
| if err33 != nil { | |||
| fmt.Println(it) | |||
| fmt.Println(err33) | |||
| os.Exit(2) | |||
| } | |||
| incomingChan <- incoming | |||
| } | |||
| } | |||
| }, | |||
| }, | |||
| }, | |||
| }) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| fmt.Println("CONNECTED TO MQTT") | |||
| fmt.Println("\n ") | |||
| fmt.Println("Visit http://" + cfg.HTTPAddr + " on your browser to see the web interface") | |||
| fmt.Println("\n ") | |||
| go httpserver.StartHTTPServer(cfg.HTTPAddr, ctx) | |||
| <-sigc | |||
| if err := cli.Disconnect(); err != nil { | |||
| panic(err) | |||
| } | |||
| } | |||
| func parseButtonState(raw string) int64 { | |||
| raw = strings.ToUpper(raw) | |||
| if strings.HasPrefix(raw, "0201060303E1FF12") && len(raw) >= 38 { | |||
| buttonField := raw[34:38] | |||
| if buttonValue, err := strconv.ParseInt(buttonField, 16, 64); err == nil { | |||
| return buttonValue | |||
| } | |||
| } | |||
| if strings.HasPrefix(raw, "02010612FF590") && len(raw) >= 24 { | |||
| counterField := raw[22:24] | |||
| buttonState, err := strconv.ParseInt(counterField, 16, 64) | |||
| if err == nil { | |||
| return buttonState | |||
| } | |||
| } | |||
| return 0 | |||
| } | |||
| @@ -0,0 +1,127 @@ | |||
| package main | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "fmt" | |||
| "net/http" | |||
| "strings" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/gorilla/handlers" | |||
| "github.com/gorilla/mux" | |||
| "github.com/redis/go-redis/v9" | |||
| "github.com/segmentio/kafka-go" | |||
| ) | |||
| func main() { | |||
| HttpServer("127.0.0.1:1902") | |||
| } | |||
| func HttpServer(addr string) { | |||
| headersOk := handlers.AllowedHeaders([]string{"X-Requested-With"}) | |||
| originsOk := handlers.AllowedOrigins([]string{"*"}) | |||
| methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}) | |||
| // Kafka writer that relays messages | |||
| writer := kafkaWriter("127.0.0.1:9092", "apibeacons") | |||
| defer writer.Close() | |||
| r := mux.NewRouter() | |||
| client := redis.NewClient(&redis.Options{ | |||
| Addr: "127.0.0.1:6379", | |||
| Password: "", | |||
| }) | |||
| // For now just add beacon DELETE / GET / POST / PUT methods | |||
| r.HandleFunc("/api/beacons/{beacon_id}", beaconsDeleteHandler(writer)).Methods("DELETE") | |||
| r.HandleFunc("/api/beacons", beaconsListHandler(client)).Methods("GET") | |||
| r.HandleFunc("/api/beacons", beaconsAddHandler(writer)).Methods("POST") | |||
| r.HandleFunc("/api/beacons", beaconsAddHandler(writer)).Methods("PUT") | |||
| http.ListenAndServe(addr, handlers.CORS(originsOk, headersOk, methodsOk)(r)) | |||
| } | |||
| // All the functions should do is just relay messages to the decoder through Kafka | |||
| func kafkaWriter(kafkaURL, topic string) *kafka.Writer { | |||
| return &kafka.Writer{ | |||
| Addr: kafka.TCP(kafkaURL), | |||
| Topic: topic, | |||
| Balancer: &kafka.LeastBytes{}, | |||
| BatchSize: 100, | |||
| BatchTimeout: 10 * time.Millisecond, | |||
| } | |||
| } | |||
| func sendKafkaMessage(writer *kafka.Writer, value *model.ApiUpdate) { | |||
| valueStr, err := json.Marshal(&value) | |||
| if err != nil { | |||
| fmt.Println("error in encoding: ", err) | |||
| } | |||
| msg := kafka.Message{ | |||
| Value: valueStr, | |||
| } | |||
| err = writer.WriteMessages(context.Background(), msg) | |||
| if err != nil { | |||
| fmt.Println("Error in sending kafka message: ") | |||
| } | |||
| } | |||
| func beaconsDeleteHandler(writer *kafka.Writer) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| vars := mux.Vars(r) | |||
| beaconId := vars["beacon_id"] | |||
| apiUpdate := model.ApiUpdate{ | |||
| Method: "DELETE", | |||
| ID: beaconId, | |||
| } | |||
| sendKafkaMessage(writer, &apiUpdate) | |||
| w.Write([]byte("ok")) | |||
| } | |||
| } | |||
| func beaconsAddHandler(writer *kafka.Writer) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| decoder := json.NewDecoder(r.Body) | |||
| var inBeacon model.Beacon | |||
| err := decoder.Decode(&inBeacon) | |||
| if err != nil { | |||
| http.Error(w, err.Error(), 400) | |||
| return | |||
| } | |||
| if (len(strings.TrimSpace(inBeacon.Name)) == 0) || (len(strings.TrimSpace(inBeacon.Beacon_id)) == 0) { | |||
| http.Error(w, "name and beacon_id cannot be blank", 400) | |||
| return | |||
| } | |||
| apiUpdate := model.ApiUpdate{ | |||
| Method: "POST", | |||
| Beacon: inBeacon, | |||
| } | |||
| sendKafkaMessage(writer, &apiUpdate) | |||
| w.Write([]byte("ok")) | |||
| } | |||
| } | |||
| func beaconsListHandler(client *redis.Client) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| beaconsList, err := client.Get(context.Background(), "beaconsList").Result() | |||
| if err == redis.Nil { | |||
| fmt.Println("no beacons list, starting empty") | |||
| http.Error(w, "list is empty", 500) | |||
| } else if err != nil { | |||
| http.Error(w, "Internal server error", 500) | |||
| panic(err) | |||
| } else { | |||
| w.Write([]byte(beaconsList)) | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| # `/configs` | |||
| Configuration file templates or default configs. | |||
| Put your `confd` or `consul-template` template files here. | |||
| @@ -0,0 +1,3 @@ | |||
| # `/deployments` | |||
| IaaS, PaaS, system and container orchestration deployment configurations and templates (docker-compose, kubernetes/helm, mesos, terraform, bosh). | |||
| @@ -0,0 +1,9 @@ | |||
| # `/docs` | |||
| Design and user documents (in addition to your godoc generated documentation). | |||
| Examples: | |||
| * https://github.com/gohugoio/hugo/tree/master/docs | |||
| * https://github.com/openshift/origin/tree/master/docs | |||
| * https://github.com/dapr/dapr/tree/master/docs | |||
| @@ -0,0 +1,9 @@ | |||
| # `/examples` | |||
| Examples for your applications and/or public libraries. | |||
| Examples: | |||
| * https://github.com/nats-io/nats.go/tree/master/examples | |||
| * https://github.com/docker-slim/docker-slim/tree/master/examples | |||
| * https://github.com/hashicorp/packer/tree/master/examples | |||
| @@ -1 +0,0 @@ | |||
| This folder is dedicated to frontend sourcecode | |||
| @@ -0,0 +1,3 @@ | |||
| # `/githooks` | |||
| Git hooks. | |||
| @@ -0,0 +1,24 @@ | |||
| module github.com/AFASystems/presence | |||
| go 1.24.0 | |||
| toolchain go1.24.9 | |||
| require ( | |||
| github.com/boltdb/bolt v1.3.1 | |||
| github.com/gorilla/handlers v1.5.2 | |||
| github.com/gorilla/mux v1.8.1 | |||
| github.com/gorilla/websocket v1.5.3 | |||
| github.com/redis/go-redis/v9 v9.16.0 | |||
| github.com/segmentio/kafka-go v0.4.49 | |||
| github.com/yosssi/gmq v0.0.1 | |||
| ) | |||
| require ( | |||
| github.com/cespare/xxhash/v2 v2.3.0 // indirect | |||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | |||
| github.com/felixge/httpsnoop v1.0.3 // indirect | |||
| github.com/klauspost/compress v1.15.9 // indirect | |||
| github.com/pierrec/lz4/v4 v4.1.15 // indirect | |||
| golang.org/x/sys v0.37.0 // indirect | |||
| ) | |||
| @@ -0,0 +1,48 @@ | |||
| github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= | |||
| github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= | |||
| github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= | |||
| github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= | |||
| github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= | |||
| github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= | |||
| github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | |||
| github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | |||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | |||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | |||
| github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | |||
| github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | |||
| github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= | |||
| github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= | |||
| github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= | |||
| github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= | |||
| github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= | |||
| github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | |||
| github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= | |||
| github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= | |||
| github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= | |||
| github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= | |||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |||
| github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= | |||
| github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= | |||
| github.com/segmentio/kafka-go v0.4.49 h1:GJiNX1d/g+kG6ljyJEoi9++PUMdXGAxb7JGPiDCuNmk= | |||
| github.com/segmentio/kafka-go v0.4.49/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E= | |||
| github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= | |||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | |||
| github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= | |||
| github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= | |||
| github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= | |||
| github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= | |||
| github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= | |||
| github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= | |||
| github.com/yosssi/gmq v0.0.1 h1:GhlDVaAQoi3Mvjul/qJXXGfL4JBeE0GQwbWp3eIsja8= | |||
| github.com/yosssi/gmq v0.0.1/go.mod h1:mReykazh0U1JabvuWh1PEbzzJftqOQWsjr0Lwg5jL1Y= | |||
| golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= | |||
| golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= | |||
| golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= | |||
| golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | |||
| golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= | |||
| golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= | |||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |||
| @@ -0,0 +1,3 @@ | |||
| # `/init` | |||
| System init (systemd, upstart, sysv) and process manager/supervisor (runit, supervisord) configs. | |||
| @@ -0,0 +1,21 @@ | |||
| # `/internal` | |||
| Private application and library code. This is the code you don't want others importing in their applications or libraries. Note that this layout pattern is enforced by the Go compiler itself. See the Go 1.4 [`release notes`](https://golang.org/doc/go1.4#internalpackages) for more details. Note that you are not limited to the top level `internal` directory. You can have more than one `internal` directory at any level of your project tree. | |||
| You can optionally add a bit of extra structure to your internal packages to separate your shared and non-shared internal code. It's not required (especially for smaller projects), but it's nice to have visual clues showing the intended package use. Your actual application code can go in the `/internal/app` directory (e.g., `/internal/app/myapp`) and the code shared by those apps in the `/internal/pkg` directory (e.g., `/internal/pkg/myprivlib`). | |||
| Examples: | |||
| * https://github.com/hashicorp/terraform/tree/main/internal | |||
| * https://github.com/influxdata/influxdb/tree/master/internal | |||
| * https://github.com/perkeep/perkeep/tree/master/internal | |||
| * https://github.com/jaegertracing/jaeger/tree/main/internal | |||
| * https://github.com/moby/moby/tree/master/internal | |||
| * https://github.com/satellity/satellity/tree/main/internal | |||
| * https://github.com/minio/minio/tree/master/internal | |||
| ## `/internal/pkg` | |||
| Examples: | |||
| * https://github.com/hashicorp/waypoint/tree/main/internal/pkg | |||
| @@ -0,0 +1,105 @@ | |||
| package mqtthandler | |||
| import ( | |||
| "fmt" | |||
| "encoding/json" | |||
| "strings" | |||
| "log" | |||
| "strconv" | |||
| "os" | |||
| "context" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/segmentio/kafka-go" | |||
| ) | |||
| func MqttHandler(writer *kafka.Writer, topicName []byte, message []byte) { | |||
| hostname := strings.Split(string(topicName), "/")[1] | |||
| msgStr := string(message) | |||
| if strings.HasPrefix(msgStr, "[") { | |||
| var readings []model.RawReading | |||
| err := json.Unmarshal(message, &readings) | |||
| if err != nil { | |||
| log.Printf("Error parsing JSON: %v", err) | |||
| return | |||
| } | |||
| for _, reading := range readings { | |||
| if reading.Type == "Gateway" { | |||
| continue | |||
| } | |||
| incoming := model.Incoming_json{ | |||
| Hostname: hostname, | |||
| MAC: reading.MAC, | |||
| RSSI: int64(reading.RSSI), | |||
| Data: reading.RawData, | |||
| HB_ButtonCounter: parseButtonState(reading.RawData), | |||
| } | |||
| encodedMsg, err := json.Marshal(incoming) | |||
| if err != nil { | |||
| fmt.Println("Error in marshaling: ", err) | |||
| } | |||
| msg := kafka.Message{ | |||
| Value: encodedMsg, | |||
| } | |||
| err = writer.WriteMessages(context.Background(), msg) | |||
| if err != nil { | |||
| fmt.Println("Error in writing to Kafka: ", err) | |||
| } | |||
| fmt.Println("message sent: ", time.Now()) | |||
| } | |||
| } else { | |||
| s := strings.Split(string(message), ",") | |||
| if len(s) < 6 { | |||
| log.Printf("Messaggio CSV non valido: %s", msgStr) | |||
| return | |||
| } | |||
| rawdata := s[4] | |||
| buttonCounter := parseButtonState(rawdata) | |||
| if buttonCounter > 0 { | |||
| incoming := model.Incoming_json{} | |||
| i, _ := strconv.ParseInt(s[3], 10, 64) | |||
| incoming.Hostname = hostname | |||
| incoming.Beacon_type = "hb_button" | |||
| incoming.MAC = s[1] | |||
| incoming.RSSI = i | |||
| incoming.Data = rawdata | |||
| incoming.HB_ButtonCounter = buttonCounter | |||
| read_line := strings.TrimRight(string(s[5]), "\r\n") | |||
| it, err33 := strconv.Atoi(read_line) | |||
| if err33 != nil { | |||
| fmt.Println(it) | |||
| fmt.Println(err33) | |||
| os.Exit(2) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| func parseButtonState(raw string) int64 { | |||
| raw = strings.ToUpper(raw) | |||
| if strings.HasPrefix(raw, "0201060303E1FF12") && len(raw) >= 38 { | |||
| buttonField := raw[34:38] | |||
| if buttonValue, err := strconv.ParseInt(buttonField, 16, 64); err == nil { | |||
| return buttonValue | |||
| } | |||
| } | |||
| if strings.HasPrefix(raw, "02010612FF590") && len(raw) >= 24 { | |||
| counterField := raw[22:24] | |||
| buttonState, err := strconv.ParseInt(counterField, 16, 64) | |||
| if err == nil { | |||
| return buttonState | |||
| } | |||
| } | |||
| return 0 | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| package config | |||
| import "os" | |||
| type Config struct { | |||
| HTTPAddr string | |||
| WSAddr string | |||
| MQTTHost string | |||
| MQTTUser string | |||
| MQTTPass string | |||
| MQTTClientID string | |||
| DBPath string | |||
| KafkaURL string | |||
| RedisURL string | |||
| } | |||
| // getEnv returns env var value or a default if not set. | |||
| func getEnv(key, def string) string { | |||
| if v := os.Getenv(key); v != "" { | |||
| return v | |||
| } | |||
| return def | |||
| } | |||
| func Load() *Config { | |||
| return &Config{ | |||
| HTTPAddr: getEnv("HTTP_HOST_PATH", "0.0.0.0:8080"), | |||
| WSAddr: getEnv("HTTPWS_HOST_PATH", "0.0.0.0:8088"), | |||
| MQTTHost: getEnv("MQTT_HOST", "127.0.0.1:11883"), | |||
| MQTTUser: getEnv("MQTT_USERNAME", "user"), | |||
| MQTTPass: getEnv("MQTT_PASSWORD", "pass"), | |||
| MQTTClientID: getEnv("MQTT_CLIENT_ID", "presence-detector"), | |||
| DBPath: getEnv("DB_PATH", "/data/conf/presence/presence.db"), | |||
| KafkaURL: getEnv("KAFKA_URL", "127.0.0.1:9092"), | |||
| RedisURL: getEnv("REDIS_URL", "127.0.0.1:6379"), | |||
| } | |||
| } | |||
| @@ -0,0 +1,371 @@ | |||
| package httpserver | |||
| import ( | |||
| "encoding/json" | |||
| "fmt" | |||
| "log" | |||
| "net/http" | |||
| "strings" | |||
| "sync" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/AFASystems/presence/internal/pkg/persistence" | |||
| "github.com/gorilla/handlers" | |||
| "github.com/gorilla/mux" | |||
| "github.com/gorilla/websocket" | |||
| ) | |||
| var ( | |||
| upgrader = websocket.Upgrader{ | |||
| ReadBufferSize: 1024, | |||
| WriteBufferSize: 1024, | |||
| CheckOrigin: func(r *http.Request) bool { | |||
| return true | |||
| }, | |||
| } | |||
| ) | |||
| const ( | |||
| writeWait = 10 * time.Second | |||
| pongWait = 60 * time.Second | |||
| pingPeriod = (pongWait * 9) / 10 | |||
| beaconPeriod = 2 * time.Second | |||
| ) | |||
| // Init store in main or anywhere else and pass it to all initializer functions | |||
| // called in main, then with controllers or handlers use wrapper that takes entire store | |||
| // allocates only the properties that need to be passed into the controller | |||
| func StartHTTPServer(addr string, ctx *model.AppContext) { | |||
| headersOk := handlers.AllowedHeaders([]string{"X-Requested-With"}) | |||
| originsOk := handlers.AllowedOrigins([]string{"*"}) | |||
| methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}) | |||
| // Set up HTTP server | |||
| r := mux.NewRouter() | |||
| r.HandleFunc("/api/results", resultsHandler(&ctx.HTTPResults)) | |||
| r.HandleFunc("/api/beacons/{beacon_id}", BeaconsDeleteHandler(&ctx.Beacons, ctx.ButtonsList)).Methods("DELETE") | |||
| r.HandleFunc("/api/beacons", BeaconsListHandler(&ctx.Beacons)).Methods("GET") | |||
| r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("POST") //since beacons are hashmap, just have put and post be same thing. it'll either add or modify that entry | |||
| r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("PUT") | |||
| r.HandleFunc("/api/latest-beacons", latestBeaconsListHandler(&ctx.LatestList)).Methods("GET") | |||
| r.HandleFunc("/api/settings", SettingsListHandler(&ctx.Settings)).Methods("GET") | |||
| r.HandleFunc("/api/settings", SettingsEditHandler(&ctx.Settings)).Methods("POST") | |||
| r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(http.Dir("static_html/js/")))) | |||
| r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir("static_html/css/")))) | |||
| r.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("static_html/img/")))) | |||
| r.PathPrefix("/").Handler(http.FileServer(http.Dir("static_html/"))) | |||
| http.Handle("/", r) | |||
| mxWS := mux.NewRouter() | |||
| mxWS.HandleFunc("/ws/api/beacons", serveWs(&ctx.HTTPResults)) | |||
| mxWS.HandleFunc("/ws/api/beacons/latest", serveLatestBeaconsWs(&ctx.LatestList)) | |||
| mxWS.HandleFunc("/ws/broadcast", handleConnections(ctx.Clients, &ctx.Broadcast)) | |||
| http.Handle("/ws/", mxWS) | |||
| go handleMessages(ctx.Clients, &ctx.Broadcast) | |||
| http.ListenAndServe(addr, handlers.CORS(originsOk, headersOk, methodsOk)(r)) | |||
| } | |||
| func resultsHandler(httpResults *model.HTTPResultsList) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| httpResults.HTTPResultsLock.Lock() | |||
| defer httpResults.HTTPResultsLock.Unlock() | |||
| js, err := json.Marshal(httpResults.HTTPResults) | |||
| if err != nil { | |||
| http.Error(w, err.Error(), http.StatusInternalServerError) | |||
| return | |||
| } | |||
| w.Write(js) | |||
| } | |||
| } | |||
| func BeaconsListHandler(beacons *model.BeaconsList) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| beacons.Lock.RLock() | |||
| js, err := json.Marshal(beacons.Beacons) | |||
| beacons.Lock.RUnlock() | |||
| if err != nil { | |||
| http.Error(w, err.Error(), http.StatusInternalServerError) | |||
| return | |||
| } | |||
| w.Write(js) | |||
| } | |||
| } | |||
| func BeaconsAddHandler(beacons *model.BeaconsList) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| decoder := json.NewDecoder(r.Body) | |||
| var inBeacon model.Beacon | |||
| err := decoder.Decode(&inBeacon) | |||
| if err != nil { | |||
| http.Error(w, err.Error(), 400) | |||
| return | |||
| } | |||
| if (len(strings.TrimSpace(inBeacon.Name)) == 0) || (len(strings.TrimSpace(inBeacon.Beacon_id)) == 0) { | |||
| http.Error(w, "name and beacon_id cannot be blank", 400) | |||
| return | |||
| } | |||
| beacons.Beacons[inBeacon.Beacon_id] = inBeacon | |||
| err = persistence.PersistBeacons(beacons) | |||
| if err != nil { | |||
| http.Error(w, "trouble persisting beacons list, create bucket", 500) | |||
| return | |||
| } | |||
| w.Write([]byte("ok")) | |||
| } | |||
| } | |||
| func BeaconsDeleteHandler(beacons *model.BeaconsList, buttonsList map[string]model.Button) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| vars := mux.Vars(r) | |||
| fmt.Println("route param: ", vars) | |||
| beaconId := vars["beacon_id"] | |||
| _, ok := beacons.Beacons[beaconId] | |||
| if !ok { | |||
| http.Error(w, "no beacon with the specified id", 400) // change the status code | |||
| return | |||
| } | |||
| delete(beacons.Beacons, beaconId) | |||
| _, ok = buttonsList[beaconId] | |||
| if ok { | |||
| delete(buttonsList, beaconId) | |||
| } | |||
| err := persistence.PersistBeacons(beacons) | |||
| if err != nil { | |||
| http.Error(w, "trouble persisting beacons list, create bucket", 500) | |||
| return | |||
| } | |||
| w.Write([]byte("ok")) | |||
| } | |||
| } | |||
| func latestBeaconsListHandler(latestList *model.LatestBeaconsList) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| latestList.Lock.RLock() | |||
| var la = make([]model.Beacon, 0) | |||
| for _, b := range latestList.LatestList { | |||
| la = append(la, b) | |||
| } | |||
| latestList.Lock.RUnlock() | |||
| js, err := json.Marshal(la) | |||
| if err != nil { | |||
| http.Error(w, err.Error(), http.StatusInternalServerError) | |||
| return | |||
| } | |||
| w.Write(js) | |||
| } | |||
| } | |||
| func SettingsListHandler(settings *model.Settings) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| js, err := json.Marshal(settings) | |||
| if err != nil { | |||
| http.Error(w, err.Error(), http.StatusInternalServerError) | |||
| return | |||
| } | |||
| w.Write(js) | |||
| } | |||
| } | |||
| func SettingsEditHandler(settings *model.Settings) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| decoder := json.NewDecoder(r.Body) | |||
| var inSettings model.Settings | |||
| err := decoder.Decode(&inSettings) | |||
| if err != nil { | |||
| http.Error(w, err.Error(), 400) | |||
| return | |||
| } | |||
| //make sure values are > 0 | |||
| if (inSettings.Location_confidence <= 0) || | |||
| (inSettings.Last_seen_threshold <= 0) || | |||
| (inSettings.HA_send_interval <= 0) { | |||
| http.Error(w, "values must be greater than 0", 400) | |||
| return | |||
| } | |||
| *settings = inSettings | |||
| err = persistence.PersistSettings(settings) | |||
| if err != nil { | |||
| http.Error(w, "trouble persisting settings, create bucket", 500) | |||
| return | |||
| } | |||
| w.Write([]byte("ok")) | |||
| } | |||
| } | |||
| func reader(ws *websocket.Conn) { | |||
| defer ws.Close() | |||
| ws.SetReadLimit(512) | |||
| ws.SetReadDeadline(time.Now().Add(pongWait)) | |||
| ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) | |||
| for { | |||
| _, _, err := ws.ReadMessage() | |||
| if err != nil { | |||
| break | |||
| } | |||
| } | |||
| } | |||
| func writer(ws *websocket.Conn, httpResult *model.HTTPResultsList) { | |||
| pingTicker := time.NewTicker(pingPeriod) | |||
| beaconTicker := time.NewTicker(beaconPeriod) | |||
| defer func() { | |||
| pingTicker.Stop() | |||
| beaconTicker.Stop() | |||
| ws.Close() | |||
| }() | |||
| for { | |||
| select { | |||
| case <-beaconTicker.C: | |||
| httpResult.HTTPResultsLock.Lock() | |||
| defer httpResult.HTTPResultsLock.Unlock() | |||
| js, err := json.Marshal(httpResult.HTTPResults) | |||
| if err != nil { | |||
| js = []byte("error") | |||
| } | |||
| ws.SetWriteDeadline(time.Now().Add(writeWait)) | |||
| if err := ws.WriteMessage(websocket.TextMessage, js); err != nil { | |||
| return | |||
| } | |||
| case <-pingTicker.C: | |||
| ws.SetWriteDeadline(time.Now().Add(writeWait)) | |||
| if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { | |||
| return | |||
| } | |||
| } | |||
| } | |||
| } | |||
| func serveWs(httpResult *model.HTTPResultsList) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| ws, err := upgrader.Upgrade(w, r, nil) | |||
| if err != nil { | |||
| if _, ok := err.(websocket.HandshakeError); !ok { | |||
| log.Println(err) | |||
| } | |||
| return | |||
| } | |||
| go writer(ws, httpResult) | |||
| reader(ws) | |||
| } | |||
| } | |||
| func latestBeaconWriter(ws *websocket.Conn, latestBeaconsList map[string]model.Beacon, lock *sync.RWMutex) { | |||
| pingTicker := time.NewTicker(pingPeriod) | |||
| beaconTicker := time.NewTicker(beaconPeriod) | |||
| defer func() { | |||
| pingTicker.Stop() | |||
| beaconTicker.Stop() | |||
| ws.Close() | |||
| }() | |||
| for { | |||
| select { | |||
| case <-beaconTicker.C: | |||
| lock.RLock() | |||
| var la = make([]model.Beacon, 0) | |||
| for _, b := range latestBeaconsList { | |||
| la = append(la, b) | |||
| } | |||
| lock.RUnlock() | |||
| js, err := json.Marshal(la) | |||
| if err != nil { | |||
| js = []byte("error") | |||
| } | |||
| ws.SetWriteDeadline(time.Now().Add(writeWait)) | |||
| if err := ws.WriteMessage(websocket.TextMessage, js); err != nil { | |||
| return | |||
| } | |||
| case <-pingTicker.C: | |||
| ws.SetWriteDeadline(time.Now().Add(writeWait)) | |||
| if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { | |||
| return | |||
| } | |||
| } | |||
| } | |||
| } | |||
| func serveLatestBeaconsWs(latestList *model.LatestBeaconsList) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| ws, err := upgrader.Upgrade(w, r, nil) | |||
| if err != nil { | |||
| if _, ok := err.(websocket.HandshakeError); !ok { | |||
| log.Println(err) | |||
| } | |||
| return | |||
| } | |||
| go latestBeaconWriter(ws, latestList.LatestList, &latestList.Lock) | |||
| reader(ws) | |||
| } | |||
| } | |||
| func handleConnections(clients map[*websocket.Conn]bool, broadcast *chan model.Message) http.HandlerFunc { | |||
| return func(w http.ResponseWriter, r *http.Request) { | |||
| ws, err := upgrader.Upgrade(w, r, nil) | |||
| if err != nil { | |||
| log.Fatal(err) | |||
| } | |||
| defer ws.Close() | |||
| clients[ws] = true | |||
| for { | |||
| var msg model.Message | |||
| err := ws.ReadJSON(&msg) | |||
| if err != nil { | |||
| log.Printf("error: %v", err) | |||
| delete(clients, ws) | |||
| break | |||
| } | |||
| *broadcast <- msg | |||
| } | |||
| } | |||
| } | |||
| func handleMessages(clients map[*websocket.Conn]bool, broadcast *chan model.Message) { | |||
| for { | |||
| msg := <-*broadcast | |||
| for client := range clients { | |||
| err := client.WriteJSON(msg) | |||
| if err != nil { | |||
| log.Printf("error: %v", err) | |||
| client.Close() | |||
| delete(clients, client) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| # Server | |||
| TODO: refactor to structure: router -> controller -> service, possibly use swagger or any other package to define structure of the API server | |||
| @@ -0,0 +1,27 @@ | |||
| package kafka | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "fmt" | |||
| "github.com/segmentio/kafka-go" | |||
| ) | |||
| func Consume[T any](r *kafka.Reader, ch chan<- T) { | |||
| for { | |||
| msg, err := r.ReadMessage(context.Background()) | |||
| if err != nil { | |||
| fmt.Println("error reading message:", err) | |||
| continue | |||
| } | |||
| var data T | |||
| if err := json.Unmarshal(msg.Value, &data); err != nil { | |||
| fmt.Println("error decoding:", err) | |||
| continue | |||
| } | |||
| ch <- data | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| package kafka | |||
| import ( | |||
| "strings" | |||
| "github.com/segmentio/kafka-go" | |||
| ) | |||
| func KafkaReader(kafkaURL, topic, groupID string) *kafka.Reader { | |||
| brokers := strings.Split(kafkaURL, ",") | |||
| return kafka.NewReader(kafka.ReaderConfig{ | |||
| Brokers: brokers, | |||
| GroupID: groupID, | |||
| Topic: topic, | |||
| MinBytes: 1, | |||
| MaxBytes: 10e6, | |||
| }) | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| package kafka | |||
| import ( | |||
| "time" | |||
| "github.com/segmentio/kafka-go" | |||
| ) | |||
| func KafkaWriter(kafkaURL, topic string) *kafka.Writer { | |||
| return &kafka.Writer{ | |||
| Addr: kafka.TCP(kafkaURL), | |||
| Topic: topic, | |||
| Balancer: &kafka.LeastBytes{}, | |||
| BatchSize: 100, | |||
| BatchTimeout: 10 * time.Millisecond, | |||
| } | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| # MODELS | |||
| This file includes type definitions for aggregate struct types | |||
| @@ -0,0 +1,207 @@ | |||
| package model | |||
| import ( | |||
| "sync" | |||
| "github.com/boltdb/bolt" | |||
| "github.com/gorilla/websocket" | |||
| ) | |||
| // Settings defines configuration parameters for presence detection behavior. | |||
| type Settings struct { | |||
| Location_confidence int64 `json:"location_confidence"` | |||
| Last_seen_threshold int64 `json:"last_seen_threshold"` | |||
| Beacon_metrics_size int `json:"beacon_metrics_size"` | |||
| HA_send_interval int64 `json:"ha_send_interval"` | |||
| HA_send_changes_only bool `json:"ha_send_changes_only"` | |||
| } | |||
| // Incoming_json represents the JSON payload received from beacon messages. | |||
| type Incoming_json struct { | |||
| Hostname string `json:"hostname"` | |||
| MAC string `json:"mac"` | |||
| RSSI int64 `json:"rssi"` | |||
| Is_scan_response string `json:"is_scan_response"` | |||
| Ttype string `json:"type"` | |||
| Data string `json:"data"` | |||
| Beacon_type string `json:"beacon_type"` | |||
| UUID string `json:"uuid"` | |||
| Major string `json:"major"` | |||
| Minor string `json:"minor"` | |||
| TX_power string `json:"tx_power"` | |||
| Namespace string `json:"namespace"` | |||
| Instance_id string `json:"instance_id"` | |||
| // button stuff | |||
| HB_ButtonCounter int64 `json:"hb_button_counter"` | |||
| HB_ButtonCounter_Prev int64 `json:"hb_button_counter_prev"` | |||
| HB_Battery int64 `json:"hb_button_battery"` | |||
| HB_RandomNonce string `json:"hb_button_random"` | |||
| HB_ButtonMode string `json:"hb_button_mode"` | |||
| } | |||
| // Advertisement describes a generic beacon advertisement payload. | |||
| type Advertisement struct { | |||
| ttype string | |||
| content string | |||
| seen int64 | |||
| } | |||
| // BeaconMetric stores signal and distance data for a beacon. | |||
| type BeaconMetric struct { | |||
| Location string | |||
| Distance float64 | |||
| Rssi int64 | |||
| Timestamp int64 | |||
| } | |||
| // Location defines a physical location and synchronization control. | |||
| type Location struct { | |||
| name string | |||
| lock sync.RWMutex | |||
| } | |||
| // BestLocation represents the most probable location of a beacon. | |||
| type BestLocation struct { | |||
| Distance float64 | |||
| Name string | |||
| Last_seen int64 | |||
| } | |||
| // HTTPLocation describes a beacon's state as served over HTTP. | |||
| type HTTPLocation struct { | |||
| Previous_confident_location string `json:"previous_confident_location"` | |||
| Distance float64 `json:"distance"` | |||
| Name string `json:"name"` | |||
| Beacon_name string `json:"beacon_name"` | |||
| Beacon_id string `json:"beacon_id"` | |||
| Beacon_type string `json:"beacon_type"` | |||
| HB_Battery int64 `json:"hb_button_battery"` | |||
| HB_ButtonMode string `json:"hb_button_mode"` | |||
| HB_ButtonCounter int64 `json:"hb_button_counter"` | |||
| Location string `json:"location"` | |||
| Last_seen int64 `json:"last_seen"` | |||
| } | |||
| // LocationChange defines a change event for a beacon's detected location. | |||
| type LocationChange struct { | |||
| Beacon_ref Beacon `json:"beacon_info"` | |||
| Name string `json:"name"` | |||
| Beacon_name string `json:"beacon_name"` | |||
| Previous_location string `json:"previous_location"` | |||
| New_location string `json:"new_location"` | |||
| Timestamp int64 `json:"timestamp"` | |||
| } | |||
| // HAMessage represents a Home Assistant integration payload. | |||
| type HAMessage struct { | |||
| Beacon_id string `json:"id"` | |||
| Beacon_name string `json:"name"` | |||
| Distance float64 `json:"distance"` | |||
| } | |||
| // HTTPLocationsList aggregates all beacon HTTP states. | |||
| type HTTPLocationsList struct { | |||
| Beacons []HTTPLocation `json:"beacons"` | |||
| //Buttons []Button `json:"buttons"` | |||
| } | |||
| // Beacon holds all relevant information about a tracked beacon device. | |||
| type Beacon struct { | |||
| Name string `json:"name"` | |||
| Beacon_id string `json:"beacon_id"` | |||
| Beacon_type string `json:"beacon_type"` | |||
| Beacon_location string `json:"beacon_location"` | |||
| Last_seen int64 `json:"last_seen"` | |||
| Incoming_JSON Incoming_json `json:"incoming_json"` | |||
| Distance float64 `json:"distance"` | |||
| Previous_location string | |||
| Previous_confident_location string | |||
| Expired_location string | |||
| Location_confidence int64 | |||
| Location_history []string | |||
| Beacon_metrics []BeaconMetric | |||
| HB_ButtonCounter int64 `json:"hb_button_counter"` | |||
| HB_ButtonCounter_Prev int64 `json:"hb_button_counter_prev"` | |||
| HB_Battery int64 `json:"hb_button_battery"` | |||
| HB_RandomNonce string `json:"hb_button_random"` | |||
| HB_ButtonMode string `json:"hb_button_mode"` | |||
| } | |||
| // Button represents a hardware button beacon device. | |||
| type Button struct { | |||
| Name string `json:"name"` | |||
| Button_id string `json:"button_id"` | |||
| Button_type string `json:"button_type"` | |||
| Button_location string `json:"button_location"` | |||
| Incoming_JSON Incoming_json `json:"incoming_json"` | |||
| Distance float64 `json:"distance"` | |||
| Last_seen int64 `json:"last_seen"` | |||
| HB_ButtonCounter int64 `json:"hb_button_counter"` | |||
| HB_Battery int64 `json:"hb_button_battery"` | |||
| HB_RandomNonce string `json:"hb_button_random"` | |||
| HB_ButtonMode string `json:"hb_button_mode"` | |||
| } | |||
| // BeaconsList holds all known beacons and their synchronization lock. | |||
| type BeaconsList struct { | |||
| Beacons map[string]Beacon `json:"beacons"` | |||
| Lock sync.RWMutex | |||
| } | |||
| // LocationsList holds all known locations with concurrency protection. | |||
| type LocationsList struct { | |||
| Locations map[string]Location | |||
| Lock sync.RWMutex | |||
| } | |||
| // Message defines the WebSocket or broadcast message payload. | |||
| type Message struct { | |||
| Email string `json:"email"` | |||
| Username string `json:"username"` | |||
| Message string `json:"message"` | |||
| } | |||
| // RawReading represents an incoming raw sensor reading. | |||
| type RawReading struct { | |||
| Timestamp string `json:"timestamp"` | |||
| Type string `json:"type"` | |||
| MAC string `json:"mac"` | |||
| RSSI int `json:"rssi"` | |||
| RawData string `json:"rawData"` | |||
| } | |||
| type LatestBeaconsList struct { | |||
| LatestList map[string]Beacon | |||
| Lock sync.RWMutex | |||
| } | |||
| type HTTPResultsList struct { | |||
| HTTPResultsLock sync.RWMutex | |||
| HTTPResults HTTPLocationsList | |||
| } | |||
| type AppContext struct { | |||
| HTTPResults HTTPResultsList | |||
| Beacons BeaconsList | |||
| ButtonsList map[string]Button | |||
| Settings Settings | |||
| Clients map[*websocket.Conn]bool | |||
| Broadcast chan Message | |||
| Locations LocationsList | |||
| LatestList LatestBeaconsList | |||
| } | |||
| type ApiUpdate struct { | |||
| Method string | |||
| Beacon Beacon | |||
| ID string | |||
| } | |||
| var World = []byte("presence") | |||
| var Db *bolt.DB | |||
| var HTTPHostPathPtr *string | |||
| var HTTPWSHostPathPtr *string | |||
| @@ -0,0 +1,128 @@ | |||
| package mqttclient | |||
| import ( | |||
| "bytes" | |||
| "encoding/json" | |||
| "fmt" | |||
| "log" | |||
| "math" | |||
| "os/exec" | |||
| "strconv" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/yosssi/gmq/mqtt" | |||
| "github.com/yosssi/gmq/mqtt/client" | |||
| ) | |||
| func GetBeaconID(incoming model.Incoming_json) string { | |||
| unique_id := fmt.Sprintf("%s", incoming.MAC) | |||
| return unique_id | |||
| } | |||
| func updateLatestList(incoming model.Incoming_json, now int64, latestList *model.LatestBeaconsList) { | |||
| latestList.Lock.Lock() | |||
| defer latestList.Lock.Unlock() | |||
| b := model.Beacon{ | |||
| Beacon_id: GetBeaconID(incoming), | |||
| Beacon_type: incoming.Beacon_type, | |||
| Last_seen: now, | |||
| Incoming_JSON: incoming, | |||
| Beacon_location: incoming.Hostname, | |||
| Distance: getBeaconDistance(incoming), | |||
| } | |||
| latestList.LatestList[b.Beacon_id] = b | |||
| for id, v := range latestList.LatestList { | |||
| if now-v.Last_seen > 10 { | |||
| delete(latestList.LatestList, id) | |||
| } | |||
| } | |||
| } | |||
| func updateBeaconData(beacon *model.Beacon, incoming model.Incoming_json, now int64, cl *client.Client, settings *model.Settings) { | |||
| beacon.Incoming_JSON = incoming | |||
| beacon.Last_seen = now | |||
| beacon.Beacon_type = incoming.Beacon_type | |||
| beacon.HB_ButtonCounter = incoming.HB_ButtonCounter | |||
| beacon.HB_Battery = incoming.HB_Battery | |||
| beacon.HB_RandomNonce = incoming.HB_RandomNonce | |||
| beacon.HB_ButtonMode = incoming.HB_ButtonMode | |||
| m := model.BeaconMetric{ | |||
| Distance: getBeaconDistance(incoming), | |||
| Timestamp: now, | |||
| Rssi: int64(incoming.RSSI), | |||
| Location: incoming.Hostname, | |||
| } | |||
| beacon.Beacon_metrics = append(beacon.Beacon_metrics, m) | |||
| if len(beacon.Beacon_metrics) > settings.Beacon_metrics_size { | |||
| beacon.Beacon_metrics = beacon.Beacon_metrics[1:] | |||
| } | |||
| if beacon.HB_ButtonCounter_Prev != beacon.HB_ButtonCounter { | |||
| beacon.HB_ButtonCounter_Prev = incoming.HB_ButtonCounter | |||
| sendButtonPressed(*beacon, cl) | |||
| } | |||
| } | |||
| func sendButtonPressed(beacon model.Beacon, cl *client.Client) { | |||
| btn_msg, err := json.Marshal(beacon) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| err = cl.Publish(&client.PublishOptions{ | |||
| QoS: mqtt.QoS1, | |||
| TopicName: []byte("afa-systems/presence/button/" + beacon.Beacon_id), | |||
| Message: btn_msg, | |||
| }) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| s := fmt.Sprintf("/usr/bin/php /usr/local/presence/alarm_handler.php --idt=%s --idr=%s --st=%d", beacon.Beacon_id, beacon.Incoming_JSON.Hostname, beacon.HB_ButtonCounter) | |||
| err, out, errout := Shellout(s) | |||
| if err != nil { | |||
| log.Printf("error: %v\n", err) | |||
| } | |||
| fmt.Println("--- stdout ---") | |||
| fmt.Println(out) | |||
| fmt.Println("--- stderr ---") | |||
| fmt.Println(errout) | |||
| } | |||
| func getBeaconDistance(incoming model.Incoming_json) float64 { | |||
| distance := 1000.0 | |||
| distance = getiBeaconDistance(incoming.RSSI, incoming.TX_power) | |||
| return distance | |||
| } | |||
| func getiBeaconDistance(rssi int64, power string) float64 { | |||
| ratio := float64(rssi) * (1.0 / float64(twos_comp(power))) | |||
| distance := 100.0 | |||
| if ratio < 1.0 { | |||
| distance = math.Pow(ratio, 10) | |||
| } else { | |||
| distance = (0.89976)*math.Pow(ratio, 7.7095) + 0.111 | |||
| } | |||
| return distance | |||
| } | |||
| func twos_comp(inp string) int64 { | |||
| i, _ := strconv.ParseInt("0x"+inp, 0, 64) | |||
| return i - 256 | |||
| } | |||
| func Shellout(command string) (error, string, string) { | |||
| var stdout bytes.Buffer | |||
| var stderr bytes.Buffer | |||
| cmd := exec.Command("bash", "-c", command) | |||
| cmd.Stdout = &stdout | |||
| cmd.Stderr = &stderr | |||
| err := cmd.Run() | |||
| return err, stdout.String(), stderr.String() | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| package mqttclient | |||
| import ( | |||
| "fmt" | |||
| "strconv" | |||
| "strings" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| ) | |||
| func IncomingBeaconFilter(incoming model.Incoming_json) model.Incoming_json { | |||
| out_json := incoming | |||
| if incoming.Beacon_type == "hb_button" { | |||
| raw_data := incoming.Data | |||
| hb_button_prefix_str := fmt.Sprintf("02010612FF5900") | |||
| if strings.HasPrefix(raw_data, hb_button_prefix_str) { | |||
| out_json.Namespace = "ddddeeeeeeffff5544ff" | |||
| counter_str := fmt.Sprintf("0x%s", raw_data[22:24]) | |||
| counter, _ := strconv.ParseInt(counter_str, 0, 64) | |||
| out_json.HB_ButtonCounter = counter | |||
| battery_str := fmt.Sprintf("0x%s%s", raw_data[20:22], raw_data[18:20]) | |||
| battery, _ := strconv.ParseInt(battery_str, 0, 64) | |||
| out_json.HB_Battery = battery | |||
| out_json.TX_power = fmt.Sprintf("0x%s", "4") | |||
| out_json.Beacon_type = "hb_button" | |||
| out_json.HB_ButtonMode = "presence_button" | |||
| } | |||
| } | |||
| return out_json | |||
| } | |||
| @@ -0,0 +1,165 @@ | |||
| package mqttclient | |||
| import ( | |||
| "encoding/json" | |||
| "log" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/AFASystems/presence/internal/pkg/persistence" | |||
| "github.com/yosssi/gmq/mqtt" | |||
| "github.com/yosssi/gmq/mqtt/client" | |||
| ) | |||
| func getLikelyLocations(settings *model.Settings, ctx *model.AppContext, cl *client.Client) { | |||
| ctx.HTTPResults.HTTPResultsLock.Lock() | |||
| defer ctx.HTTPResults.HTTPResultsLock.Unlock() | |||
| ctx.HTTPResults.HTTPResults = model.HTTPLocationsList{Beacons: []model.HTTPLocation{}} | |||
| shouldPersist := false | |||
| for id, beacon := range ctx.Beacons.Beacons { | |||
| if len(beacon.Beacon_metrics) == 0 { | |||
| continue | |||
| } | |||
| if isExpired(&beacon, settings) { | |||
| handleExpiredBeacon(&beacon, cl, ctx) | |||
| continue | |||
| } | |||
| best := calculateBestLocation(&beacon) | |||
| updateBeaconState(&beacon, best, settings, ctx, cl) | |||
| appendHTTPResult(ctx, beacon, best) | |||
| ctx.Beacons.Beacons[id] = beacon | |||
| shouldPersist = true | |||
| } | |||
| if shouldPersist { | |||
| persistence.PersistBeacons(&ctx.Beacons) | |||
| } | |||
| } | |||
| func isExpired(b *model.Beacon, s *model.Settings) bool { | |||
| return time.Now().Unix()-b.Beacon_metrics[len(b.Beacon_metrics)-1].Timestamp > s.Last_seen_threshold | |||
| } | |||
| func handleExpiredBeacon(b *model.Beacon, cl *client.Client, ctx *model.AppContext) { | |||
| if b.Expired_location == "expired" { | |||
| return | |||
| } | |||
| b.Expired_location = "expired" | |||
| msg := model.Message{ | |||
| Email: b.Previous_confident_location, | |||
| Username: b.Name, | |||
| Message: "expired", | |||
| } | |||
| data, _ := json.Marshal(msg) | |||
| log.Println(string(data)) | |||
| ctx.Broadcast <- msg | |||
| } | |||
| func calculateBestLocation(b *model.Beacon) model.BestLocation { | |||
| locScores := map[string]float64{} | |||
| for _, m := range b.Beacon_metrics { | |||
| score := 1.5 + 0.75*(1.0-(float64(m.Rssi)/-100.0)) | |||
| locScores[m.Location] += score | |||
| } | |||
| bestName, bestScore := "", 0.0 | |||
| for name, score := range locScores { | |||
| if score > bestScore { | |||
| bestName, bestScore = name, score | |||
| } | |||
| } | |||
| last := b.Beacon_metrics[len(b.Beacon_metrics)-1] | |||
| return model.BestLocation{Name: bestName, Distance: last.Distance, Last_seen: last.Timestamp} | |||
| } | |||
| func updateBeaconState(b *model.Beacon, best model.BestLocation, s *model.Settings, ctx *model.AppContext, cl *client.Client) { | |||
| updateLocationHistory(b, best.Name) | |||
| updateConfidence(b, best.Name, s) | |||
| if locationChanged(b, best, s) { | |||
| publishLocationChange(b, best, cl) | |||
| b.Location_confidence = 0 | |||
| b.Previous_confident_location = best.Name | |||
| } | |||
| } | |||
| func updateLocationHistory(b *model.Beacon, loc string) { | |||
| b.Location_history = append(b.Location_history, loc) | |||
| if len(b.Location_history) > 10 { | |||
| b.Location_history = b.Location_history[1:] | |||
| } | |||
| } | |||
| func updateConfidence(b *model.Beacon, loc string, s *model.Settings) { | |||
| counts := map[string]int{} | |||
| for _, l := range b.Location_history { | |||
| counts[l]++ | |||
| } | |||
| maxCount, mostCommon := 0, "" | |||
| for l, c := range counts { | |||
| if c > maxCount { | |||
| maxCount, mostCommon = c, l | |||
| } | |||
| } | |||
| if maxCount >= 7 { | |||
| if mostCommon == b.Previous_confident_location { | |||
| b.Location_confidence++ | |||
| } else { | |||
| b.Location_confidence = 1 | |||
| b.Previous_confident_location = mostCommon | |||
| } | |||
| } | |||
| } | |||
| func locationChanged(b *model.Beacon, best model.BestLocation, s *model.Settings) bool { | |||
| return (b.Location_confidence == s.Location_confidence && | |||
| b.Previous_confident_location != best.Name) || | |||
| b.Expired_location == "expired" | |||
| } | |||
| func publishLocationChange(b *model.Beacon, best model.BestLocation, cl *client.Client) { | |||
| location := best.Name | |||
| if b.Expired_location == "expired" { | |||
| location = "expired" | |||
| } | |||
| js, err := json.Marshal(model.LocationChange{ | |||
| Beacon_ref: *b, | |||
| Name: b.Name, | |||
| Previous_location: b.Previous_confident_location, | |||
| New_location: location, | |||
| Timestamp: time.Now().Unix(), | |||
| }) | |||
| if err != nil { | |||
| return | |||
| } | |||
| err = cl.Publish(&client.PublishOptions{ | |||
| QoS: mqtt.QoS1, | |||
| TopicName: []byte("afa-systems/presence/changes"), | |||
| Message: js, | |||
| }) | |||
| if err != nil { | |||
| log.Printf("mqtt publish error: %v", err) | |||
| } | |||
| } | |||
| func appendHTTPResult(ctx *model.AppContext, b model.Beacon, best model.BestLocation) { | |||
| ctx.HTTPResults.HTTPResultsLock.Lock() | |||
| defer ctx.HTTPResults.HTTPResultsLock.Unlock() | |||
| r := model.HTTPLocation{ | |||
| Name: b.Name, | |||
| Beacon_id: b.Beacon_id, | |||
| Location: best.Name, | |||
| Distance: best.Distance, | |||
| Last_seen: best.Last_seen, | |||
| } | |||
| ctx.HTTPResults.HTTPResults.Beacons = append(ctx.HTTPResults.HTTPResults.Beacons, r) | |||
| } | |||
| @@ -0,0 +1,62 @@ | |||
| package mqttclient | |||
| import ( | |||
| "fmt" | |||
| "log" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/AFASystems/presence/internal/pkg/persistence" | |||
| "github.com/boltdb/bolt" | |||
| "github.com/yosssi/gmq/mqtt/client" | |||
| ) | |||
| func IncomingMQTTProcessor(updateInterval time.Duration, cl *client.Client, db *bolt.DB, ctx *model.AppContext) chan<- model.Incoming_json { | |||
| ch := make(chan model.Incoming_json, 2000) | |||
| persistence.CreateBucketIfNotExists(db) | |||
| ticker := time.NewTicker(updateInterval) | |||
| go runProcessor(ticker, cl, ch, ctx) | |||
| return ch | |||
| } | |||
| func runProcessor(ticker *time.Ticker, cl *client.Client, ch <-chan model.Incoming_json, ctx *model.AppContext) { | |||
| for { | |||
| select { | |||
| case <-ticker.C: | |||
| getLikelyLocations(&ctx.Settings, ctx, cl) | |||
| case incoming := <-ch: | |||
| ProcessIncoming(incoming, cl, ctx) | |||
| } | |||
| } | |||
| } | |||
| func ProcessIncoming(incoming model.Incoming_json, cl *client.Client, ctx *model.AppContext) { | |||
| defer func() { | |||
| if err := recover(); err != nil { | |||
| log.Println("work failed:", err) | |||
| } | |||
| }() | |||
| incoming = IncomingBeaconFilter(incoming) | |||
| id := GetBeaconID(incoming) | |||
| now := time.Now().Unix() | |||
| beacons := &ctx.Beacons | |||
| beacons.Lock.Lock() | |||
| defer beacons.Lock.Unlock() | |||
| latestList := &ctx.LatestList | |||
| settings := &ctx.Settings | |||
| beacon, ok := beacons.Beacons[id] | |||
| if !ok { | |||
| updateLatestList(incoming, now, latestList) | |||
| return | |||
| } | |||
| fmt.Println("updating beacon data") | |||
| updateBeaconData(&beacon, incoming, now, cl, settings) | |||
| beacons.Beacons[beacon.Beacon_id] = beacon | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| package persistence | |||
| import ( | |||
| "log" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/boltdb/bolt" | |||
| ) | |||
| func CreateBucketIfNotExists(db *bolt.DB) { | |||
| err := db.Update(func(tx *bolt.Tx) error { | |||
| _, err := tx.CreateBucketIfNotExists(model.World) | |||
| return err | |||
| }) | |||
| if err != nil { | |||
| log.Fatal(err) | |||
| } | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| package persistence | |||
| import ( | |||
| "bytes" | |||
| "encoding/gob" | |||
| "log" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/boltdb/bolt" | |||
| ) | |||
| func LoadState(db *bolt.DB, ctx *model.AppContext) { | |||
| err := db.View(func(tx *bolt.Tx) error { | |||
| bucket := tx.Bucket(model.World) | |||
| if bucket == nil { | |||
| return nil | |||
| } | |||
| decode := func(key string, dest interface{}) { | |||
| val := bucket.Get([]byte(key)) | |||
| if val == nil { | |||
| return | |||
| } | |||
| buf := bytes.NewBuffer(val) | |||
| if err := gob.NewDecoder(buf).Decode(dest); err != nil { | |||
| log.Fatal("decode error: ", err) | |||
| } | |||
| } | |||
| decode("beaconsList", &ctx.Beacons.Beacons) | |||
| decode("buttonsList", &ctx.ButtonsList) | |||
| decode("settings", &ctx.Settings) | |||
| return nil | |||
| }) | |||
| if err != nil { | |||
| log.Fatal(err) | |||
| } | |||
| } | |||
| @@ -0,0 +1,50 @@ | |||
| package persistence | |||
| import ( | |||
| "bytes" | |||
| "encoding/gob" | |||
| "fmt" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/boltdb/bolt" | |||
| ) | |||
| func PersistBeacons(beacons *model.BeaconsList) error { | |||
| buf := &bytes.Buffer{} | |||
| enc := gob.NewEncoder(buf) | |||
| if err := enc.Encode(beacons.Beacons); err != nil { | |||
| fmt.Println("error in encoding: ", err) | |||
| return err | |||
| } | |||
| key := []byte("beacons_list") | |||
| err := model.Db.Update(func(tx *bolt.Tx) error { | |||
| bucket, err := tx.CreateBucketIfNotExists(model.World) | |||
| if err != nil { | |||
| fmt.Println("error in creating a bucket") | |||
| return err | |||
| } | |||
| return bucket.Put(key, buf.Bytes()) | |||
| }) | |||
| return err | |||
| } | |||
| func PersistSettings(settings *model.Settings) error { | |||
| buf := &bytes.Buffer{} | |||
| enc := gob.NewEncoder(buf) | |||
| if err := enc.Encode(settings); err != nil { | |||
| return err | |||
| } | |||
| key := []byte("settings") | |||
| err := model.Db.Update(func(tx *bolt.Tx) error { | |||
| bucket, err := tx.CreateBucketIfNotExists(model.World) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return bucket.Put(key, buf.Bytes()) | |||
| }) | |||
| return err | |||
| } | |||
| @@ -0,0 +1,62 @@ | |||
| package presenseredis | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "fmt" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/redis/go-redis/v9" | |||
| ) | |||
| func LoadBeaconsList(client *redis.Client, ctx context.Context) map[string]model.Beacon { | |||
| beaconsList, err := client.Get(ctx, "beaconsList").Result() | |||
| beaconsMap := make(map[string]model.Beacon) | |||
| if err == redis.Nil { | |||
| fmt.Println("no beacons list, starting empty") | |||
| } else if err != nil { | |||
| fmt.Println("no connection to redis") | |||
| } else { | |||
| json.Unmarshal([]byte(beaconsList), &beaconsMap) | |||
| } | |||
| return beaconsMap | |||
| } | |||
| func LoadLatestList(client *redis.Client, ctx context.Context) map[string]model.Beacon { | |||
| latestList, err := client.Get(ctx, "latestList").Result() | |||
| latestMap := make(map[string]model.Beacon) | |||
| if err == redis.Nil { | |||
| fmt.Println("no beacons list, starting empty") | |||
| } else if err != nil { | |||
| fmt.Println("no connection to redis") | |||
| } else { | |||
| json.Unmarshal([]byte(latestList), &latestMap) | |||
| } | |||
| return latestMap | |||
| } | |||
| func SaveBeaconsList(appCtx *model.AppContext, client *redis.Client, ctx context.Context) { | |||
| appCtx.Beacons.Lock.Lock() | |||
| data, _ := json.Marshal(appCtx.Beacons.Beacons) | |||
| appCtx.Beacons.Lock.Unlock() | |||
| err := client.Set(ctx, "beaconsList", data, 0).Err() | |||
| if err != nil { | |||
| fmt.Println("error in saving to redis: ", err) | |||
| } | |||
| } | |||
| func SaveLatestList(appCtx *model.AppContext, client *redis.Client, ctx context.Context) { | |||
| appCtx.LatestList.Lock.Lock() | |||
| data, _ := json.Marshal(appCtx.LatestList.LatestList) | |||
| appCtx.LatestList.Lock.Unlock() | |||
| err := client.Set(ctx, "latestList", data, 0).Err() | |||
| if err != nil { | |||
| fmt.Println("error in saving to redis: ", err) | |||
| } | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| internal/ | |||
| │ | |||
| ├── pkg/ | |||
| │ ├── model/ # All data types, structs, constants | |||
| │ │ ├── beacons.go | |||
| │ │ ├── settings.go | |||
| │ │ ├── context.go # AppContext with locks and maps | |||
| │ │ └── types.go | |||
| │ │ | |||
| │ ├── httpserver/ # HTTP + WebSocket handlers | |||
| │ │ ├── routes.go # Registers all endpoints | |||
| │ │ ├── handlers.go # Core REST handlers | |||
| │ │ ├── websocket.go # WS logic (connections, broadcast) | |||
| │ │ └── server.go # StartHTTPServer() | |||
| │ │ | |||
| │ ├── mqtt/ # MQTT-specific logic | |||
| │ │ ├── processor.go # IncomingMQTTProcessor + helpers | |||
| │ │ ├── publisher.go # sendHARoomMessage, sendButtonMessage | |||
| │ │ └── filters.go # incomingBeaconFilter, distance helpers | |||
| │ │ | |||
| │ ├── persistence/ # BoltDB helpers | |||
| │ │ ├── load.go # LoadState, SaveState | |||
| │ │ ├── buckets.go # createBucketIfNotExists | |||
| │ │ └── persist_beacons.go | |||
| │ │ | |||
| │ ├── utils/ # Small utility helpers (time, logging, etc.) | |||
| │ │ ├── time.go | |||
| │ │ ├── logging.go | |||
| │ │ └── shell.go | |||
| │ │ | |||
| │ └── config/ # Default values, env vars, flags | |||
| │ └── config.go | |||
| │ | |||
| └── test/ | |||
| ├── httpserver_test/ | |||
| │ └── beacons_test.go | |||
| ├── mqtt_test/ | |||
| │ └── processor_test.go | |||
| └── persistence_test/ | |||
| └── load_test.go | |||
| @@ -0,0 +1,112 @@ | |||
| # `/pkg` | |||
| Library code that's ok to use by external applications (e.g., `/pkg/mypubliclib`). Other projects will import these libraries expecting them to work, so think twice before you put something here :-) Note that the `internal` directory is a better way to ensure your private packages are not importable because it's enforced by Go. The `/pkg` directory is still a good way to explicitly communicate that the code in that directory is safe for use by others. The [`I'll take pkg over internal`](https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/) blog post by Travis Jeffery provides a good overview of the `pkg` and `internal` directories and when it might make sense to use them. | |||
| It's also a way to group Go code in one place when your root directory contains lots of non-Go components and directories making it easier to run various Go tools (as mentioned in these talks: [`Best Practices for Industrial Programming`](https://www.youtube.com/watch?v=PTE4VJIdHPg) from GopherCon EU 2018, [GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps](https://www.youtube.com/watch?v=oL6JBUk6tj0) and [GoLab 2018 - Massimiliano Pippi - Project layout patterns in Go](https://www.youtube.com/watch?v=3gQa1LWwuzk)). | |||
| Note that this is not a universally accepted pattern and for every popular repo that uses it you can find 10 that don't. It's up to you to decide if you want to use this pattern or not. Regardless of whether or not it's a good pattern more people will know what you mean than not. It might a bit confusing for some of the new Go devs, but it's a pretty simple confusion to resolve and that's one of the goals for this project layout repo. | |||
| Ok not to use it if your app project is really small and where an extra level of nesting doesn't add much value (unless you really want to). Think about it when it's getting big enough and your root directory gets pretty busy (especially if you have a lot of non-Go app components). | |||
| The `pkg` directory origins: The old Go source code used to use `pkg` for its packages and then various Go projects in the community started copying the pattern (see [`this`](https://twitter.com/bradfitz/status/1039512487538970624) Brad Fitzpatrick's tweet for more context). | |||
| Examples: | |||
| * https://github.com/containerd/containerd/tree/main/pkg | |||
| * https://github.com/slimtoolkit/slim/tree/master/pkg | |||
| * https://github.com/telepresenceio/telepresence/tree/release/v2/pkg | |||
| * https://github.com/jaegertracing/jaeger/tree/master/pkg | |||
| * https://github.com/istio/istio/tree/master/pkg | |||
| * https://github.com/GoogleContainerTools/kaniko/tree/master/pkg | |||
| * https://github.com/google/gvisor/tree/master/pkg | |||
| * https://github.com/google/syzkaller/tree/master/pkg | |||
| * https://github.com/perkeep/perkeep/tree/master/pkg | |||
| * https://github.com/heptio/ark/tree/master/pkg | |||
| * https://github.com/argoproj/argo-workflows/tree/master/pkg | |||
| * https://github.com/argoproj/argo-cd/tree/master/pkg | |||
| * https://github.com/heptio/sonobuoy/tree/master/pkg | |||
| * https://github.com/helm/helm/tree/master/pkg | |||
| * https://github.com/k3s-io/k3s/tree/master/pkg | |||
| * https://github.com/kubernetes/kubernetes/tree/master/pkg | |||
| * https://github.com/kubernetes/kops/tree/master/pkg | |||
| * https://github.com/moby/moby/tree/master/pkg | |||
| * https://github.com/grafana/grafana/tree/master/pkg | |||
| * https://github.com/influxdata/influxdb/tree/master/pkg | |||
| * https://github.com/cockroachdb/cockroach/tree/master/pkg | |||
| * https://github.com/derekparker/delve/tree/master/pkg | |||
| * https://github.com/etcd-io/etcd/tree/master/pkg | |||
| * https://github.com/oklog/oklog/tree/master/pkg | |||
| * https://github.com/flynn/flynn/tree/master/pkg | |||
| * https://github.com/jesseduffield/lazygit/tree/master/pkg | |||
| * https://github.com/gopasspw/gopass/tree/master/pkg | |||
| * https://github.com/sosedoff/pgweb/tree/master/pkg | |||
| * https://github.com/GoogleContainerTools/skaffold/tree/master/pkg | |||
| * https://github.com/knative/serving/tree/master/pkg | |||
| * https://github.com/grafana/loki/tree/master/pkg | |||
| * https://github.com/bloomberg/goldpinger/tree/master/pkg | |||
| * https://github.com/Ne0nd0g/merlin/tree/master/pkg | |||
| * https://github.com/jenkins-x/jx/tree/master/pkg | |||
| * https://github.com/DataDog/datadog-agent/tree/master/pkg | |||
| * https://github.com/dapr/dapr/tree/master/pkg | |||
| * https://github.com/cortexproject/cortex/tree/master/pkg | |||
| * https://github.com/dexidp/dex/tree/master/pkg | |||
| * https://github.com/pusher/oauth2_proxy/tree/master/pkg | |||
| * https://github.com/pdfcpu/pdfcpu/tree/master/pkg | |||
| * https://github.com/weaveworks/kured/tree/master/pkg | |||
| * https://github.com/weaveworks/footloose/tree/master/pkg | |||
| * https://github.com/weaveworks/ignite/tree/master/pkg | |||
| * https://github.com/tmrts/boilr/tree/master/pkg | |||
| * https://github.com/kata-containers/runtime/tree/master/pkg | |||
| * https://github.com/okteto/okteto/tree/master/pkg | |||
| * https://github.com/solo-io/squash/tree/master/pkg | |||
| * https://github.com/google/exposure-notifications-server/tree/main/pkg | |||
| * https://github.com/spiffe/spire/tree/main/pkg | |||
| * https://github.com/rook/rook/tree/master/pkg | |||
| * https://github.com/buildpacks/pack/tree/main/pkg | |||
| * https://github.com/cilium/cilium/tree/main/pkg | |||
| * https://github.com/containernetworking/cni/tree/main/pkg | |||
| * https://github.com/crossplane/crossplane/tree/master/pkg | |||
| * https://github.com/dragonflyoss/Dragonfly2/tree/main/pkg | |||
| * https://github.com/kubeedge/kubeedge/tree/master/pkg | |||
| * https://github.com/kubevela/kubevela/tree/master/pkg | |||
| * https://github.com/kubevirt/kubevirt/tree/main/pkg | |||
| * https://github.com/kyverno/kyverno/tree/main/pkg | |||
| * https://github.com/thanos-io/thanos/tree/main/pkg | |||
| * https://github.com/cri-o/cri-o/tree/main/pkg | |||
| * https://github.com/fluxcd/flux2/tree/main/pkg | |||
| * https://github.com/kedacore/keda/tree/main/pkg | |||
| * https://github.com/linkerd/linkerd2/tree/main/pkg | |||
| * https://github.com/opencost/opencost/tree/develop/pkg | |||
| * https://github.com/antrea-io/antrea/tree/main/pkg | |||
| * https://github.com/karmada-io/karmada/tree/master/pkg | |||
| * https://github.com/kuberhealthy/kuberhealthy/tree/master/pkg | |||
| * https://github.com/submariner-io/submariner/tree/devel/pkg | |||
| * https://github.com/trickstercache/trickster/tree/main/pkg | |||
| * https://github.com/tellerops/teller/tree/master/pkg | |||
| * https://github.com/OpenFunction/OpenFunction/tree/main/pkg | |||
| * https://github.com/external-secrets/external-secrets/tree/main/pkg | |||
| * https://github.com/ko-build/ko/tree/main/pkg | |||
| * https://github.com/lima-vm/lima/tree/master/pkg | |||
| * https://github.com/clastix/capsule/tree/master/pkg | |||
| * https://github.com/carvel-dev/ytt/tree/develop/pkg | |||
| * https://github.com/clusternet/clusternet/tree/main/pkg | |||
| * https://github.com/fluid-cloudnative/fluid/tree/master/pkg | |||
| * https://github.com/inspektor-gadget/inspektor-gadget/tree/main/pkg | |||
| * https://github.com/sustainable-computing-io/kepler/tree/main/pkg | |||
| * https://github.com/GoogleContainerTools/kpt/tree/main/pkg | |||
| * https://github.com/guacsec/guac/tree/main/pkg | |||
| * https://github.com/kubeovn/kube-ovn/tree/master/pkg | |||
| * https://github.com/kube-vip/kube-vip/tree/main/pkg | |||
| * https://github.com/kubescape/kubescape/tree/master/pkg | |||
| * https://github.com/kudobuilder/kudo/tree/main/pkg | |||
| * https://github.com/kumahq/kuma/tree/master/pkg | |||
| * https://github.com/kubereboot/kured/tree/main/pkg | |||
| * https://github.com/nocalhost/nocalhost/tree/main/pkg | |||
| * https://github.com/openelb/openelb/tree/master/pkg | |||
| * https://github.com/openfga/openfga/tree/main/pkg | |||
| * https://github.com/openyurtio/openyurt/tree/master/pkg | |||
| * https://github.com/getporter/porter/tree/main/pkg | |||
| * https://github.com/sealerio/sealer/tree/main/pkg | |||
| * https://github.com/werf/werf/tree/main/pkg | |||
| @@ -0,0 +1,28 @@ | |||
| [Unit] | |||
| Description=Presense | |||
| PartOf=podman.service | |||
| Wants=network-online.target podman-conf-login.service | |||
| After=podman.service network-online.target podman-conf-login.service | |||
| StartLimitIntervalSec=0 | |||
| [Container] | |||
| Image=presense-go:latest | |||
| ContainerName=presense | |||
| PodmanArgs=-a stdout -a stderr | |||
| Network=sandbox.network | |||
| PublishPort=127.0.0.1:1902:8080 | |||
| Environment=HTTP_HOST_PATH=0.0.0.0:8080 | |||
| Environment=HTTPWS_HOST_PATH=0.0.0.0:8088 | |||
| Environment=MQTT_HOST=emqx:1883 | |||
| Environment=MQTT_USERNAME=sandbox | |||
| Environment=MQTT_PASSWORD=sandbox2025 | |||
| Environment=MQTT_CLIENT_ID=presence-detector | |||
| Environment=DB_PATH=.presence.db | |||
| [Service] | |||
| Restart=always | |||
| TimeoutStartSec=infinity | |||
| RestartSec=5 | |||
| [Install] | |||
| WantedBy=multi-user.target podman.service | |||
| @@ -0,0 +1,11 @@ | |||
| # `/scripts` | |||
| Scripts to perform various build, install, analysis, etc operations. | |||
| These scripts keep the root level Makefile small and simple. | |||
| Examples: | |||
| * https://github.com/kubernetes/helm/tree/master/scripts | |||
| * https://github.com/cockroachdb/cockroach/tree/master/scripts | |||
| * https://github.com/hashicorp/terraform/tree/master/scripts | |||
| @@ -0,0 +1,41 @@ | |||
| #!/bin/bash | |||
| URL="http://127.0.0.1:1902/api/beacons" | |||
| BEACON_ID="C3000057B9F7" | |||
| echo "POST (create)" | |||
| curl -s -X POST $URL \ | |||
| -H "Content-Type: application/json" \ | |||
| -d '{"Beacon_id":"'"$BEACON_ID"'","Name":"Beacon1","tx_power":-59,"rssi":-70}' | |||
| echo -e "\n" | |||
| sleep 1 | |||
| echo "GET (list after create)" | |||
| curl -s -X GET $URL | |||
| echo -e "\n" | |||
| sleep 1 | |||
| echo "PUT (update)" | |||
| curl -s -X PUT $URL \ | |||
| -H "Content-Type: application/json" \ | |||
| -d '{"Beacon_id":"'"$BEACON_ID"'","Name":"Beacon1-updated","tx_power":-60}' | |||
| echo -e "\n" | |||
| sleep 1 | |||
| echo "GET (list after update)" | |||
| curl -s -X GET $URL | |||
| echo -e "\n" | |||
| sleep 1 | |||
| echo "DELETE" | |||
| curl -s -X DELETE "$URL/$BEACON_ID" | |||
| echo -e "\n" | |||
| sleep 1 | |||
| echo "GET (list after delete)" | |||
| curl -s -X GET $URL | |||
| echo -e "\n" | |||
| @@ -0,0 +1,9 @@ | |||
| # `/test` | |||
| Additional external test apps and test data. Feel free to structure the `/test` directory anyway you want. For bigger projects it makes sense to have a data subdirectory. For example, you can have `/test/data` or `/test/testdata` if you need Go to ignore what's in that directory. Note that Go will also ignore directories or files that begin with "." or "_", so you have more flexibility in terms of how you name your test data directory. | |||
| Examples: | |||
| * https://github.com/openshift/origin/tree/master/test (test data is in the `/testdata` subdirectory) | |||
| @@ -0,0 +1,160 @@ | |||
| package httpservertest_test | |||
| import ( | |||
| "bytes" | |||
| "encoding/json" | |||
| "fmt" | |||
| "net/http" | |||
| "net/http/httptest" | |||
| "os" | |||
| "sync" | |||
| "testing" | |||
| "github.com/AFASystems/presence/internal/pkg/httpserver" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/boltdb/bolt" | |||
| "github.com/gorilla/mux" | |||
| ) | |||
| // Functions beaconsAddHandler, beaconsListHandler, beaconsDeleteHandler | |||
| func TestBeaconCRUD(t *testing.T) { | |||
| tmpfile, _ := os.CreateTemp("", "testdb-*.db") | |||
| defer os.Remove(tmpfile.Name()) | |||
| db, err := bolt.Open(tmpfile.Name(), 0600, nil) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| model.Db = db | |||
| ctx := model.AppContext{ | |||
| Beacons: model.BeaconsList{ | |||
| Beacons: make(map[string]model.Beacon), | |||
| Lock: sync.RWMutex{}, | |||
| }, | |||
| ButtonsList: make(map[string]model.Button), | |||
| } | |||
| b := model.Beacon{Name: "B1", Beacon_id: "1"} | |||
| body, err := json.Marshal(b) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| req := httptest.NewRequest("POST", "/api/beacons", bytes.NewReader(body)) | |||
| w := httptest.NewRecorder() | |||
| httpserver.BeaconsAddHandler(&ctx.Beacons)(w, req) | |||
| if w.Code != http.StatusOK { | |||
| t.Fatalf("create failed: %d", w.Code) | |||
| } | |||
| fmt.Println("--------------------------------------------------------------") | |||
| req = httptest.NewRequest("GET", "/api/beacons", nil) | |||
| w = httptest.NewRecorder() | |||
| httpserver.BeaconsListHandler(&ctx.Beacons)(w, req) | |||
| fmt.Println("Status:", w.Code) | |||
| fmt.Println("Body:", w.Body.String()) | |||
| fmt.Println("--------------------------------------------------------------") | |||
| newB := model.Beacon{Name: "B2", Beacon_id: "2"} | |||
| newBody, err := json.Marshal(newB) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| req = httptest.NewRequest("PUT", "/api/beacons", bytes.NewReader(newBody)) | |||
| w = httptest.NewRecorder() | |||
| httpserver.BeaconsAddHandler(&ctx.Beacons)(w, req) | |||
| if w.Code != http.StatusOK { | |||
| t.Fatalf("create failed: %d", w.Code) | |||
| } | |||
| req = httptest.NewRequest("GET", "/api/beacons", nil) | |||
| w = httptest.NewRecorder() | |||
| httpserver.BeaconsListHandler(&ctx.Beacons)(w, req) | |||
| fmt.Println("Status:", w.Code) | |||
| fmt.Println("Body:", w.Body.String()) | |||
| fmt.Println("--------------------------------------------------------------") | |||
| req = httptest.NewRequest("DELETE", "/api/beacons/1", nil) | |||
| req = mux.SetURLVars(req, map[string]string{"beacon_id": "1"}) | |||
| w = httptest.NewRecorder() | |||
| httpserver.BeaconsDeleteHandler(&ctx.Beacons, ctx.ButtonsList)(w, req) | |||
| fmt.Println("Status: ", w.Code) | |||
| fmt.Println("--------------------------------------------------------------") | |||
| req = httptest.NewRequest("GET", "/api/beacons", nil) | |||
| w = httptest.NewRecorder() | |||
| httpserver.BeaconsListHandler(&ctx.Beacons)(w, req) | |||
| fmt.Println("Status:", w.Code) | |||
| fmt.Println("Body:", w.Body.String()) | |||
| fmt.Println("--------------------------------------------------------------") | |||
| } | |||
| func TestSettingsCRUD(t *testing.T) { | |||
| tmpfile, _ := os.CreateTemp("", "testdb-*.db") | |||
| defer os.Remove(tmpfile.Name()) | |||
| db, err := bolt.Open(tmpfile.Name(), 0600, nil) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| model.Db = db | |||
| ctx := model.AppContext{ | |||
| Settings: model.Settings{}, | |||
| } | |||
| settings := model.Settings{ | |||
| Location_confidence: 10, | |||
| Last_seen_threshold: 10, | |||
| Beacon_metrics_size: 10, | |||
| HA_send_interval: 10, | |||
| HA_send_changes_only: true, | |||
| } | |||
| body, err := json.Marshal(settings) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| req := httptest.NewRequest("POST", "/api/settings", bytes.NewReader(body)) | |||
| w := httptest.NewRecorder() | |||
| httpserver.SettingsEditHandler(&ctx.Settings)(w, req) | |||
| fmt.Println("status: ", w.Code) | |||
| if w.Code != http.StatusOK { | |||
| t.Fatalf("create failed: %d", w.Code) | |||
| } | |||
| fmt.Println("--------------------------------------------------------------") | |||
| req = httptest.NewRequest("GET", "/api/settings", nil) | |||
| w = httptest.NewRecorder() | |||
| httpserver.SettingsListHandler(&ctx.Settings)(w, req) | |||
| fmt.Println("Status:", w.Code) | |||
| fmt.Println("Body:", w.Body.String()) | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| package mqtt_test | |||
| import ( | |||
| "os" | |||
| "testing" | |||
| "time" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/AFASystems/presence/internal/pkg/mqttclient" | |||
| "github.com/AFASystems/presence/internal/pkg/persistence" | |||
| "github.com/boltdb/bolt" | |||
| ) | |||
| func TestIncomingMQTTProcessor(t *testing.T) { | |||
| ctx := &model.AppContext{ | |||
| Beacons: model.BeaconsList{Beacons: make(map[string]model.Beacon)}, | |||
| Settings: model.Settings{ | |||
| Last_seen_threshold: 10, | |||
| Location_confidence: 3, | |||
| }, | |||
| } | |||
| tmpfile, _ := os.CreateTemp("", "testdb-*.db") | |||
| defer os.Remove(tmpfile.Name()) | |||
| db, err := bolt.Open(tmpfile.Name(), 0600, nil) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| model.Db = db | |||
| persistence.LoadState(model.Db, ctx) | |||
| ch := mqttclient.IncomingMQTTProcessor(20*time.Millisecond, nil, model.Db, ctx) | |||
| msg := model.Incoming_json{MAC: "15:02:31", Hostname: "testHost", RSSI: -55} | |||
| ch <- msg | |||
| time.Sleep(100 * time.Millisecond) | |||
| ctx.Beacons.Lock.RLock() | |||
| defer ctx.Beacons.Lock.RUnlock() | |||
| if len(ctx.LatestList.LatestList) == 0 { | |||
| t.Fatal("latest list map to update") | |||
| } | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| # `/third_party` | |||
| External helper tools, forked code and other 3rd party utilities (e.g., Swagger UI). | |||
| @@ -0,0 +1,9 @@ | |||
| # `/tools` | |||
| Supporting tools for this project. Note that these tools can import code from the `/pkg` and `/internal` directories. | |||
| Examples: | |||
| * https://github.com/istio/istio/tree/master/tools | |||
| * https://github.com/openshift/origin/tree/master/tools | |||
| * https://github.com/dapr/dapr/tree/master/tools | |||
| @@ -0,0 +1,3 @@ | |||
| # `/web` | |||
| Web application specific components: static web assets, server side templates and SPAs. | |||
| @@ -0,0 +1,8 @@ | |||
| # `/website` | |||
| This is the place to put your project's website data if you are not using GitHub pages. | |||
| Examples: | |||
| * https://github.com/hashicorp/vault/tree/master/website | |||
| * https://github.com/perkeep/perkeep/tree/master/website | |||