From 8e7689328ddf6f35a8c783217d0315315e3dc0de Mon Sep 17 00:00:00 2001 From: blazSenlab Date: Wed, 15 Oct 2025 10:32:52 +0200 Subject: [PATCH] chore: modify project structure, move types into internal package, define main package boilerplate --- cmd/{_your_app_ => presenSe}/.keep | 0 cmd/presenSe/presense.go | 7 + .../pkg/{_your_private_lib_ => model}/.keep | 0 internal/pkg/model/model.md | 3 + internal/pkg/model/types.go | 168 ++ main.go | 1568 +++++++++++++++++ 6 files changed, 1746 insertions(+) rename cmd/{_your_app_ => presenSe}/.keep (100%) create mode 100644 cmd/presenSe/presense.go rename internal/pkg/{_your_private_lib_ => model}/.keep (100%) create mode 100644 internal/pkg/model/model.md create mode 100644 internal/pkg/model/types.go create mode 100644 main.go diff --git a/cmd/_your_app_/.keep b/cmd/presenSe/.keep similarity index 100% rename from cmd/_your_app_/.keep rename to cmd/presenSe/.keep diff --git a/cmd/presenSe/presense.go b/cmd/presenSe/presense.go new file mode 100644 index 0000000..e64fcf2 --- /dev/null +++ b/cmd/presenSe/presense.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("this is the main file") +} diff --git a/internal/pkg/_your_private_lib_/.keep b/internal/pkg/model/.keep similarity index 100% rename from internal/pkg/_your_private_lib_/.keep rename to internal/pkg/model/.keep diff --git a/internal/pkg/model/model.md b/internal/pkg/model/model.md new file mode 100644 index 0000000..3caa8c5 --- /dev/null +++ b/internal/pkg/model/model.md @@ -0,0 +1,3 @@ +# MODELS + +This file includes type definitions for aggregate struct types \ No newline at end of file diff --git a/internal/pkg/model/types.go b/internal/pkg/model/types.go new file mode 100644 index 0000000..12d981a --- /dev/null +++ b/internal/pkg/model/types.go @@ -0,0 +1,168 @@ +package model + +import "sync" + +// 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"` + 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"` + 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"` +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..dc77a3b --- /dev/null +++ b/main.go @@ -0,0 +1,1568 @@ +package main + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "flag" + "fmt" + "log" + "math" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "sync" + "time" + "io/ioutil" + //"./utils" + "gopkg.in/natefinch/lumberjack.v2" + "os/exec" + "github.com/boltdb/bolt" + + "github.com/yosssi/gmq/mqtt" + "github.com/yosssi/gmq/mqtt/client" + + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/gorilla/handlers" +) + +const ( + // Time allowed to write the file to the client. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the client. + pongWait = 60 * time.Second + + // Send pings to client with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + beaconPeriod = 2 * time.Second +) + +// data structures + +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"` +} + +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"` + HB_Battery int64 `json:"hb_button_battery"` + HB_RandomNonce string `json:"hb_button_random"` + HB_ButtonMode string `json:"hb_button_mode"` +} + +type Advertisement struct { + ttype string + content string + seen int64 +} + +type beacon_metric struct { + location string + distance float64 + rssi int64 + timestamp int64 +} + +type Location struct { + name string + lock sync.RWMutex +} + +type Best_location struct { + distance float64 + name string + last_seen int64 +} + +type HTTP_location 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"` + +} + +type Location_change 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"` +} + +type HA_message struct { + Beacon_id string `json:"id"` + Beacon_name string `json:"name"` + Distance float64 `json:"distance"` +} + +type HTTP_locations_list struct { + Beacons []HTTP_location `json:"beacons"` + //Buttons []Button `json:"buttons"` +} + +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 []beacon_metric + + HB_ButtonCounter int64 `json:"hb_button_counter"` + HB_ButtonCounter_Prev 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"` + +} + +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"` +} + +type Beacons_list struct { + Beacons map[string]Beacon `json:"beacons"` + lock sync.RWMutex +} + +type Locations_list struct { + locations map[string]Location + lock sync.RWMutex +} + +var clients = make(map[*websocket.Conn]bool) // connected clients +var broadcast = make(chan Message) // broadcast channel + + +// Define our message object +type Message struct { + Email string `json:"email"` + Username string `json:"username"` + Message string `json:"message"` +} + +// Struttura per il parsing JSON multiplo +type RawReading struct { + Timestamp string `json:"timestamp"` + Type string `json:"type"` + MAC string `json:"mac"` + RSSI int `json:"rssi"` + RawData string `json:"rawData"` +} + + +// GLOBALS + +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) + + // create the file if it doesn't exists with O_CREATE, Set the file up for read write, add the append flag and set the permission + //f, err := os.OpenFile("/data/conf/presence/db.json", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0660) + //if err != nil { + // log.Fatal(err) + //} + // write to file, f.Write() + //f.Write(btn_msg) +} + + +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 + } + //fmt.Printf("beacon: %s list: %#v\n", beacon.Name, loc_list) + // now go through the list and find the largest, that's the location + 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} + +// //filter, only let this location become best if it was X times in a row +// if best_location.name == beacon.Previous_location { +// beacon.Location_confidence = beacon.Location_confidence + 1 +// } else { +// beacon.Location_confidence = 0 +// /////fmt.Printf("beacon.Location_confidence %f\n", beacon.Location_confidence) +// } + + // Aggiungiamo il nuovo best_location allo storico + 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) + } + + + + // 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) + 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) + } + } + } + + /////fmt.Printf("\n\n%s is most likely in %s with average distance %f \n\n", beacon.Name, best_location.name, best_location.distance) + ////beacon.logger.Printf("Log content: user id %v \n", beacon.Name) + // publish this to a topic + // Publish a message. + 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) + } + + } + + /*for _, button := range Buttons_list { + http_results.Buttons = append(http_results.Buttons, button) + }*/ + + if should_persist { + persistBeacons() + } +} + + +/*func doSomething(bcon Beacon, testo string ) { + bcon.logger.Printf("Log content: user id %v \n", beacon.Name) +}*/ + +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) + } + + //debug list them out + + /*fmt.Println("Database beacons:") + for _, beacon := range BEACONS.Beacons { + fmt.Println("Database has known beacon: " + beacon.Beacon_id + " " + beacon.Name) + dog := new(user) + //createUser( beacon.Name, true) + + //user1 := createUser( beacon.Name, true) + //doSomething(beacon, "hello") + // + + userFIle := &lumberjack.Logger{ + Filename: "/data/presence/presence/beacon_log_" + beacon.Name + ".log", + MaxSize: 250, // mb + MaxBackups: 5, + MaxAge: 10, // in days + } + dog.id = beacon.Name + dog.logger = log.New(userFIle, "User: ", log.Ldate|log.Ltime|log.Lshortfile) + dog.logger.Printf("Log content: user id %v \n", beacon.Name) + logger=append(logger,dog) + } + fmt.Println("leng has %d\n",len(logger)) + fmt.Printf("%v", logger) + fmt.Println("Settings has %#v\n", settings)*/ + /**/ + 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() + + ///fmt.Println("sawbeacon " + this_beacon_id + " at " + incoming.Hostname) + //logger["FCB8351F5A21"].logger.Printf("Log content: user id \n") + //if this beacon isn't in our search list, add it to the latest_beacons pile. + 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{} + // 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") + 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() + + ///utils.NewLog(*logpath) + ///utils.Log.Println("hello") + // Set up channel on which to send signal notifications. + 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{ + TopicFilter: []byte("publish_out/#"), + QoS: mqtt.QoS0, + Handler: func(topicName, message []byte) { + msgStr := string(message) + 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) + if err != nil { + log.Printf("Errore parsing JSON: %v", err) + return + } + + for _, reading := range readings { + if reading.Type == "Gateway" { + continue + } + incoming := Incoming_json{ + Hostname: hostname, + MAC: reading.MAC, + RSSI: int64(reading.RSSI), + Data: reading.RawData, + HB_ButtonCounter: parseButtonState(reading.RawData), + } + incoming_updates_chan <- incoming + } + } else { + //Formato CSV + //ingics solo annuncio + //publish_out/171061001180 $GPRP,C83F8F17DB35,F5B0B0419FEF,-44,02010612FF590080BC280102FFFFFFFF000000000000,1749648798 + //ingics tasto premuto + //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) + return + } + + rawdata := s[4] + buttonCounter := parseButtonState(rawdata) + if buttonCounter > 0 { + incoming := 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) + } + incoming_updates_chan <- incoming + } + } + }, + }, + }, + }) + if err != nil { + panic(err) + } + + + fmt.Println("CONNECTED TO MQTT") + fmt.Println("\n ") + fmt.Println("Visit http://" + *http_host_path_ptr + " on your browser to see the web interface") + fmt.Println("\n ") + + go startServer() + + // Wait for receiving a signal. + <-sigc + + // Disconnect the Network Connection. + if err := cli.Disconnect(); err != nil { + panic(err) + } +} + +func startServer() { + + 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) + + r.HandleFunc("/api/beacons/{beacon_id}", beaconsDeleteHandler).Methods("DELETE") + r.HandleFunc("/api/beacons", beaconsListHandler).Methods("GET") + r.HandleFunc("/api/beacons", beaconsAddHandler).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).Methods("PUT") + + r.HandleFunc("/api/latest-beacons", latestBeaconsListHandler).Methods("GET") + + + r.HandleFunc("/api/settings", settingsListHandler).Methods("GET") + r.HandleFunc("/api/settings", settingsEditHandler).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) + mxWS.HandleFunc("/ws/api/beacons/latest", serveLatestBeaconsWs) + mxWS.HandleFunc("/ws/broadcast", handleConnections) + http.Handle("/ws/", mxWS) + + + + go func() { + log.Fatal(http.ListenAndServe(*httpws_host_path_ptr, nil)) + }() + + // Start listening for incoming chat messages + go handleMessages() + + ///"/conf/etc/cert/certs/services/htdocs/majornet.crt", "/conf/etc/cert/private/services/htdocs/majornet.key" + http.ListenAndServe(*http_host_path_ptr, handlers.CORS(originsOk, headersOk, methodsOk)(r)) + +} + + +func handleConnections(w http.ResponseWriter, r *http.Request) { + // Upgrade initial GET request to a websocket + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Fatal(err) + } + // Make sure we close the connection when the function returns + defer ws.Close() + + // Register our new client + clients[ws] = true + + for { + var msg Message + // Read in a new message as JSON and map it to a Message object + err := ws.ReadJSON(&msg) + if err != nil { + log.Printf("error: %v", err) + delete(clients, ws) + break + } + // Send the newly received message to the broadcast channel + broadcast <- msg + } +} + +func handleMessages() { + for { + // Grab the next message from the broadcast channel + msg := <-broadcast + // Send it out to every client that is currently connected + for client := range clients { + err := client.WriteJSON(msg) + if err != nil { + log.Printf("error: %v", err) + client.Close() + delete(clients, client) + } + } + } +} + + + + + +func resultsHandler(w http.ResponseWriter, r *http.Request) { + http_results_lock.RLock() + js, err := json.Marshal(http_results) + http_results_lock.RUnlock() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Write(js) +} + +func beaconsListHandler(w http.ResponseWriter, r *http.Request) { + latest_list_lock.RLock() + js, err := json.Marshal(BEACONS) + latest_list_lock.RUnlock() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Write(js) +} + +func persistBeacons() error { + // gob it first + buf := &bytes.Buffer{} + enc := gob.NewEncoder(buf) + if err := enc.Encode(BEACONS); err != nil { + return err + } + + key := []byte("beacons_list") + // store some data + err = db.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(world) + if err != nil { + return err + } + + err = bucket.Put(key, []byte(buf.String())) + if err != nil { + return err + } + return nil + }) + return nil +} + +func persistSettings() error { + // gob it first + buf := &bytes.Buffer{} + enc := gob.NewEncoder(buf) + if err := enc.Encode(settings); err != nil { + return err + } + + key := []byte("settings") + // store some data + err = db.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(world) + if err != nil { + return err + } + + err = bucket.Put(key, []byte(buf.String())) + if err != nil { + return err + } + return nil + }) + return nil +} + +func beaconsAddHandler(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var in_beacon Beacon + err = decoder.Decode(&in_beacon) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + + //make sure name and beacon_id are present + if (len(strings.TrimSpace(in_beacon.Name)) == 0) || (len(strings.TrimSpace(in_beacon.Beacon_id)) == 0) { + http.Error(w, "name and beacon_id cannot be blank", 400) + return + } + + BEACONS.Beacons[in_beacon.Beacon_id] = in_beacon + + err := persistBeacons() + if err != nil { + http.Error(w, "trouble persisting beacons list, create bucket", 500) + return + } + + w.Write([]byte("ok")) +} + +func beaconsDeleteHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + beacon_id := vars["beacon_id"] + delete(BEACONS.Beacons, beacon_id) + + _, ok := Buttons_list[beacon_id] + if ok { + delete(Buttons_list, beacon_id) + } + + err := persistBeacons() + if err != nil { + http.Error(w, "trouble persisting beacons list, create bucket", 500) + return + } + + w.Write([]byte("ok")) +} + +func latestBeaconsListHandler(w http.ResponseWriter, r *http.Request) { + latest_list_lock.RLock() + var la = make([]Beacon, 0) + for _, b := range Latest_beacons_list { + la = append(la, b) + } + latest_list_lock.RUnlock() + js, err := json.Marshal(la) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Write(js) +} + +func settingsListHandler(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(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var in_settings Settings + err = decoder.Decode(&in_settings) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + + //make sure values are > 0 + if (in_settings.Location_confidence <= 0) || + (in_settings.Last_seen_threshold <= 0) || + (in_settings.HA_send_interval <= 0) { + http.Error(w, "values must be greater than 0", 400) + return + } + + settings = in_settings + + err := persistSettings() + if err != nil { + http.Error(w, "trouble persisting settings, create bucket", 500) + return + } + + w.Write([]byte("ok")) +} + +//websocket stuff +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) { + pingTicker := time.NewTicker(pingPeriod) + beaconTicker := time.NewTicker(beaconPeriod) + defer func() { + pingTicker.Stop() + beaconTicker.Stop() + ws.Close() + }() + for { + select { + case <-beaconTicker.C: + + http_results_lock.RLock() + js, err := json.Marshal(http_results) + http_results_lock.RUnlock() + + 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(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) + reader(ws) +} + +func latestBeaconWriter(ws *websocket.Conn) { + pingTicker := time.NewTicker(pingPeriod) + beaconTicker := time.NewTicker(beaconPeriod) + defer func() { + pingTicker.Stop() + beaconTicker.Stop() + ws.Close() + }() + for { + select { + case <-beaconTicker.C: + + latest_list_lock.RLock() + var la = make([]Beacon, 0) + for _, b := range Latest_beacons_list { + la = append(la, b) + } + latest_list_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(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) + reader(ws) +}