소스 검색

chore: finished refactoring, without testing functionallity bar some API calls and one MQTT functionallity

chore/proposed-structure
Blaz Smehov 1 개월 전
부모
커밋
b6ef1bc1c2
16개의 변경된 파일844개의 추가작업 그리고 864개의 파일을 삭제
  1. +177
    -2
      cmd/presenSe/presense.go
  2. +0
    -40
      copy_files/_main.go
  3. +0
    -773
      copy_files/_maincopy.go
  4. +33
    -0
      internal/pkg/config/config.go
  5. +28
    -28
      internal/pkg/httpserver/server.go
  6. +29
    -20
      internal/pkg/model/types.go
  7. +128
    -0
      internal/pkg/mqtt_client/beacon.go
  8. +35
    -0
      internal/pkg/mqtt_client/fillter.go
  9. +165
    -0
      internal/pkg/mqtt_client/location.go
  10. +61
    -0
      internal/pkg/mqtt_client/processor.go
  11. +18
    -0
      internal/pkg/persistence/buckets.go
  12. +39
    -0
      internal/pkg/persistence/load.go
  13. +1
    -1
      internal/pkg/persistence/persist.go
  14. +40
    -0
      internal/structure.md
  15. +44
    -0
      test/httpserver_test/httpserver_test.go
  16. +46
    -0
      test/mqtt_test/mqtt_test.go

+ 177
- 2
cmd/presenSe/presense.go 파일 보기

@@ -1,7 +1,182 @@
package main

import "fmt"
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/mqtt_client"
"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() {
fmt.Println("this is the main file")
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, os.Kill)
cfg := config.Load()

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

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 {
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 := mqtt_client.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]

if strings.HasPrefix(msgStr, "[") {
var readings []model.RawReading
err := json.Unmarshal(message, &readings)
if err != nil {
log.Printf("Errore parsing JSON: %v", err)
return
}

for _, reading := range readings {
if reading.Type == "Gateway" {
continue
}
incoming := model.Incoming_json{
Hostname: hostname,
MAC: reading.MAC,
RSSI: int64(reading.RSSI),
Data: reading.RawData,
HB_ButtonCounter: parseButtonState(reading.RawData),
}
incomingChan <- incoming
}
} else {
s := strings.Split(string(message), ",")
if len(s) < 6 {
log.Printf("Messaggio CSV non valido: %s", msgStr)
return
}

rawdata := s[4]
buttonCounter := parseButtonState(rawdata)
if buttonCounter > 0 {
incoming := model.Incoming_json{}
i, _ := strconv.ParseInt(s[3], 10, 64)
incoming.Hostname = hostname
incoming.Beacon_type = "hb_button"
incoming.MAC = s[1]
incoming.RSSI = i
incoming.Data = rawdata
incoming.HB_ButtonCounter = buttonCounter

read_line := strings.TrimRight(string(s[5]), "\r\n")
it, err33 := strconv.Atoi(read_line)
if err33 != nil {
fmt.Println(it)
fmt.Println(err33)
os.Exit(2)
}
incomingChan <- incoming
}
}
},
},
},
})
if err != nil {
panic(err)
}

fmt.Println("CONNECTED TO MQTT")
fmt.Println("\n ")
fmt.Println("Visit http://" + cfg.HTTPAddr + " on your browser to see the web interface")
fmt.Println("\n ")

go httpserver.StartHTTPServer(cfg.HTTPAddr, ctx)

<-sigc

if err := cli.Disconnect(); err != nil {
panic(err)
}
}

func parseButtonState(raw string) int64 {
raw = strings.ToUpper(raw)

if strings.HasPrefix(raw, "0201060303E1FF12") && len(raw) >= 38 {
buttonField := raw[34:38]
if buttonValue, err := strconv.ParseInt(buttonField, 16, 64); err == nil {
return buttonValue
}
}

if strings.HasPrefix(raw, "02010612FF590") && len(raw) >= 24 {
counterField := raw[22:24]
buttonState, err := strconv.ParseInt(counterField, 16, 64)
if err == nil {
return buttonState
}
}

return 0
}

+ 0
- 40
copy_files/_main.go 파일 보기

@@ -603,27 +603,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl
if err != nil {
panic(err)
}

// Read in a new message as JSON and map it to a Message object
//err := ws.ReadJSON(&msg)

/*msg := Message{
Email: "apple",
Username: "peach",
Message: "change",}
res1B, _ := json.Marshal(msg)
fmt.Println(string(res1B))



if err != nil {
log.Printf("error: %v", err)

}
// Send the newly received message to the broadcast channel
broadcast <- msg*/

///utils.Log.Printf("%s changes ",beacon.Beacon_id)
s := fmt.Sprintf("/usr/bin/php /usr/local/presence/alarm_handler.php --idt=%s --idr=%s --loct=%s", beacon.Beacon_id, beacon.Incoming_JSON.Hostname, location)
///utils.Log.Printf("%s",s)
err, out, errout := Shellout(s)
@@ -985,25 +964,6 @@ func createUser(id string, logWanted bool) user {
func main() {

loggers := []*user{}
// initialize empty-object json file if not found
//if _, err := ioutil.ReadFile(Db); err != nil {
// str := "{}"
// if err = ioutil.WriteFile(Db, []byte(str), 0644); err != nil {
// log.Fatal(err)
// }
//}

// create channel to communicate over
//jobs := make(chan Job)

// start watching jobs channel for work
//go ProcessJobs(jobs, Db)

// create dependencies
//client := &TodoClient{Jobs: jobs}
//handlers := &TodoHandlers{Client: client}
//work := WorkRequest{Name: name, Delay: delay}
//jobs <- work

http_host_path_ptr = flag.String("http_host_path", "0.0.0.0:8080", "The host:port that the HTTP server should listen on")
//https_host_path_ptr = flag.String("https_host_path", "0.0.0.0:5443", "The host:port that the HTTP server should listen on")


+ 0
- 773
copy_files/_maincopy.go 파일 보기

@@ -1,797 +1,27 @@
package main

import (
"bytes"
"encoding/gob"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"math"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"time"

//"./utils"
"os/exec"

"github.com/boltdb/bolt"
"gopkg.in/natefinch/lumberjack.v2"

"github.com/yosssi/gmq/mqtt"
"github.com/yosssi/gmq/mqtt/client"

"github.com/gorilla/websocket"
)

var clients = make(map[*websocket.Conn]bool) // connected clients
var broadcast = make(chan Message)
var BEACONS Beacons_list

var Buttons_list map[string]Button

var cli *client.Client

var http_results HTTP_locations_list
var http_results_lock sync.RWMutex

var Latest_beacons_list map[string]Beacon
var latest_list_lock sync.RWMutex

var db *bolt.DB
var err error

var world = []byte("presence")

var (
// /logpath = flag.String("logpath", "/data/var/log/presence/presence.log", "Log Path")
)

var (
// Websocket http upgrader
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)

var settings = Settings{
Location_confidence: 4,
Last_seen_threshold: 15,
Beacon_metrics_size: 30,
HA_send_interval: 5,
HA_send_changes_only: false,
}

// utility function
func parseButtonState(raw string) int64 {
raw = strings.ToUpper(raw)

// Minew B7 / C7 / D7 - frame tipo: 0201060303E1FF1216E1FFA103...
if strings.HasPrefix(raw, "0201060303E1FF12") && len(raw) >= 38 {
// La posizione 34-38 (indice 26:30) contiene il buttonCounter su 2 byte (hex)
buttonField := raw[34:38] // NB: offset 34-38 zero-based
if buttonValue, err := strconv.ParseInt(buttonField, 16, 64); err == nil {
return buttonValue
}
}

// Ingics (02010612FF590)
if strings.HasPrefix(raw, "02010612FF590") && len(raw) >= 24 {
counterField := raw[22:24]
buttonState, err := strconv.ParseInt(counterField, 16, 64)
if err == nil {
return buttonState
}
}

// Aggiungeremo qui facilmente nuovi beacon in futuro

return 0
}

func twos_comp(inp string) int64 {
i, _ := strconv.ParseInt("0x"+inp, 0, 64)

return i - 256
}

func getBeaconID(incoming Incoming_json) string {
unique_id := fmt.Sprintf("%s", incoming.MAC)
/*if incoming.Beacon_type == "ibeacon" {
unique_id = fmt.Sprintf("%s_%s_%s", incoming.UUID, incoming.Major, incoming.Minor)
} else if incoming.Beacon_type == "eddystone" {
unique_id = fmt.Sprintf("%s_%s", incoming.Namespace, incoming.Instance_id)
} else if incoming.Beacon_type == "hb_button" {
unique_id = fmt.Sprintf("%s_%s", incoming.Namespace, incoming.Instance_id)
}*/
return unique_id
}

func incomingBeaconFilter(incoming Incoming_json) Incoming_json {
out_json := incoming
if incoming.Beacon_type == "hb_button" {
//do additional checks here to detect if a Habby Bubbles Button
// looks like 020104020a0011ff045600012d3859db59e1000b9453

raw_data := incoming.Data
//company_id := []byte{0x04, 0x56}
//product_id := []byte{0x00, 0x01}
hb_button_prefix_str := fmt.Sprintf("02010612FF5900")
if strings.HasPrefix(raw_data, hb_button_prefix_str) {
out_json.Namespace = "ddddeeeeeeffff5544ff"
//out_json.Instance_id = raw_data[24:36]
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])
////fmt.Println("battery has %s\n", battery_str)

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"

///fmt.Println("Button adv has %#v\n", out_json)
}
}

return out_json
}

func processButton(bbeacon Beacon, cl *client.Client) {
btn := Button{Name: bbeacon.Name}
btn.Button_id = bbeacon.Beacon_id
btn.Button_type = bbeacon.Beacon_type
btn.Button_location = bbeacon.Previous_location
btn.Incoming_JSON = bbeacon.Incoming_JSON
btn.Distance = bbeacon.Distance
btn.Last_seen = bbeacon.Last_seen
btn.HB_ButtonCounter = bbeacon.HB_ButtonCounter
btn.HB_Battery = bbeacon.HB_Battery
btn.HB_RandomNonce = bbeacon.HB_RandomNonce
btn.HB_ButtonMode = bbeacon.HB_ButtonMode

nonce, ok := Buttons_list[btn.Button_id]
if !ok || nonce.HB_RandomNonce != btn.HB_RandomNonce {
// send the button message to MQTT
sendButtonMessage(btn, cl)
}
Buttons_list[btn.Button_id] = btn
}

func getiBeaconDistance(rssi int64, power string) float64 {

ratio := float64(rssi) * (1.0 / float64(twos_comp(power)))
//fmt.Printf("beaconpower: rssi %d ratio %e power %e \n",rssi, ratio, 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 getBeaconDistance(incoming Incoming_json) float64 {
distance := 1000.0

distance = getiBeaconDistance(incoming.RSSI, incoming.TX_power)
//distance = math.Abs(float64(incoming.RSSI))

return distance
}

func getAverageDistance(beacon_metrics []beacon_metric) float64 {
total := 0.0

for _, v := range beacon_metrics {
total += v.distance
}
return (total / float64(len(beacon_metrics)))
}

func sendHARoomMessage(beacon_id string, beacon_name string, distance float64, location string, cl *client.Client) {
//first make the json
ha_msg, err := json.Marshal(HA_message{Beacon_id: beacon_id, Beacon_name: beacon_name, Distance: distance})
if err != nil {
panic(err)
}

//send the message to HA
err = cl.Publish(&client.PublishOptions{
QoS: mqtt.QoS1,
TopicName: []byte("afa-systems/presence/ha/" + location),
Message: ha_msg,
})
if err != nil {
panic(err)
}
}

func sendButtonMessage(btn Button, cl *client.Client) {
//first make the json
btn_msg, err := json.Marshal(btn)
if err != nil {
panic(err)
}

//send the message to HA
err = cl.Publish(&client.PublishOptions{
QoS: mqtt.QoS1,
TopicName: []byte("afa-systems/presence/button/" + btn.Button_id),
Message: btn_msg,
})
if err != nil {
panic(err)
}

}

func sendButtonPressed(bcn Beacon, cl *client.Client) {
//first make the json
btn_msg, err := json.Marshal(bcn)
if err != nil {
panic(err)
}

//send the message to HA
err = cl.Publish(&client.PublishOptions{
QoS: mqtt.QoS1,
TopicName: []byte("afa-systems/presence/button/" + bcn.Beacon_id),
Message: btn_msg,
})
if err != nil {
panic(err)
}
///utils.Log.Printf("%s pressed ",bcn.Beacon_id)
s := fmt.Sprintf("/usr/bin/php /usr/local/presence/alarm_handler.php --idt=%s --idr=%s --st=%d", bcn.Beacon_id, bcn.Incoming_JSON.Hostname, bcn.HB_ButtonCounter)
///utils.Log.Printf("%s",s)
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 getLikelyLocations(settings Settings, locations_list Locations_list, cl *client.Client) {
// create the http results structure
http_results_lock.Lock()
http_results = HTTP_locations_list{}
http_results.Beacons = make([]HTTP_location, 0)
///http_results.Buttons = make([]Button, 0)
http_results_lock.Unlock()

should_persist := false

// iterate through the beacons we want to search for
for _, beacon := range BEACONS.Beacons {

if len(beacon.beacon_metrics) == 0 {
////fmt.Printf("beacon_metrics = 0:\n")
continue
}

if (int64(time.Now().Unix()) - (beacon.beacon_metrics[len(beacon.beacon_metrics)-1].timestamp)) > settings.Last_seen_threshold {
////fmt.Printf("beacon_metrics timestamp = %s %s \n",beacon.Name, beacon.beacon_metrics[len(beacon.beacon_metrics)-1].timestamp )
if beacon.expired_location == "expired" {
//beacon.Location_confidence = - 1
continue
} else {
beacon.expired_location = "expired"
msg := Message{
Email: beacon.Previous_confident_location,
Username: beacon.Name,
Message: beacon.expired_location}
res1B, _ := json.Marshal(msg)
fmt.Println(string(res1B))

if err != nil {
log.Printf("error: %v", err)

}
// Send the newly received message to the broadcast channel
broadcast <- msg
}
} else {
beacon.expired_location = ""

}

best_location := Best_location{}

// go through its beacon metrics and pick out the location that appears most often
loc_list := make(map[string]float64)
seen_weight := 1.5
rssi_weight := 0.75
for _, metric := range beacon.beacon_metrics {
loc, ok := loc_list[metric.location]
if !ok {
loc = seen_weight + (rssi_weight * (1.0 - (float64(metric.rssi) / -100.0)))
} else {
loc = loc + seen_weight + (rssi_weight * (1.0 - (float64(metric.rssi) / -100.0)))
}
loc_list[metric.location] = loc
}
best_name := ""
ts := 0.0
for name, times_seen := range loc_list {
if times_seen > ts {
best_name = name
ts = times_seen
}
}
/////fmt.Printf("BEST LOCATION FOR %s IS: %s with score: %f\n", beacon.Name, best_name, ts)
best_location = Best_location{name: best_name, distance: beacon.beacon_metrics[len(beacon.beacon_metrics)-1].distance, last_seen: beacon.beacon_metrics[len(beacon.beacon_metrics)-1].timestamp}

beacon.Location_history = append(beacon.Location_history, best_location.name)
if len(beacon.Location_history) > 10 {
beacon.Location_history = beacon.Location_history[1:] // manteniamo solo gli ultimi 10
}

// Calcoliamo la location più votata nello storico
location_counts := make(map[string]int)
for _, loc := range beacon.Location_history {
location_counts[loc]++
}

max_count := 0
most_common_location := ""
for loc, count := range location_counts {
if count > max_count {
max_count = count
most_common_location = loc
}
}

// Applichiamo un filtro: consideriamo il cambio solo se almeno 7 su 10 votano per una location
if max_count >= 7 {
beacon.Previous_location = most_common_location
if most_common_location == beacon.Previous_confident_location {
beacon.Location_confidence++
} else {
beacon.Location_confidence = 1
beacon.Previous_confident_location = most_common_location
}
}

//create an http result from this
r := HTTP_location{}
r.Distance = best_location.distance
r.Name = beacon.Name
r.Beacon_name = beacon.Name
r.Beacon_id = beacon.Beacon_id
r.Beacon_type = beacon.Beacon_type
r.HB_Battery = beacon.HB_Battery
r.HB_ButtonMode = beacon.HB_ButtonMode
r.HB_ButtonCounter = beacon.HB_ButtonCounter
r.Location = best_location.name
r.Last_seen = best_location.last_seen

////fmt.Printf("beacon.Location_confidence %s, settings.Location_confidence %s, beacon.Previous_confident_location %s: best_location.name %s\n",beacon.Location_confidence, settings.Location_confidence, beacon.Previous_confident_location, best_location.name)

if (beacon.Location_confidence == settings.Location_confidence && beacon.Previous_confident_location != best_location.name) || beacon.expired_location == "expired" {
// location has changed, send an mqtt message

should_persist = true
fmt.Printf("detected a change!!! %#v\n\n", beacon)
if beacon.Previous_confident_location == "expired" && beacon.expired_location == "" {
msg := Message{
Email: beacon.Previous_confident_location,
Username: beacon.Name,
Message: "OK"}
res1B, _ := json.Marshal(msg)
fmt.Println(string(res1B))

if err != nil {
log.Printf("error: %v", err)

}
// Send the newly received message to the broadcast channel
broadcast <- msg
}
beacon.Location_confidence = 0
location := ""
if beacon.expired_location == "expired" {

location = "expired"
} else {
location = best_location.name
}
//first make the json
js, err := json.Marshal(Location_change{Beacon_ref: beacon, Name: beacon.Name, Beacon_name: beacon.Name, Previous_location: beacon.Previous_confident_location, New_location: location, Timestamp: time.Now().Unix()})
if err != nil {
continue
}

//send the message
err = cl.Publish(&client.PublishOptions{
QoS: mqtt.QoS1,
TopicName: []byte("afa-systems/presence/changes"),
Message: js,
})
if err != nil {
panic(err)
}
s := fmt.Sprintf("/usr/bin/php /usr/local/presence/alarm_handler.php --idt=%s --idr=%s --loct=%s", beacon.Beacon_id, beacon.Incoming_JSON.Hostname, location)
///utils.Log.Printf("%s",s)
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)
//////beacon.logger.Printf("Log content: user id %v \n", best_location.name)
if settings.HA_send_changes_only {
sendHARoomMessage(beacon.Beacon_id, beacon.Name, best_location.distance, best_location.name, cl)
}

if beacon.expired_location == "expired" {

beacon.Previous_confident_location = "expired"
r.Location = "expired"
} else {
beacon.Previous_confident_location = best_location.name
}
///beacon.Previous_confident_location = best_location.name

}

beacon.Previous_location = best_location.name
r.Previous_confident_location = beacon.expired_location
BEACONS.Beacons[beacon.Beacon_id] = beacon

http_results_lock.Lock()
http_results.Beacons = append(http_results.Beacons, r)
http_results_lock.Unlock()

if best_location.name != "" {
if !settings.HA_send_changes_only {
secs := int64(time.Now().Unix())
if secs%settings.HA_send_interval == 0 {
sendHARoomMessage(beacon.Beacon_id, beacon.Name, best_location.distance, best_location.name, cl)
}
}
}
err := cl.Publish(&client.PublishOptions{
QoS: mqtt.QoS0,
TopicName: []byte("afa-systems/presence"),
Message: []byte(fmt.Sprintf("%s is most likely in %s with average distance %f", beacon.Name, best_location.name, best_location.distance)),
})
if err != nil {
panic(err)
}

}

if should_persist {
persistBeacons()
}
}

func IncomingMQTTProcessor(updateInterval time.Duration, cl *client.Client, db *bolt.DB, logger []*user) chan<- Incoming_json {

incoming_msgs_chan := make(chan Incoming_json, 2000)

// load initial BEACONS
BEACONS.Beacons = make(map[string]Beacon)
// retrieve the data

// create bucket if not exist
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(world)
if err != nil {
return err
}
return nil
})

err = db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(world)
if bucket == nil {
return err
}

key := []byte("beacons_list")
val := bucket.Get(key)
if val != nil {
buf := bytes.NewBuffer(val)
dec := gob.NewDecoder(buf)
err = dec.Decode(&BEACONS)
if err != nil {
log.Fatal("decode error:", err)
}
}

key = []byte("buttons_list")
val = bucket.Get(key)
if val != nil {
buf := bytes.NewBuffer(val)
dec := gob.NewDecoder(buf)
err = dec.Decode(&Buttons_list)
if err != nil {
log.Fatal("decode error:", err)
}
}

key = []byte("settings")
val = bucket.Get(key)
if val != nil {
buf := bytes.NewBuffer(val)
dec := gob.NewDecoder(buf)
err = dec.Decode(&settings)
if err != nil {
log.Fatal("decode error:", err)
}
}

return nil
})

if err != nil {
log.Fatal(err)
}
Latest_beacons_list = make(map[string]Beacon)

Buttons_list = make(map[string]Button)

//create a map of locations, looked up by hostnames
locations_list := Locations_list{}
ls := make(map[string]Location)
locations_list.locations = ls

ticker := time.NewTicker(updateInterval)

go func() {
for {
select {

case <-ticker.C:
getLikelyLocations(settings, locations_list, cl)
case incoming := <-incoming_msgs_chan:
func() {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()

incoming = incomingBeaconFilter(incoming)
this_beacon_id := getBeaconID(incoming)

now := time.Now().Unix()

beacon, ok := BEACONS.Beacons[this_beacon_id]
if !ok {
//should be unique
//if it's already in list, forget it.
latest_list_lock.Lock()
x, ok := Latest_beacons_list[this_beacon_id]
if ok {
//update its timestamp
x.Last_seen = now
x.Incoming_JSON = incoming
x.Distance = getBeaconDistance(incoming)

Latest_beacons_list[this_beacon_id] = x
} else {
Latest_beacons_list[this_beacon_id] = Beacon{Beacon_id: this_beacon_id, Beacon_type: incoming.Beacon_type, Last_seen: now, Incoming_JSON: incoming, Beacon_location: incoming.Hostname, Distance: getBeaconDistance(incoming)}
}
for k, v := range Latest_beacons_list {
if (now - v.Last_seen) > 10 { // 10 seconds
delete(Latest_beacons_list, k)
}
}
latest_list_lock.Unlock()
//continue
return
}

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
////fmt.Println("button pressed " + this_beacon_id + " at " + strconv.Itoa(int(incoming.HB_ButtonCounter)) )

if beacon.beacon_metrics == nil {
beacon.beacon_metrics = make([]beacon_metric, settings.Beacon_metrics_size)
}
//create metric for this beacon
this_metric := beacon_metric{}
this_metric.distance = getBeaconDistance(incoming)
this_metric.timestamp = now
this_metric.rssi = int64(incoming.RSSI)
this_metric.location = incoming.Hostname
beacon.beacon_metrics = append(beacon.beacon_metrics, this_metric)
///fmt.Printf("APPENDING a metric from %s len %d\n", beacon.Name, len(beacon.beacon_metrics))
if len(beacon.beacon_metrics) > settings.Beacon_metrics_size {
//fmt.Printf("deleting a metric from %s len %d\n", beacon.Name, len(beacon.beacon_metrics))
beacon.beacon_metrics = append(beacon.beacon_metrics[:0], beacon.beacon_metrics[0+1:]...)
}
//fmt.Printf("%#v\n", beacon.beacon_metrics)
if beacon.HB_ButtonCounter_Prev != beacon.HB_ButtonCounter {
beacon.HB_ButtonCounter_Prev = incoming.HB_ButtonCounter
// send the button message to MQTT
sendButtonPressed(beacon, cl)
}
BEACONS.Beacons[beacon.Beacon_id] = beacon

/*if beacon.Beacon_type == "hb_button" {
processButton(beacon, cl)
}*/

//lookup location by hostname in locations
location, ok := locations_list.locations[incoming.Hostname]
if !ok {
//create the location
locations_list.locations[incoming.Hostname] = Location{}
location, ok = locations_list.locations[incoming.Hostname]
location.name = incoming.Hostname
}
locations_list.locations[incoming.Hostname] = location
}()
}
}
}()

return incoming_msgs_chan
}
func ParseTimeStamp(utime string) (string, error) {
i, err := strconv.ParseInt(utime, 10, 64)
if err != nil {
return "", err
}
t := time.Unix(i, 0)
return t.Format(time.UnixDate), nil
}

var http_host_path_ptr *string

// var https_host_path_ptr *string
var httpws_host_path_ptr *string

//var httpwss_host_path_ptr *string

type Todo struct {
Id string `json:"id"`
Value string `json:"value" binding:"required"`
}

type Job interface {
ExitChan() chan error
Run(todos map[string]Todo) (map[string]Todo, error)
}

func ProcessJobs(jobs chan Job, db string) {
for {
j := <-jobs

todos := make(map[string]Todo, 0)
content, err := ioutil.ReadFile(db)
if err == nil {
if err = json.Unmarshal(content, &todos); err == nil {
todosMod, err := j.Run(todos)

if err == nil && todosMod != nil {
b, err := json.Marshal(todosMod)
if err == nil {
err = ioutil.WriteFile(db, b, 0644)
}
}
}
}

j.ExitChan() <- err
}
}

type user struct {
id string
logger *log.Logger
}

const ShellToUse = "bash"

func Shellout(command string) (error, string, string) {
var stdout bytes.Buffer
var stderr bytes.Buffer
///utils.Log.Printf("command: %s",command)
cmd := exec.Command(ShellToUse, "-c", command)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return err, stdout.String(), stderr.String()
}

func createUser(id string, logWanted bool) user {
var l *log.Logger

if logWanted {
// Here the log content will be added in the user log file
userFIle := &lumberjack.Logger{
Filename: "/data/var/log/presence/presence/log_" + id + ".log",
MaxSize: 250, // mb
MaxBackups: 5,
MaxAge: 10, // in days
}
l = log.New(userFIle, "User: ", log.Ldate|log.Ltime|log.Lshortfile)
} else {
// Here the log content will go nowhere
l = log.New(ioutil.Discard, "User: ", log.Ldate|log.Ltime|log.Lshortfile)
}
return user{id, l}
}

func main() {

loggers := []*user{}

http_host_path_ptr = flag.String("http_host_path", "0.0.0.0:8080", "The host:port that the HTTP server should listen on")
//https_host_path_ptr = flag.String("https_host_path", "0.0.0.0:5443", "The host:port that the HTTP server should listen on")
httpws_host_path_ptr = flag.String("httpws_host_path", "0.0.0.0:8088", "The host:port websocket listen")
//httpwss_host_path_ptr = flag.String("httpwss_host_path", "0.0.0.0:8443", "The host:port secure websocket listen")

mqtt_host_ptr := flag.String("mqtt_host", "localhost:1883", "The host:port of the MQTT server to listen for beacons on")
mqtt_username_ptr := flag.String("mqtt_username", "none", "The username needed to connect to the MQTT server, 'none' if it doesn't need one")
mqtt_password_ptr := flag.String("mqtt_password", "none", "The password needed to connect to the MQTT server, 'none' if it doesn't need one")
mqtt_client_id_ptr := flag.String("mqtt_client_id", "presence-detector", "The client ID for the MQTT server")

flag.Parse()
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, os.Kill)

// Create an MQTT Client.
cli := client.New(&client.Options{
// Define the processing of the error handler.
ErrorHandler: func(err error) {
fmt.Println(err)
},
})
// Terminate the Client.
defer cli.Terminate()

//open the database
db, err = bolt.Open("/data/conf/presence/presence.db", 0644, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()

// Connect to the MQTT Server.
err = cli.Connect(&client.ConnectOptions{
Network: "tcp",
Address: *mqtt_host_ptr,
ClientID: []byte(*mqtt_client_id_ptr),
UserName: []byte(*mqtt_username_ptr),
Password: []byte(*mqtt_password_ptr),
})
if err != nil {
panic(err)
}

incoming_updates_chan := IncomingMQTTProcessor(1*time.Second, cli, db, loggers)

// Subscribe to topics.
err = cli.Subscribe(&client.SubscribeOptions{
SubReqs: []*client.SubReq{
&client.SubReq{
@@ -802,8 +32,6 @@ func main() {
t := strings.Split(string(topicName), "/")
hostname := t[1]

//Formato JSON multiplo
//publish_out/170361001234 [{"timestamp":"2025-06-11T11:27:28.492Z","type":"Gateway","mac":"E4B3230DB5CC","nums":10},{"timestamp":"2025-06-11T11:27:28.483Z","mac":"36CE2D7CA4E5","rssi":-27,"rawData":"1EFF0600010F20226F50BB5F834F6C9CE3D876B0C3F665882955B368D3B96C"},{"timestamp":"2025-06-11T11:27:28.586Z","mac":"36CE2D7CA4E5","rssi":-30,"rawData":"1EFF0600010F20226F50BB5F834F6C9CE3D876B0C3F665882955B368D3B96C"},{"timestamp":"2025-06-11T11:27:28.612Z","mac":"406260A302FC","rssi":-35,"rawData":"02011A020A0B0BFF4C001006371AAE2F6F5B"},{"timestamp":"2025-06-11T11:27:28.798Z","mac":"36CE2D7CA4E5","rssi":-28,"rawData":"1EFF0600010F20226F50BB5F834F6C9CE3D876B0C3F665882955B368D3B96C"},{"timestamp":"2025-06-11T11:27:28.905Z","mac":"36CE2D7CA4E5","rssi":-30,"rawData":"1EFF0600010F20226F50BB5F834F6C9CE3D876B0C3F665882955B368D3B96C"},{"timestamp":"2025-06-11T11:27:28.945Z","mac":"C300003947DF","rssi":-32,"rawData":"0201061AFF4C000215FDA50693A4E24FB1AFCFC6EB0764782500000000C5"},{"timestamp":"2025-06-11T11:27:29.013Z","mac":"36CE2D7CA4E5","rssi":-29,"rawData":"1EFF0600010F20226F50BB5F834F6C9CE3D876B0C3F665882955B368D3B96C"},{"timestamp":"2025-06-11T11:27:29.120Z","mac":"36CE2D7CA4E5","rssi":-27,"rawData":"1EFF0600010F20226F50BB5F834F6C9CE3D876B0C3F665882955B368D3B96C"},{"timestamp":"2025-06-11T11:27:29.166Z","mac":"406260A302FC","rssi":-34,"rawData":"02011A020A0B0BFF4C001006371AAE2F6F5B"},{"timestamp":"2025-06-11T11:27:29.337Z","mac":"36CE2D7CA4E5","rssi":-26,"rawData":"1EFF0600010F20226F50BB5F834F6C9CE3D876B0C3F665882955B368D3B96C"}]
if strings.HasPrefix(msgStr, "[") {
var readings []RawReading
err := json.Unmarshal(message, &readings)
@@ -826,7 +54,6 @@ func main() {
incoming_updates_chan <- incoming
}
} else {
//publish_out/171061001180 $GPRP,C83F8F17DB35,F5B0B0419FEF,-44,02010612FF590080BC280103FFFFFFFF000000000000,1749648798
s := strings.Split(string(message), ",")
if len(s) < 6 {
log.Printf("Messaggio CSV non valido: %s", msgStr)


+ 33
- 0
internal/pkg/config/config.go 파일 보기

@@ -0,0 +1,33 @@
package config

import "os"

type Config struct {
HTTPAddr string
WSAddr string
MQTTHost string
MQTTUser string
MQTTPass string
MQTTClientID string
DBPath 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", "localhost:1883"),
MQTTUser: getEnv("MQTT_USERNAME", "none"),
MQTTPass: getEnv("MQTT_PASSWORD", "none"),
MQTTClientID: getEnv("MQTT_CLIENT_ID", "presence-detector"),
DBPath: getEnv("DB_PATH", "/data/conf/presence/presence.db"),
}
}

+ 28
- 28
internal/pkg/httpserver/server.go 파일 보기

@@ -10,7 +10,7 @@ import (
"time"

"github.com/AFASystems/presence/internal/pkg/model"
"github.com/AFASystems/presence/internal/pkg/utils"
"github.com/AFASystems/presence/internal/pkg/persistence"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
@@ -44,17 +44,17 @@ func StartHTTPServer(addr string, ctx *model.AppContext) {

// Set up HTTP server
r := mux.NewRouter()
r.HandleFunc("/api/results", resultsHandler(&ctx.HTTPResultLock, &ctx.HTTPResults))
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.LatestListLock, ctx.LatestBeaconsList)).Methods("GET")
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.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/"))))
@@ -64,8 +64,8 @@ func StartHTTPServer(addr string, ctx *model.AppContext) {
http.Handle("/", r)

mxWS := mux.NewRouter()
mxWS.HandleFunc("/ws/api/beacons", serveWs(&ctx.HTTPResultLock, &ctx.HTTPResults))
mxWS.HandleFunc("/ws/api/beacons/latest", serveLatestBeaconsWs(&ctx.LatestListLock, ctx.LatestBeaconsList))
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)

@@ -81,11 +81,11 @@ func StartHTTPServer(addr string, ctx *model.AppContext) {

// TODO: rather add defer to unlock the files

func resultsHandler(lock *sync.RWMutex, httpResults *model.HTTPLocationsList) http.HandlerFunc {
func resultsHandler(httpResults *model.HTTPResultsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
lock.Lock()
js, err := json.Marshal(httpResults)
lock.Unlock()
httpResults.HTTPResultsLock.Lock()
defer httpResults.HTTPResultsLock.Unlock()
js, err := json.Marshal(httpResults.HTTPResults)

if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -128,7 +128,7 @@ func BeaconsAddHandler(beacons *model.BeaconsList) http.HandlerFunc {

beacons.Beacons[inBeacon.Beacon_id] = inBeacon

err = utils.PersistBeacons(beacons)
err = persistence.PersistBeacons(beacons)

if err != nil {
http.Error(w, "trouble persisting beacons list, create bucket", 500)
@@ -156,7 +156,7 @@ func BeaconsDeleteHandler(beacons *model.BeaconsList, buttonsList map[string]mod
delete(buttonsList, beaconId)
}

err := utils.PersistBeacons(beacons)
err := persistence.PersistBeacons(beacons)
if err != nil {
http.Error(w, "trouble persisting beacons list, create bucket", 500)
return
@@ -166,14 +166,14 @@ func BeaconsDeleteHandler(beacons *model.BeaconsList, buttonsList map[string]mod
}
}

func latestBeaconsListHandler(lock *sync.RWMutex, latestBeaconsList map[string]model.Beacon) http.HandlerFunc {
func latestBeaconsListHandler(latestList *model.LatestBeaconsList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
lock.RLock()
latestList.LatestListLock.RLock()
var la = make([]model.Beacon, 0)
for _, b := range latestBeaconsList {
for _, b := range latestList.LatestList {
la = append(la, b)
}
lock.RUnlock()
latestList.LatestListLock.RUnlock()
js, err := json.Marshal(la)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -184,7 +184,7 @@ func latestBeaconsListHandler(lock *sync.RWMutex, latestBeaconsList map[string]m
}
}

func settingsListHandler(settings *model.Settings) http.HandlerFunc {
func SettingsListHandler(settings *model.Settings) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
js, err := json.Marshal(settings)
if err != nil {
@@ -196,7 +196,7 @@ func settingsListHandler(settings *model.Settings) http.HandlerFunc {
}
}

func settingsEditHandler(settings *model.Settings) http.HandlerFunc {
func SettingsEditHandler(settings *model.Settings) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var inSettings model.Settings
@@ -216,7 +216,7 @@ func settingsEditHandler(settings *model.Settings) http.HandlerFunc {

*settings = inSettings

err = utils.PersistSettings(settings)
err = persistence.PersistSettings(settings)
if err != nil {
http.Error(w, "trouble persisting settings, create bucket", 500)
return
@@ -239,7 +239,7 @@ func reader(ws *websocket.Conn) {
}
}

func writer(ws *websocket.Conn, lock *sync.RWMutex, httpResults *model.HTTPLocationsList) {
func writer(ws *websocket.Conn, httpResult *model.HTTPResultsList) {
pingTicker := time.NewTicker(pingPeriod)
beaconTicker := time.NewTicker(beaconPeriod)
defer func() {
@@ -250,9 +250,9 @@ func writer(ws *websocket.Conn, lock *sync.RWMutex, httpResults *model.HTTPLocat
for {
select {
case <-beaconTicker.C:
lock.RLock()
js, err := json.Marshal(httpResults)
lock.RUnlock()
httpResult.HTTPResultsLock.Lock()
defer httpResult.HTTPResultsLock.Unlock()
js, err := json.Marshal(httpResult.HTTPResults)

if err != nil {
js = []byte("error")
@@ -271,7 +271,7 @@ func writer(ws *websocket.Conn, lock *sync.RWMutex, httpResults *model.HTTPLocat
}
}

func serveWs(lock *sync.RWMutex, httpResults *model.HTTPLocationsList) http.HandlerFunc {
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 {
@@ -281,7 +281,7 @@ func serveWs(lock *sync.RWMutex, httpResults *model.HTTPLocationsList) http.Hand
return
}

go writer(ws, lock, httpResults)
go writer(ws, httpResult)
reader(ws)
}
}
@@ -323,7 +323,7 @@ func latestBeaconWriter(ws *websocket.Conn, latestBeaconsList map[string]model.B
}
}

func serveLatestBeaconsWs(lock *sync.RWMutex, latestBeaconsList map[string]model.Beacon) http.HandlerFunc {
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 {
@@ -333,7 +333,7 @@ func serveLatestBeaconsWs(lock *sync.RWMutex, latestBeaconsList map[string]model
return
}

go latestBeaconWriter(ws, latestBeaconsList, lock)
go latestBeaconWriter(ws, latestList.LatestList, &latestList.LatestListLock)
reader(ws)
}
}


+ 29
- 20
internal/pkg/model/types.go 파일 보기

@@ -48,10 +48,10 @@ type Advertisement struct {

// BeaconMetric stores signal and distance data for a beacon.
type BeaconMetric struct {
location string
distance float64
rssi int64
timestamp int64
Location string
Distance float64
Rssi int64
Timestamp int64
}

// Location defines a physical location and synchronization control.
@@ -62,9 +62,9 @@ type Location struct {

// BestLocation represents the most probable location of a beacon.
type BestLocation struct {
distance float64
name string
last_seen int64
Distance float64
Name string
Last_seen int64
}

// HTTPLocation describes a beacon's state as served over HTTP.
@@ -116,10 +116,10 @@ type Beacon struct {
Distance float64 `json:"distance"`
Previous_location string
Previous_confident_location string
expired_location string
Expired_location string
Location_confidence int64
Location_history []string
beacon_metrics []BeaconMetric
Beacon_metrics []BeaconMetric

HB_ButtonCounter int64 `json:"hb_button_counter"`
HB_ButtonCounter_Prev int64 `json:"hb_button_counter"`
@@ -152,8 +152,8 @@ type BeaconsList struct {

// LocationsList holds all known locations with concurrency protection.
type LocationsList struct {
locations map[string]Location
lock sync.RWMutex
Locations map[string]Location
Lock sync.RWMutex
}

// Message defines the WebSocket or broadcast message payload.
@@ -172,16 +172,25 @@ type RawReading struct {
RawData string `json:"rawData"`
}

type LatestBeaconsList struct {
LatestList map[string]Beacon
LatestListLock sync.RWMutex
}

type HTTPResultsList struct {
HTTPResultsLock sync.RWMutex
HTTPResults HTTPLocationsList
}

type AppContext struct {
HTTPResultLock sync.RWMutex
HTTPResults HTTPLocationsList
LatestListLock sync.RWMutex
Beacons BeaconsList
ButtonsList map[string]Button
LatestBeaconsList map[string]Beacon
Settings Settings
Clients map[*websocket.Conn]bool
Broadcast chan Message
HTTPResults HTTPResultsList
Beacons BeaconsList
ButtonsList map[string]Button
Settings Settings
Clients map[*websocket.Conn]bool
Broadcast chan Message
Locations LocationsList
LatestList LatestBeaconsList
}

var World = []byte("presence")


+ 128
- 0
internal/pkg/mqtt_client/beacon.go 파일 보기

@@ -0,0 +1,128 @@
package mqtt_client

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.LatestListLock.Lock()
defer latestList.LatestListLock.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/mqtt_client/fillter.go 파일 보기

@@ -0,0 +1,35 @@
package mqtt_client

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/mqtt_client/location.go 파일 보기

@@ -0,0 +1,165 @@
package mqtt_client

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

+ 61
- 0
internal/pkg/mqtt_client/processor.go 파일 보기

@@ -0,0 +1,61 @@
package mqtt_client

import (
"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) // DONE
id := getBeaconID(incoming) // DONE
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) // DONE
return
}

updateBeaconData(&beacon, incoming, now, cl, settings)
beacons.Beacons[beacon.Beacon_id] = beacon
}

+ 18
- 0
internal/pkg/persistence/buckets.go 파일 보기

@@ -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 파일 보기

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

internal/pkg/utils/utils.go → internal/pkg/persistence/persist.go 파일 보기

@@ -1,4 +1,4 @@
package utils
package persistence

import (
"bytes"

+ 40
- 0
internal/structure.md 파일 보기

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

+ 44
- 0
test/httpserver_test/httpserver_test.go 파일 보기

@@ -112,5 +112,49 @@ func TestBeaconCRUD(t *testing.T) {
}

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 파일 보기

@@ -0,0 +1,46 @@
package mqtt_test

import (
"os"
"testing"
"time"

"github.com/AFASystems/presence/internal/pkg/model"
"github.com/AFASystems/presence/internal/pkg/mqtt"
"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.LoadInitialState(ctx)

ch := mqtt.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")
}
}

불러오는 중...
취소
저장