Compare commits

...

No commits in common. 'master' and 'chore/proposed-structure' have entirely different histories.

77 changed files with 14793 additions and 3 deletions
Split View
  1. +22
    -0
      .editorconfig
  2. +1
    -0
      .gitattributes
  3. +25
    -0
      .gitignore
  4. +1
    -0
      LICENSE.md
  5. +1
    -0
      Makefile
  6. +196
    -1
      README.md
  7. +320
    -0
      ResLevis-Diagram-2.0.drawio
  8. +354
    -0
      ResLevis-Diagram.drawio
  9. +8
    -0
      api/README.md
  10. +3
    -0
      assets/README.md
  11. +0
    -1
      backend/README.md
  12. +11
    -0
      build/README.md
  13. +0
    -0
      build/ci/.keep
  14. +84
    -0
      build/docker-compose.yml
  15. +0
    -0
      build/package/.keep
  16. +16
    -0
      build/package/Dockerfile
  17. +17
    -0
      build/package/Dockerfile.bridge
  18. +17
    -0
      build/package/Dockerfile.decoder
  19. +18
    -0
      build/package/Dockerfile.server
  20. +19
    -0
      cmd/README.md
  21. +57
    -0
      cmd/bridge/main.go
  22. +206
    -0
      cmd/decoder/main.go
  23. +71
    -0
      cmd/location/main.go
  24. +0
    -0
      cmd/presenSe/.keep
  25. +188
    -0
      cmd/presenSe/presense.go
  26. +127
    -0
      cmd/server/main.go
  27. +5
    -0
      configs/README.md
  28. +1389
    -0
      copy_files/_main.go
  29. +3
    -0
      deployments/README.md
  30. +9
    -0
      docs/README.md
  31. +9
    -0
      examples/README.md
  32. +0
    -1
      frontend/README.md
  33. +3
    -0
      githooks/README.md
  34. +24
    -0
      go.mod
  35. +48
    -0
      go.sum
  36. +3
    -0
      init/README.md
  37. +21
    -0
      internal/README.md
  38. +0
    -0
      internal/app/_your_app_/.keep
  39. +105
    -0
      internal/pkg/bridge/mqtthandler/mqtthandler.go
  40. +37
    -0
      internal/pkg/config/config.go
  41. +371
    -0
      internal/pkg/httpserver/server.go
  42. +3
    -0
      internal/pkg/httpserver/server.md
  43. +27
    -0
      internal/pkg/kafka/consumer.go
  44. +18
    -0
      internal/pkg/kafka/reader.go
  45. +17
    -0
      internal/pkg/kafka/writer.go
  46. +0
    -0
      internal/pkg/model/.keep
  47. +3
    -0
      internal/pkg/model/model.md
  48. +207
    -0
      internal/pkg/model/types.go
  49. +128
    -0
      internal/pkg/mqttclient/beacon.go
  50. +35
    -0
      internal/pkg/mqttclient/fillter.go
  51. +165
    -0
      internal/pkg/mqttclient/location.go
  52. +62
    -0
      internal/pkg/mqttclient/processor.go
  53. +18
    -0
      internal/pkg/persistence/buckets.go
  54. +39
    -0
      internal/pkg/persistence/load.go
  55. +50
    -0
      internal/pkg/persistence/persist.go
  56. +62
    -0
      internal/pkg/redis/redis.go
  57. +40
    -0
      internal/structure.md
  58. BIN
      log_files/device_positions_x-y.xlsx
  59. +9600
    -0
      log_files/mqtt_20250724_1700 copy.log
  60. +100
    -0
      log_files/mqtt_20250724_1700.log
  61. BIN
      log_files/mqtt_20250724_1700.log.bz2
  62. +112
    -0
      pkg/README.md
  63. +0
    -0
      pkg/_your_public_lib_/.keep
  64. BIN
      presence.db
  65. +28
    -0
      presense.container
  66. +11
    -0
      scripts/README.md
  67. +41
    -0
      scripts/testAPI.sh
  68. +9
    -0
      test/README.md
  69. +160
    -0
      test/httpserver_test/httpserver_test.go
  70. +46
    -0
      test/mqtt_test/mqtt_test.go
  71. +3
    -0
      third_party/README.md
  72. +9
    -0
      tools/README.md
  73. +3
    -0
      web/README.md
  74. +0
    -0
      web/app/.keep
  75. +0
    -0
      web/static/.keep
  76. +0
    -0
      web/template/.keep
  77. +8
    -0
      website/README.md

+ 22
- 0
.editorconfig View File

@@ -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

+ 1
- 0
.gitattributes View File

@@ -0,0 +1 @@
* -text

+ 25
- 0
.gitignore View File

@@ -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/

+ 1
- 0
LICENSE.md View File

@@ -0,0 +1 @@
Hacker license!

+ 1
- 0
Makefile View File

@@ -0,0 +1 @@
# note: call scripts from /scripts

+ 196
- 1
README.md View File

@@ -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.

[![Go Report Card](https://goreportcard.com/badge/github.com/golang-standards/project-layout?style=flat-square)](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.~~

[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](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).

[![PkgGoDev](https://pkg.go.dev/badge/github.com/golang-standards/project-layout)](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.

[![Release](https://img.shields.io/github/release/golang-standards/project-layout.svg?style=flat-square)](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.

+ 320
- 0
ResLevis-Diagram-2.0.drawio View File

@@ -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&lt;br&gt;(FastAPI)&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;+&lt;br&gt;&lt;br&gt;Logic Connector&lt;/div&gt;" 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&lt;br&gt;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&lt;br&gt;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&lt;br&gt;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&lt;br&gt;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&lt;br&gt;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&amp;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&lt;br&gt;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="&lt;br&gt;Tracker&lt;br&gt;LOCALIZATION&lt;br&gt;(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&lt;br&gt;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&amp;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="&lt;br&gt;Tracker Sensors:&lt;br&gt;Alarm, Temp, &lt;br&gt;battery level&lt;br&gt;...&lt;br&gt;Heartbest&lt;br&gt;(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&lt;br&gt;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="&lt;b&gt;&lt;font style=&quot;font-size: 27px;&quot;&gt;ResLevis DIAGRAM&lt;/font&gt;&lt;/b&gt;" 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="&lt;b style=&quot;&quot;&gt;&lt;font style=&quot;font-size: 13px;&quot;&gt;Ver 2.0 2025/10/09&lt;/font&gt;&lt;/b&gt;" 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="&lt;br&gt;GW &lt;br&gt;and Tracker definition&lt;br&gt;model&lt;br&gt;supported&lt;br&gt;Decode-lib&lt;br&gt;Raw-Data&lt;div&gt;&lt;br&gt;&lt;/div&gt;" 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&lt;br&gt;Subscribe&lt;br&gt;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&lt;br&gt;Publish&lt;br&gt;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="&lt;b&gt;Developed by SenLab&lt;/b&gt;" 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="&lt;b&gt;Developed by AFA Systems&lt;/b&gt;" 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="&lt;b&gt;Developed Maestry&amp;nbsp;&lt;/b&gt;" 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>

+ 354
- 0
ResLevis-Diagram.drawio View File

@@ -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&lt;br&gt;(FastAPI)&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;+&lt;br&gt;&lt;br&gt;Logic Connector&lt;/div&gt;" 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&lt;br&gt;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&lt;br&gt;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&lt;br&gt;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&lt;br&gt;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&lt;br&gt;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&amp;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&lt;br&gt;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="&lt;br&gt;Tracker&lt;br&gt;LOCALIZATION&lt;br&gt;(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&lt;br&gt;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&amp;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="&lt;br&gt;Tracker Sensors:&lt;br&gt;Alarm, Temp, &lt;br&gt;battery level&lt;br&gt;...&lt;br&gt;Heartbest&lt;br&gt;(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="&lt;b&gt;&lt;font style=&quot;font-size: 27px;&quot;&gt;ResLevis DIAGRAM&lt;/font&gt;&lt;/b&gt;" 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="&lt;b style=&quot;&quot;&gt;&lt;font style=&quot;font-size: 13px;&quot;&gt;Ver 2.0 2025/10/09&lt;/font&gt;&lt;/b&gt;" 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&lt;br&gt;Subscribe&lt;br&gt;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&lt;br&gt;Publish&lt;br&gt;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="&lt;b&gt;Developed by SenLab&lt;/b&gt;" 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="&lt;b&gt;Developed by AFA Systems&lt;/b&gt;" 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="&lt;b&gt;Developed Maestry&amp;nbsp;&lt;/b&gt;" 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="&lt;br&gt;GW &lt;br&gt;and Tracker definition&lt;br&gt;model&lt;br&gt;supported&lt;br&gt;Decode-lib&lt;br&gt;Raw-Data&lt;div&gt;&lt;br&gt;&lt;/div&gt;" 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>

+ 8
- 0
api/README.md View File

@@ -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

+ 3
- 0
assets/README.md View File

@@ -0,0 +1,3 @@
# `/assets`

Other assets to go along with your repository (images, logos, etc).

+ 0
- 1
backend/README.md View File

@@ -1 +0,0 @@
This folder is dedicated to backend sourcecode

+ 11
- 0
build/README.md View File

@@ -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
build/ci/.keep View File


+ 84
- 0
build/docker-compose.yml View File

@@ -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
build/package/.keep View File


+ 16
- 0
build/package/Dockerfile View File

@@ -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"]

+ 17
- 0
build/package/Dockerfile.bridge View File

@@ -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"]

+ 17
- 0
build/package/Dockerfile.decoder View File

@@ -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"]

+ 18
- 0
build/package/Dockerfile.server View File

@@ -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"]

+ 19
- 0
cmd/README.md View File

@@ -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

+ 57
- 0
cmd/bridge/main.go View File

@@ -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 {}
}

+ 206
- 0
cmd/decoder/main.go View File

@@ -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
}

+ 71
- 0
cmd/location/main.go View File

@@ -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
cmd/presenSe/.keep View File


+ 188
- 0
cmd/presenSe/presense.go View File

@@ -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
}

+ 127
- 0
cmd/server/main.go View File

@@ -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))
}
}
}

+ 5
- 0
configs/README.md View File

@@ -0,0 +1,5 @@
# `/configs`

Configuration file templates or default configs.

Put your `confd` or `consul-template` template files here.

+ 1389
- 0
copy_files/_main.go
File diff suppressed because it is too large
View File


+ 3
- 0
deployments/README.md View File

@@ -0,0 +1,3 @@
# `/deployments`

IaaS, PaaS, system and container orchestration deployment configurations and templates (docker-compose, kubernetes/helm, mesos, terraform, bosh).

+ 9
- 0
docs/README.md View File

@@ -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

+ 9
- 0
examples/README.md View File

@@ -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

+ 0
- 1
frontend/README.md View File

@@ -1 +0,0 @@
This folder is dedicated to frontend sourcecode

+ 3
- 0
githooks/README.md View File

@@ -0,0 +1,3 @@
# `/githooks`

Git hooks.

+ 24
- 0
go.mod View File

@@ -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
)

+ 48
- 0
go.sum View File

@@ -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=

+ 3
- 0
init/README.md View File

@@ -0,0 +1,3 @@
# `/init`

System init (systemd, upstart, sysv) and process manager/supervisor (runit, supervisord) configs.

+ 21
- 0
internal/README.md View File

@@ -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
internal/app/_your_app_/.keep View File


+ 105
- 0
internal/pkg/bridge/mqtthandler/mqtthandler.go View File

@@ -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
}

+ 37
- 0
internal/pkg/config/config.go View File

@@ -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"),
}
}

+ 371
- 0
internal/pkg/httpserver/server.go View File

@@ -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)
}
}
}
}

+ 3
- 0
internal/pkg/httpserver/server.md View File

@@ -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

+ 27
- 0
internal/pkg/kafka/consumer.go View File

@@ -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
}
}

+ 18
- 0
internal/pkg/kafka/reader.go View File

@@ -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,
})
}

+ 17
- 0
internal/pkg/kafka/writer.go View File

@@ -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
internal/pkg/model/.keep View File


+ 3
- 0
internal/pkg/model/model.md View File

@@ -0,0 +1,3 @@
# MODELS

This file includes type definitions for aggregate struct types

+ 207
- 0
internal/pkg/model/types.go View File

@@ -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

+ 128
- 0
internal/pkg/mqttclient/beacon.go View File

@@ -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()
}

+ 35
- 0
internal/pkg/mqttclient/fillter.go View File

@@ -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
}

+ 165
- 0
internal/pkg/mqttclient/location.go View File

@@ -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)
}

+ 62
- 0
internal/pkg/mqttclient/processor.go View File

@@ -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
}

+ 18
- 0
internal/pkg/persistence/buckets.go View File

@@ -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)
}
}

+ 39
- 0
internal/pkg/persistence/load.go View File

@@ -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)
}
}

+ 50
- 0
internal/pkg/persistence/persist.go View File

@@ -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
}

+ 62
- 0
internal/pkg/redis/redis.go View File

@@ -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)
}
}

+ 40
- 0
internal/structure.md View File

@@ -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

BIN
log_files/device_positions_x-y.xlsx View File


+ 9600
- 0
log_files/mqtt_20250724_1700 copy.log
File diff suppressed because it is too large
View File


+ 100
- 0
log_files/mqtt_20250724_1700.log
File diff suppressed because it is too large
View File


BIN
log_files/mqtt_20250724_1700.log.bz2 View File


+ 112
- 0
pkg/README.md View File

@@ -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
pkg/_your_public_lib_/.keep View File


BIN
presence.db View File


+ 28
- 0
presense.container View File

@@ -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

+ 11
- 0
scripts/README.md View File

@@ -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

+ 41
- 0
scripts/testAPI.sh View File

@@ -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"

+ 9
- 0
test/README.md View File

@@ -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)



+ 160
- 0
test/httpserver_test/httpserver_test.go View File

@@ -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())
}

+ 46
- 0
test/mqtt_test/mqtt_test.go View File

@@ -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")
}
}

+ 3
- 0
third_party/README.md View File

@@ -0,0 +1,3 @@
# `/third_party`

External helper tools, forked code and other 3rd party utilities (e.g., Swagger UI).

+ 9
- 0
tools/README.md View File

@@ -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

+ 3
- 0
web/README.md View File

@@ -0,0 +1,3 @@
# `/web`

Web application specific components: static web assets, server side templates and SPAs.

+ 0
- 0
web/app/.keep View File


+ 0
- 0
web/static/.keep View File


+ 0
- 0
web/template/.keep View File


+ 8
- 0
website/README.md View File

@@ -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

Loading…
Cancel
Save