| @@ -1,13 +1,13 @@ | |||||
| package main | package main | ||||
| import ( | import ( | ||||
| "time" | |||||
| "fmt" | "fmt" | ||||
| "github.com/AFASystems/presence/internal/pkg/bridge/mqtthandler" | |||||
| "github.com/AFASystems/presence/internal/pkg/config" | "github.com/AFASystems/presence/internal/pkg/config" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafka" | |||||
| "github.com/yosssi/gmq/mqtt" | "github.com/yosssi/gmq/mqtt" | ||||
| "github.com/yosssi/gmq/mqtt/client" | "github.com/yosssi/gmq/mqtt/client" | ||||
| "github.com/AFASystems/presence/internal/pkg/bridge/mqtthandler" | |||||
| "github.com/segmentio/kafka-go" | |||||
| ) | ) | ||||
| func main() { | func main() { | ||||
| @@ -33,15 +33,15 @@ func main() { | |||||
| panic(err) | panic(err) | ||||
| } | } | ||||
| writer := kafkaWriter("127.0.0.1:9092", "rawbeacons") | |||||
| writer := kafka.KafkaWriter("127.0.0.1:9092", "rawbeacons") | |||||
| defer writer.Close() | defer writer.Close() | ||||
| err = cli.Subscribe(&client.SubscribeOptions{ | err = cli.Subscribe(&client.SubscribeOptions{ | ||||
| SubReqs: []*client.SubReq{ | SubReqs: []*client.SubReq{ | ||||
| &client.SubReq{ | |||||
| { | |||||
| TopicFilter: []byte("publish_out/#"), | TopicFilter: []byte("publish_out/#"), | ||||
| QoS: mqtt.QoS0, | QoS: mqtt.QoS0, | ||||
| Handler: func(topicName, message[]byte) { | |||||
| Handler: func(topicName, message []byte) { | |||||
| mqtthandler.MqttHandler(writer, topicName, message) | mqtthandler.MqttHandler(writer, topicName, message) | ||||
| }, | }, | ||||
| }, | }, | ||||
| @@ -53,13 +53,3 @@ func main() { | |||||
| select {} | select {} | ||||
| } | } | ||||
| func kafkaWriter(kafkaURL, topic string) *kafka.Writer { | |||||
| return &kafka.Writer{ | |||||
| Addr: kafka.TCP(kafkaURL), | |||||
| Topic: topic, | |||||
| Balancer: &kafka.LeastBytes{}, | |||||
| BatchSize: 100, | |||||
| BatchTimeout: 10 * time.Millisecond, | |||||
| } | |||||
| } | |||||
| @@ -2,17 +2,31 @@ package main | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "encoding/json" | |||||
| "fmt" | "fmt" | ||||
| "strings" | |||||
| "math" | |||||
| "strconv" | |||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/config" | |||||
| "github.com/AFASystems/presence/internal/pkg/kafka" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/AFASystems/presence/internal/pkg/mqttclient" | "github.com/AFASystems/presence/internal/pkg/mqttclient" | ||||
| presenseredis "github.com/AFASystems/presence/internal/pkg/redis" | |||||
| "github.com/redis/go-redis/v9" | "github.com/redis/go-redis/v9" | ||||
| "github.com/segmentio/kafka-go" | |||||
| ) | ) | ||||
| // Move Kafka topics, Redis keys, intervals to env config | |||||
| // Replace hardcoded IPs with env vars | |||||
| // avoid defers -> lock and unlock right before and after usage | |||||
| // Distance formula uses twos_comp incorrectly should parse signed int not hex string | |||||
| // Use buffered log instead of fmt.Println ??? | |||||
| // Limit metrics slice size with ring buffer ?? | |||||
| // handle go routine exit signals with context.WithCancel() ?? | |||||
| // Make internal package for Kafka and Redis | |||||
| // Make internal package for processor: | |||||
| // Helper functions: twos_comp, getBeaconId | |||||
| func main() { | func main() { | ||||
| // Load global context to init beacons and latest list | // Load global context to init beacons and latest list | ||||
| appCtx := model.AppContext{ | appCtx := model.AppContext{ | ||||
| @@ -24,19 +38,21 @@ func main() { | |||||
| }, | }, | ||||
| } | } | ||||
| cfg := config.Load() | |||||
| // Kafka writer idk why yet | // Kafka writer idk why yet | ||||
| writer := kafkaWriter("127.0.0.1:9092", "beacons") | |||||
| writer := kafka.KafkaWriter(cfg.KafkaURL, "beacons") | |||||
| // Kafka reader for Raw MQTT beacons | // Kafka reader for Raw MQTT beacons | ||||
| rawReader := kafkaReader("127.0.0.1:9092", "rawbeacons", "someID") | |||||
| rawReader := kafka.KafkaReader(cfg.KafkaURL, "rawbeacons", "someID") | |||||
| defer rawReader.Close() | defer rawReader.Close() | ||||
| // Kafka reader for API server updates | // Kafka reader for API server updates | ||||
| apiReader := kafkaReader("127.0.0.1:9092", "apibeacons", "someID") | |||||
| apiReader := kafka.KafkaReader(cfg.KafkaURL, "apibeacons", "someID") | |||||
| defer apiReader.Close() | defer apiReader.Close() | ||||
| // Kafka reader for latest list updates | // Kafka reader for latest list updates | ||||
| latestReader := kafkaReader("127.0.0.1:9092", "latestbeacons", "someID") | |||||
| latestReader := kafka.KafkaReader(cfg.KafkaURL, "latestbeacons", "someID") | |||||
| defer latestReader.Close() | defer latestReader.Close() | ||||
| defer writer.Close() | defer writer.Close() | ||||
| @@ -45,61 +61,33 @@ func main() { | |||||
| // Init Redis Client | // Init Redis Client | ||||
| client := redis.NewClient(&redis.Options{ | client := redis.NewClient(&redis.Options{ | ||||
| Addr: "127.0.0.1:6379", | |||||
| Addr: cfg.RedisURL, | |||||
| Password: "", | Password: "", | ||||
| }) | }) | ||||
| // Initialize list values from Redis | |||||
| beaconsList, err := client.Get(ctx, "beaconsList").Result() | |||||
| if err == redis.Nil { | |||||
| fmt.Println("no beacons list, starting empty") | |||||
| } else if err != nil { | |||||
| panic(err) | |||||
| } else { | |||||
| json.Unmarshal([]byte(beaconsList), &appCtx.Beacons.Beacons) | |||||
| } | |||||
| beaconsList := presenseredis.LoadBeaconsList(client, ctx) | |||||
| appCtx.Beacons.Beacons = beaconsList | |||||
| // Initialize list values from Redis | |||||
| latestList, err := client.Get(ctx, "latestList").Result() | |||||
| if err == redis.Nil { | |||||
| fmt.Println("no latest list, starting empty") | |||||
| } else if err != nil { | |||||
| panic(err) | |||||
| } else { | |||||
| json.Unmarshal([]byte(latestList), &appCtx.LatestList.LatestList) | |||||
| } | |||||
| latestList := presenseredis.LoadLatestList(client, ctx) | |||||
| appCtx.LatestList.LatestList = latestList | |||||
| // declare channel for collecting Kafka messages | // declare channel for collecting Kafka messages | ||||
| chRaw := make(chan model.Incoming_json, 2000) | chRaw := make(chan model.Incoming_json, 2000) | ||||
| chApi := make(chan model.ApiUpdate, 2000) | chApi := make(chan model.ApiUpdate, 2000) | ||||
| chLatest := make(chan model.Incoming_json, 2000) | chLatest := make(chan model.Incoming_json, 2000) | ||||
| go consume(rawReader, chRaw) | |||||
| go consume(apiReader, chApi) | |||||
| go consume(latestReader, chLatest) | |||||
| go kafka.Consume(rawReader, chRaw) | |||||
| go kafka.Consume(apiReader, chApi) | |||||
| go kafka.Consume(latestReader, chLatest) | |||||
| go func() { | go func() { | ||||
| // Syncing Redis cache every 1s with 2 lists: beacons, latest list | |||||
| ticker := time.NewTicker(1 * time.Second) | ticker := time.NewTicker(1 * time.Second) | ||||
| defer ticker.Stop() | defer ticker.Stop() | ||||
| for range ticker.C { | for range ticker.C { | ||||
| appCtx.Beacons.Lock.Lock() | |||||
| data, _ := json.Marshal(appCtx.Beacons.Beacons) | |||||
| appCtx.Beacons.Lock.Unlock() | |||||
| err := client.Set(ctx, "beaconsList", data, 0).Err() | |||||
| if err != nil { | |||||
| fmt.Println("error saving to redis:", err) | |||||
| } | |||||
| appCtx.LatestList.Lock.Lock() | |||||
| ldata, _ := json.Marshal(appCtx.LatestList.LatestList) | |||||
| appCtx.LatestList.Lock.Unlock() | |||||
| err = client.Set(ctx, "latestList", ldata, 0).Err() | |||||
| if err != nil { | |||||
| fmt.Println("error saving latest list:", err) | |||||
| } | |||||
| presenseredis.SaveBeaconsList(&appCtx, client, ctx) | |||||
| presenseredis.SaveLatestList(&appCtx, client, ctx) | |||||
| } | } | ||||
| }() | }() | ||||
| @@ -110,7 +98,6 @@ func main() { | |||||
| case msg := <-chApi: | case msg := <-chApi: | ||||
| switch msg.Method { | switch msg.Method { | ||||
| case "POST": | case "POST": | ||||
| fmt.Println("method POST") | |||||
| appCtx.Beacons.Lock.Lock() | appCtx.Beacons.Lock.Lock() | ||||
| appCtx.Beacons.Beacons[msg.Beacon.Beacon_id] = msg.Beacon | appCtx.Beacons.Beacons[msg.Beacon.Beacon_id] = msg.Beacon | ||||
| case "DELETE": | case "DELETE": | ||||
| @@ -119,7 +106,6 @@ func main() { | |||||
| appCtx.Beacons.Lock.Lock() | appCtx.Beacons.Lock.Lock() | ||||
| delete(appCtx.Beacons.Beacons, msg.ID) | delete(appCtx.Beacons.Beacons, msg.ID) | ||||
| } | } | ||||
| fmt.Println("method DELETE") | |||||
| default: | default: | ||||
| fmt.Println("unknown method: ", msg.Method) | fmt.Println("unknown method: ", msg.Method) | ||||
| } | } | ||||
| @@ -130,45 +116,6 @@ func main() { | |||||
| } | } | ||||
| } | } | ||||
| func kafkaWriter(kafkaURL, topic string) *kafka.Writer { | |||||
| return &kafka.Writer{ | |||||
| Addr: kafka.TCP(kafkaURL), | |||||
| Topic: topic, | |||||
| Balancer: &kafka.LeastBytes{}, | |||||
| BatchSize: 100, | |||||
| BatchTimeout: 10 * time.Millisecond, | |||||
| } | |||||
| } | |||||
| func kafkaReader(kafkaURL, topic, groupID string) *kafka.Reader { | |||||
| brokers := strings.Split(kafkaURL, ",") | |||||
| return kafka.NewReader(kafka.ReaderConfig{ | |||||
| Brokers: brokers, | |||||
| GroupID: groupID, | |||||
| Topic: topic, | |||||
| MinBytes: 1, | |||||
| MaxBytes: 10e6, | |||||
| }) | |||||
| } | |||||
| func consume[T any](r *kafka.Reader, ch chan<- T) { | |||||
| for { | |||||
| msg, err := r.ReadMessage(context.Background()) | |||||
| if err != nil { | |||||
| fmt.Println("error reading message:", err) | |||||
| continue | |||||
| } | |||||
| var data T | |||||
| if err := json.Unmarshal(msg.Value, &data); err != nil { | |||||
| fmt.Println("error decoding:", err) | |||||
| continue | |||||
| } | |||||
| ch <- data | |||||
| } | |||||
| } | |||||
| func processIncoming(incoming model.Incoming_json, ctx *model.AppContext) { | func processIncoming(incoming model.Incoming_json, ctx *model.AppContext) { | ||||
| defer func() { | defer func() { | ||||
| if err := recover(); err != nil { | if err := recover(); err != nil { | ||||
| @@ -192,10 +139,68 @@ func processIncoming(incoming model.Incoming_json, ctx *model.AppContext) { | |||||
| beacon, exists := beacons.Beacons[id] | beacon, exists := beacons.Beacons[id] | ||||
| if !exists { | if !exists { | ||||
| fmt.Println("beacon does not yet exist") | |||||
| fmt.Println("time now: ", now) | |||||
| x, exists := latestList.LatestList[id] | |||||
| if exists { | |||||
| x.Last_seen = now | |||||
| x.Incoming_JSON = incoming | |||||
| x.Distance = getBeaconDistance(incoming) | |||||
| latestList.LatestList[id] = x | |||||
| } else { | |||||
| latestList.LatestList[id] = model.Beacon{Beacon_id: id, Beacon_type: incoming.Beacon_type, Last_seen: now, Incoming_JSON: incoming, Beacon_location: incoming.Hostname, Distance: getBeaconDistance(incoming)} | |||||
| } | |||||
| // Move this to seperate routine? | |||||
| for k, v := range latestList.LatestList { | |||||
| if (now - v.Last_seen) > 10 { | |||||
| delete(latestList.LatestList, k) | |||||
| } | |||||
| } | |||||
| return | return | ||||
| } | } | ||||
| fmt.Println("Beacon does exist: ", beacon) | |||||
| updateBeacon(&beacon, incoming) | |||||
| beacons.Beacons[id] = beacon | |||||
| } | |||||
| func getBeaconDistance(incoming model.Incoming_json) float64 { | |||||
| rssi := incoming.RSSI | |||||
| power := incoming.TX_power | |||||
| distance := 100.0 | |||||
| ratio := float64(rssi) * (1.0 / float64(twos_comp(power))) | |||||
| if ratio < 1.0 { | |||||
| distance = math.Pow(ratio, 10) | |||||
| } else { | |||||
| distance = (0.89976)*math.Pow(ratio, 7.7095) + 0.111 | |||||
| } | |||||
| return distance | |||||
| } | |||||
| func updateBeacon(beacon *model.Beacon, incoming model.Incoming_json) { | |||||
| now := time.Now().Unix() | |||||
| beacon.Incoming_JSON = incoming | |||||
| beacon.Last_seen = now | |||||
| beacon.Beacon_type = incoming.Beacon_type | |||||
| beacon.HB_ButtonCounter = incoming.HB_ButtonCounter | |||||
| beacon.HB_Battery = incoming.HB_Battery | |||||
| beacon.HB_RandomNonce = incoming.HB_RandomNonce | |||||
| beacon.HB_ButtonMode = incoming.HB_ButtonMode | |||||
| if beacon.Beacon_metrics == nil { | |||||
| beacon.Beacon_metrics = make([]model.BeaconMetric, 10) // 10 is a placeholder for now | |||||
| } | |||||
| metric := model.BeaconMetric{} | |||||
| metric.Distance = getBeaconDistance(incoming) | |||||
| metric.Timestamp = now | |||||
| metric.Rssi = int64(incoming.RSSI) | |||||
| metric.Location = incoming.Hostname | |||||
| beacon.Beacon_metrics = append(beacon.Beacon_metrics, metric) | |||||
| // Leave the HB button implementation for now | |||||
| } | |||||
| func twos_comp(inp string) int64 { | |||||
| i, _ := strconv.ParseInt("0x"+inp, 0, 64) | |||||
| return i - 256 | |||||
| } | } | ||||
| @@ -0,0 +1,71 @@ | |||||
| package main | |||||
| import ( | |||||
| "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | |||||
| "github.com/redis/go-redis/v9" | |||||
| ) | |||||
| func main() { | |||||
| ctx := context.Background() | |||||
| client := redis.NewClient(&redis.Options{ | |||||
| Addr: "127.0.0.1:6379", | |||||
| Password: "", | |||||
| }) | |||||
| } | |||||
| func getLikelyLocations(client *redis.Client, ctx context.Context) { | |||||
| beaconsList, err := client.Get(ctx, "beaconsList").Result() | |||||
| var beacons = make(map[string]model.Beacon) | |||||
| if err == redis.Nil { | |||||
| fmt.Println("no beacons list, starting empty") | |||||
| } else if err != nil { | |||||
| panic(err) | |||||
| } else { | |||||
| json.Unmarshal([]byte(beaconsList), &beacons) | |||||
| } | |||||
| for id, beacon := range beacons { | |||||
| if len(beacon.Beacon_metrics) == 0 { | |||||
| continue | |||||
| } | |||||
| if isExpired(&beacon, settings) { | |||||
| handleExpiredBeacon(&beacon, cl, ctx) | |||||
| continue | |||||
| } | |||||
| best := calculateBestLocation(&beacon) | |||||
| updateBeaconState(&beacon, best, settings, ctx, cl) | |||||
| appendHTTPResult(ctx, beacon, best) | |||||
| ctx.Beacons.Beacons[id] = beacon | |||||
| } | |||||
| } | |||||
| // get likely locations: | |||||
| /* | |||||
| 1. Locks the http_results list | |||||
| 2. inits list to empty struct type -> TODO: what is this list used for | |||||
| 3. loops through beacons list -> should be locked? | |||||
| 4. check for beacon metrics -> how do you get beacon metrics, I guess it has an array of timestamps | |||||
| 5. check for threshold value in the settings | |||||
| 5.1. check for property expired location | |||||
| 5.2. if location is not expired -> mark it as expired, generate message and send to all clients, | |||||
| if clients do not respond close the connection | |||||
| 6. Init best location with type Best_location{} -> what is this type | |||||
| 7. make locations list -> key: string, val: float64 | |||||
| 7.1 set weight for seen and rssi | |||||
| 7.2 loop over metrics of the beacon -> some alogirthm based on location value | |||||
| I think the algorithm is recording names of different gateways and their rssi's and then from | |||||
| that it checks gateway name and makes decisions based on calculated values | |||||
| 7.3 writes result in best location and updates list location history with this name if the list | |||||
| is longer than 10 elements it removes the first element | |||||
| */ | |||||
| @@ -256,31 +256,18 @@ func parseButtonState(raw string) int64 { | |||||
| func twos_comp(inp string) int64 { | func twos_comp(inp string) int64 { | ||||
| i, _ := strconv.ParseInt("0x"+inp, 0, 64) | i, _ := strconv.ParseInt("0x"+inp, 0, 64) | ||||
| return i - 256 | return i - 256 | ||||
| } | } | ||||
| func getBeaconID(incoming Incoming_json) string { | func getBeaconID(incoming Incoming_json) string { | ||||
| unique_id := fmt.Sprintf("%s", incoming.MAC) | 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 | return unique_id | ||||
| } | } | ||||
| func incomingBeaconFilter(incoming Incoming_json) Incoming_json { | func incomingBeaconFilter(incoming Incoming_json) Incoming_json { | ||||
| out_json := incoming | out_json := incoming | ||||
| if incoming.Beacon_type == "hb_button" { | if incoming.Beacon_type == "hb_button" { | ||||
| //do additional checks here to detect if a Habby Bubbles Button | |||||
| // looks like 020104020a0011ff045600012d3859db59e1000b9453 | |||||
| raw_data := incoming.Data | raw_data := incoming.Data | ||||
| //company_id := []byte{0x04, 0x56} | |||||
| //product_id := []byte{0x00, 0x01} | |||||
| hb_button_prefix_str := fmt.Sprintf("02010612FF5900") | hb_button_prefix_str := fmt.Sprintf("02010612FF5900") | ||||
| if strings.HasPrefix(raw_data, hb_button_prefix_str) { | if strings.HasPrefix(raw_data, hb_button_prefix_str) { | ||||
| out_json.Namespace = "ddddeeeeeeffff5544ff" | out_json.Namespace = "ddddeeeeeeffff5544ff" | ||||
| @@ -423,14 +410,6 @@ func sendButtonPressed(bcn Beacon, cl *client.Client) { | |||||
| fmt.Println(out) | fmt.Println(out) | ||||
| fmt.Println("--- stderr ---") | fmt.Println("--- stderr ---") | ||||
| fmt.Println(errout) | 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) { | func getLikelyLocations(settings Settings, locations_list Locations_list, cl *client.Client) { | ||||
| @@ -447,14 +426,11 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| for _, beacon := range BEACONS.Beacons { | for _, beacon := range BEACONS.Beacons { | ||||
| if len(beacon.beacon_metrics) == 0 { | if len(beacon.beacon_metrics) == 0 { | ||||
| ////fmt.Printf("beacon_metrics = 0:\n") | |||||
| continue | continue | ||||
| } | } | ||||
| if (int64(time.Now().Unix()) - (beacon.beacon_metrics[len(beacon.beacon_metrics)-1].timestamp)) > settings.Last_seen_threshold { | 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" { | if beacon.expired_location == "expired" { | ||||
| //beacon.Location_confidence = - 1 | |||||
| continue | continue | ||||
| } else { | } else { | ||||
| beacon.expired_location = "expired" | beacon.expired_location = "expired" | ||||
| @@ -469,7 +445,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| log.Printf("error: %v", err) | log.Printf("error: %v", err) | ||||
| } | } | ||||
| // Send the newly received message to the broadcast channel | |||||
| broadcast <- msg | broadcast <- msg | ||||
| } | } | ||||
| } else { | } else { | ||||
| @@ -479,7 +454,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| best_location := Best_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) | loc_list := make(map[string]float64) | ||||
| seen_weight := 1.5 | seen_weight := 1.5 | ||||
| rssi_weight := 0.75 | rssi_weight := 0.75 | ||||
| @@ -492,8 +466,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| } | } | ||||
| loc_list[metric.location] = loc | 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 := "" | best_name := "" | ||||
| ts := 0.0 | ts := 0.0 | ||||
| for name, times_seen := range loc_list { | for name, times_seen := range loc_list { | ||||
| @@ -504,16 +476,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| } | } | ||||
| /////fmt.Printf("BEST LOCATION FOR %s IS: %s with score: %f\n", beacon.Name, best_name, ts) | /////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} | 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) | beacon.Location_history = append(beacon.Location_history, best_location.name) | ||||
| if len(beacon.Location_history) > 10 { | if len(beacon.Location_history) > 10 { | ||||
| beacon.Location_history = beacon.Location_history[1:] // manteniamo solo gli ultimi 10 | beacon.Location_history = beacon.Location_history[1:] // manteniamo solo gli ultimi 10 | ||||
| @@ -534,7 +496,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| } | } | ||||
| } | } | ||||
| // Applichiamo un filtro: consideriamo il cambio solo se almeno 7 su 10 votano per una location | |||||
| if max_count >= 7 { | if max_count >= 7 { | ||||
| beacon.Previous_location = most_common_location | beacon.Previous_location = most_common_location | ||||
| if most_common_location == beacon.Previous_confident_location { | if most_common_location == beacon.Previous_confident_location { | ||||
| @@ -558,11 +519,7 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| r.Location = best_location.name | r.Location = best_location.name | ||||
| r.Last_seen = best_location.last_seen | 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" { | 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 | should_persist = true | ||||
| fmt.Printf("detected a change!!! %#v\n\n", beacon) | fmt.Printf("detected a change!!! %#v\n\n", beacon) | ||||
| if beacon.Previous_confident_location == "expired" && beacon.expired_location == "" { | if beacon.Previous_confident_location == "expired" && beacon.expired_location == "" { | ||||
| @@ -577,7 +534,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| log.Printf("error: %v", err) | log.Printf("error: %v", err) | ||||
| } | } | ||||
| // Send the newly received message to the broadcast channel | |||||
| broadcast <- msg | broadcast <- msg | ||||
| } | } | ||||
| beacon.Location_confidence = 0 | beacon.Location_confidence = 0 | ||||
| @@ -646,10 +602,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *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{ | err := cl.Publish(&client.PublishOptions{ | ||||
| QoS: mqtt.QoS0, | QoS: mqtt.QoS0, | ||||
| TopicName: []byte("afa-systems/presence"), | TopicName: []byte("afa-systems/presence"), | ||||
| @@ -661,10 +613,6 @@ func getLikelyLocations(settings Settings, locations_list Locations_list, cl *cl | |||||
| } | } | ||||
| /*for _, button := range Buttons_list { | |||||
| http_results.Buttons = append(http_results.Buttons, button) | |||||
| }*/ | |||||
| if should_persist { | if should_persist { | ||||
| persistBeacons() | persistBeacons() | ||||
| } | } | ||||
| @@ -737,38 +685,10 @@ func IncomingMQTTProcessor(updateInterval time.Duration, cl *client.Client, db * | |||||
| log.Fatal(err) | 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) | Latest_beacons_list = make(map[string]Beacon) | ||||
| Buttons_list = make(map[string]Button) | Buttons_list = make(map[string]Button) | ||||
| //create a map of locations, looked up by hostnames | |||||
| locations_list := Locations_list{} | locations_list := Locations_list{} | ||||
| ls := make(map[string]Location) | ls := make(map[string]Location) | ||||
| locations_list.locations = ls | locations_list.locations = ls | ||||
| @@ -793,18 +713,11 @@ func IncomingMQTTProcessor(updateInterval time.Duration, cl *client.Client, db * | |||||
| this_beacon_id := getBeaconID(incoming) | this_beacon_id := getBeaconID(incoming) | ||||
| now := time.Now().Unix() | 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] | beacon, ok := BEACONS.Beacons[this_beacon_id] | ||||
| if !ok { | if !ok { | ||||
| //should be unique | |||||
| //if it's already in list, forget it. | |||||
| latest_list_lock.Lock() | latest_list_lock.Lock() | ||||
| x, ok := Latest_beacons_list[this_beacon_id] | x, ok := Latest_beacons_list[this_beacon_id] | ||||
| if ok { | if ok { | ||||
| //update its timestamp | |||||
| x.Last_seen = now | x.Last_seen = now | ||||
| x.Incoming_JSON = incoming | x.Incoming_JSON = incoming | ||||
| x.Distance = getBeaconDistance(incoming) | x.Distance = getBeaconDistance(incoming) | ||||
| @@ -1,107 +0,0 @@ | |||||
| package main | |||||
| import ( | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "log" | |||||
| "os" | |||||
| "os/signal" | |||||
| "strconv" | |||||
| "strings" | |||||
| "time" | |||||
| //"./utils" | |||||
| "github.com/yosssi/gmq/mqtt" | |||||
| "github.com/yosssi/gmq/mqtt/client" | |||||
| ) | |||||
| func main() { | |||||
| sigc := make(chan os.Signal, 1) | |||||
| signal.Notify(sigc, os.Interrupt, os.Kill) | |||||
| incoming_updates_chan := IncomingMQTTProcessor(1*time.Second, cli, db, loggers) | |||||
| 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 []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 { | |||||
| 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) | |||||
| } | |||||
| } | |||||
| @@ -10,6 +10,8 @@ type Config struct { | |||||
| MQTTPass string | MQTTPass string | ||||
| MQTTClientID string | MQTTClientID string | ||||
| DBPath string | DBPath string | ||||
| KafkaURL string | |||||
| RedisURL string | |||||
| } | } | ||||
| // getEnv returns env var value or a default if not set. | // getEnv returns env var value or a default if not set. | ||||
| @@ -29,5 +31,7 @@ func Load() *Config { | |||||
| MQTTPass: getEnv("MQTT_PASSWORD", "sandbox2024"), | MQTTPass: getEnv("MQTT_PASSWORD", "sandbox2024"), | ||||
| MQTTClientID: getEnv("MQTT_CLIENT_ID", "presence-detector"), | MQTTClientID: getEnv("MQTT_CLIENT_ID", "presence-detector"), | ||||
| DBPath: getEnv("DB_PATH", "/data/conf/presence/presence.db"), | DBPath: getEnv("DB_PATH", "/data/conf/presence/presence.db"), | ||||
| KafkaURL: getEnv("KAFKA_URL", "127.0.0.1:9092"), | |||||
| RedisURL: getEnv("REDIS_URL", "127.0.0.1:6379"), | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,27 @@ | |||||
| package kafka | |||||
| import ( | |||||
| "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "github.com/segmentio/kafka-go" | |||||
| ) | |||||
| func Consume[T any](r *kafka.Reader, ch chan<- T) { | |||||
| for { | |||||
| msg, err := r.ReadMessage(context.Background()) | |||||
| if err != nil { | |||||
| fmt.Println("error reading message:", err) | |||||
| continue | |||||
| } | |||||
| var data T | |||||
| if err := json.Unmarshal(msg.Value, &data); err != nil { | |||||
| fmt.Println("error decoding:", err) | |||||
| continue | |||||
| } | |||||
| ch <- data | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,18 @@ | |||||
| package kafka | |||||
| import ( | |||||
| "strings" | |||||
| "github.com/segmentio/kafka-go" | |||||
| ) | |||||
| func KafkaReader(kafkaURL, topic, groupID string) *kafka.Reader { | |||||
| brokers := strings.Split(kafkaURL, ",") | |||||
| return kafka.NewReader(kafka.ReaderConfig{ | |||||
| Brokers: brokers, | |||||
| GroupID: groupID, | |||||
| Topic: topic, | |||||
| MinBytes: 1, | |||||
| MaxBytes: 10e6, | |||||
| }) | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| package kafka | |||||
| import ( | |||||
| "time" | |||||
| "github.com/segmentio/kafka-go" | |||||
| ) | |||||
| func KafkaWriter(kafkaURL, topic string) *kafka.Writer { | |||||
| return &kafka.Writer{ | |||||
| Addr: kafka.TCP(kafkaURL), | |||||
| Topic: topic, | |||||
| Balancer: &kafka.LeastBytes{}, | |||||
| BatchSize: 100, | |||||
| BatchTimeout: 10 * time.Millisecond, | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,62 @@ | |||||
| package presenseredis | |||||
| import ( | |||||
| "context" | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | |||||
| "github.com/redis/go-redis/v9" | |||||
| ) | |||||
| func LoadBeaconsList(client *redis.Client, ctx context.Context) map[string]model.Beacon { | |||||
| beaconsList, err := client.Get(ctx, "beaconsList").Result() | |||||
| beaconsMap := make(map[string]model.Beacon) | |||||
| if err == redis.Nil { | |||||
| fmt.Println("no beacons list, starting empty") | |||||
| } else if err != nil { | |||||
| fmt.Println("no connection to redis") | |||||
| } else { | |||||
| json.Unmarshal([]byte(beaconsList), &beaconsMap) | |||||
| } | |||||
| return beaconsMap | |||||
| } | |||||
| func LoadLatestList(client *redis.Client, ctx context.Context) map[string]model.Beacon { | |||||
| latestList, err := client.Get(ctx, "latestList").Result() | |||||
| latestMap := make(map[string]model.Beacon) | |||||
| if err == redis.Nil { | |||||
| fmt.Println("no beacons list, starting empty") | |||||
| } else if err != nil { | |||||
| fmt.Println("no connection to redis") | |||||
| } else { | |||||
| json.Unmarshal([]byte(latestList), &latestMap) | |||||
| } | |||||
| return latestMap | |||||
| } | |||||
| func SaveBeaconsList(appCtx *model.AppContext, client *redis.Client, ctx context.Context) { | |||||
| appCtx.Beacons.Lock.Lock() | |||||
| data, _ := json.Marshal(appCtx.Beacons.Beacons) | |||||
| appCtx.Beacons.Lock.Unlock() | |||||
| err := client.Set(ctx, "beaconsList", data, 0).Err() | |||||
| if err != nil { | |||||
| fmt.Println("error in saving to redis: ", err) | |||||
| } | |||||
| } | |||||
| func SaveLatestList(appCtx *model.AppContext, client *redis.Client, ctx context.Context) { | |||||
| appCtx.LatestList.Lock.Lock() | |||||
| data, _ := json.Marshal(appCtx.LatestList.LatestList) | |||||
| appCtx.LatestList.Lock.Unlock() | |||||
| err := client.Set(ctx, "latestList", data, 0).Err() | |||||
| if err != nil { | |||||
| fmt.Println("error in saving to redis: ", err) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| #!/bin/bash | |||||
| URL="http://127.0.0.1:1902/api/beacons" | |||||
| BEACON_ID="C3000057B9F7" | |||||
| echo "POST (create)" | |||||
| curl -s -X POST $URL \ | |||||
| -H "Content-Type: application/json" \ | |||||
| -d '{"Beacon_id":"'"$BEACON_ID"'","Name":"Beacon1","tx_power":-59,"rssi":-70}' | |||||
| echo -e "\n" | |||||
| sleep 1 | |||||
| echo "GET (list after create)" | |||||
| curl -s -X GET $URL | |||||
| echo -e "\n" | |||||
| sleep 1 | |||||
| echo "PUT (update)" | |||||
| curl -s -X PUT $URL \ | |||||
| -H "Content-Type: application/json" \ | |||||
| -d '{"Beacon_id":"'"$BEACON_ID"'","Name":"Beacon1-updated","tx_power":-60}' | |||||
| echo -e "\n" | |||||
| sleep 1 | |||||
| echo "GET (list after update)" | |||||
| curl -s -X GET $URL | |||||
| echo -e "\n" | |||||
| sleep 1 | |||||
| echo "DELETE" | |||||
| curl -s -X DELETE "$URL/$BEACON_ID" | |||||
| echo -e "\n" | |||||
| sleep 1 | |||||
| echo "GET (list after delete)" | |||||
| curl -s -X GET $URL | |||||
| echo -e "\n" | |||||