| @@ -0,0 +1,32 @@ | |||||
| Ingics iBS01G | |||||
| id: C83F8F17DB35 | |||||
| 020106 12 FF590080BC240100FFFFFFFF000000000000 | |||||
| id: C83F8F17DB35 | |||||
| 020106 12 FF590080BC240100FFFFFFFF000000000000 | |||||
| type 0x16 - service Data | |||||
| Minew B7 | |||||
| id: C300003947C4 | |||||
| 020106 0303E1FF 1216E1FFA1031AFFFEFEFB0000C447390000C3 | |||||
| id: C300003947C4 | |||||
| 0201061AFF4C000215FDA50693A4E24FB1AFCFC6EB0764782500000000EC - iBeacon | |||||
| id: C300003947C4 | |||||
| 020106 0303AAFE 1516AAFE00E800112233445566778899ABCDE7280002 - eddystone | |||||
| id: C300003947C4 | |||||
| 0201060303E1FF1216E1FFA1031AFFFEFEFB0000C447390000C3 | |||||
| id: C300003947C4 | |||||
| 0201060303E1FF0E16E1FFA1081AC447390000C34237 | |||||
| Minew MWB01 | |||||
| id: C7AE561E38B7 | |||||
| 02010617FF0001000000000000000000005F0700006F4C0000640003095336 | |||||
| id: C7AE561E38B7 | |||||
| 02010617FF00020000FF0000FF0000FF0006001D005200000B200803095336 | |||||
| Minew MWC01 | |||||
| id: E01F9A7A47D2 | |||||
| 02010617FF00020000FF0000FF0000FF0006001D005200000A242D03095332 | |||||
| id: E01F9A7A47D2 | |||||
| 02010617FF000100000000000000000000780700006F4C0000640003095332 | |||||
| @@ -1,16 +1,22 @@ | |||||
| package main | package main | ||||
| import ( | import ( | ||||
| "bytes" | |||||
| "context" | |||||
| "encoding/binary" | |||||
| "encoding/hex" | "encoding/hex" | ||||
| "encoding/json" | |||||
| "fmt" | "fmt" | ||||
| "math" | "math" | ||||
| "strconv" | "strconv" | ||||
| "strings" | |||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/config" | "github.com/AFASystems/presence/internal/pkg/config" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | "github.com/AFASystems/presence/internal/pkg/kafkaclient" | ||||
| "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" | ||||
| "github.com/segmentio/kafka-go" | |||||
| ) | ) | ||||
| func main() { | func main() { | ||||
| @@ -19,9 +25,6 @@ func main() { | |||||
| Beacons: model.BeaconsList{ | Beacons: model.BeaconsList{ | ||||
| Beacons: make(map[string]model.Beacon), | Beacons: make(map[string]model.Beacon), | ||||
| }, | }, | ||||
| LatestList: model.LatestBeaconsList{ | |||||
| LatestList: make(map[string]model.Beacon), | |||||
| }, | |||||
| Settings: model.Settings{ | Settings: model.Settings{ | ||||
| Settings: model.SettingsVal{ | Settings: model.SettingsVal{ | ||||
| Location_confidence: 4, | Location_confidence: 4, | ||||
| @@ -31,10 +34,12 @@ func main() { | |||||
| HA_send_changes_only: false, | HA_send_changes_only: false, | ||||
| }, | }, | ||||
| }, | }, | ||||
| BeaconEvents: model.BeaconEventList{ | |||||
| Beacons: make(map[string]model.BeaconEvent), | |||||
| }, | |||||
| BeaconsLookup: make(map[string]struct{}), | |||||
| } | } | ||||
| fmt.Println("init") | |||||
| cfg := config.Load() | cfg := config.Load() | ||||
| // Kafka reader for Raw MQTT beacons | // Kafka reader for Raw MQTT beacons | ||||
| @@ -42,8 +47,13 @@ func main() { | |||||
| defer rawReader.Close() | defer rawReader.Close() | ||||
| // Kafka reader for API server updates | // Kafka reader for API server updates | ||||
| // apiReader := kafkaclient.KafkaReader(cfg.KafkaURL, "apibeacons", "gid-api") | |||||
| // defer apiReader.Close() | |||||
| apiReader := kafkaclient.KafkaReader(cfg.KafkaURL, "apibeacons", "gid-api") | |||||
| defer apiReader.Close() | |||||
| alertWriter := kafkaclient.KafkaWriter(cfg.KafkaURL, "alertbeacons") | |||||
| defer alertWriter.Close() | |||||
| fmt.Println("Decoder initialized, subscribed to Kafka topics") | |||||
| // // Kafka reader for latest list updates | // // Kafka reader for latest list updates | ||||
| // latestReader := kafkaclient.KafkaReader(cfg.KafkaURL, "latestbeacons", "gid-latest") | // latestReader := kafkaclient.KafkaReader(cfg.KafkaURL, "latestbeacons", "gid-latest") | ||||
| @@ -55,36 +65,27 @@ func main() { | |||||
| // 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) | ||||
| // chSettings := make(chan model.SettingsVal, 10) | // chSettings := make(chan model.SettingsVal, 10) | ||||
| go kafkaclient.Consume(rawReader, chRaw) | go kafkaclient.Consume(rawReader, chRaw) | ||||
| // go kafkaclient.Consume(apiReader, chApi) | |||||
| go kafkaclient.Consume(apiReader, chApi) | |||||
| // go kafkaclient.Consume(latestReader, chLatest) | // go kafkaclient.Consume(latestReader, chLatest) | ||||
| // go kafkaclient.Consume(settingsReader, chSettings) | // go kafkaclient.Consume(settingsReader, chSettings) | ||||
| for { | for { | ||||
| select { | select { | ||||
| case msg := <-chRaw: | case msg := <-chRaw: | ||||
| processIncoming(msg, &appCtx) | |||||
| // case msg := <-chApi: | |||||
| // switch msg.Method { | |||||
| // case "POST": | |||||
| // fmt.Println("Incoming POST") | |||||
| // appCtx.Beacons.Lock.Lock() | |||||
| // appCtx.Beacons.Beacons[msg.Beacon.Beacon_id] = msg.Beacon | |||||
| // case "DELETE": | |||||
| // fmt.Println("Incoming delete") | |||||
| // _, exists := appCtx.Beacons.Beacons[msg.ID] | |||||
| // if exists { | |||||
| // appCtx.Beacons.Lock.Lock() | |||||
| // delete(appCtx.Beacons.Beacons, msg.ID) | |||||
| // } | |||||
| // default: | |||||
| // fmt.Println("unknown method: ", msg.Method) | |||||
| // } | |||||
| // appCtx.Beacons.Lock.Unlock() | |||||
| processIncoming(msg, &appCtx, alertWriter) | |||||
| case msg := <-chApi: | |||||
| switch msg.Method { | |||||
| case "POST": | |||||
| id := msg.Beacon.Beacon_id | |||||
| appCtx.BeaconsLookup[id] = struct{}{} | |||||
| case "DELETE": | |||||
| fmt.Println("Incoming delete message") | |||||
| } | |||||
| // case msg := <-chLatest: | // case msg := <-chLatest: | ||||
| // fmt.Println("latest msg: ", msg) | // fmt.Println("latest msg: ", msg) | ||||
| // case msg := <-chSettings: | // case msg := <-chSettings: | ||||
| @@ -96,44 +97,136 @@ func main() { | |||||
| } | } | ||||
| } | } | ||||
| func processIncoming(incoming model.Incoming_json, ctx *model.AppContext) { | |||||
| defer func() { | |||||
| if err := recover(); err != nil { | |||||
| fmt.Println("work failed:", err) | |||||
| } | |||||
| }() | |||||
| func processIncoming(incoming model.Incoming_json, ctx *model.AppContext, writer *kafka.Writer) { | |||||
| id := mqttclient.GetBeaconID(incoming) | |||||
| _, ok := ctx.BeaconsLookup[id] | |||||
| if !ok { | |||||
| return | |||||
| } | |||||
| // Get ID | |||||
| err := decodeBeacon(incoming, ctx, writer) | |||||
| if err != nil { | |||||
| fmt.Println("error in decoding") | |||||
| return | |||||
| } | |||||
| } | |||||
| func decodeBeacon(incoming model.Incoming_json, ctx *model.AppContext, writer *kafka.Writer) error { | |||||
| beacon := strings.TrimSpace(incoming.Data) | |||||
| id := mqttclient.GetBeaconID(incoming) | id := mqttclient.GetBeaconID(incoming) | ||||
| fmt.Println(incoming.Data) | |||||
| if beacon == "" { | |||||
| return nil // How to return error?, do I even need to return error | |||||
| } | |||||
| b, err := hex.DecodeString(beacon) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| // check for flag byte, if first AD structure is flag bytes, remove it | |||||
| if len(b) > 1 && b[1] == 0x01 { | |||||
| l := int(b[0]) // length of AD structure | |||||
| if 1+l <= len(b) { | |||||
| b = b[1+l:] | |||||
| } | |||||
| } | |||||
| adStructureIndeces := ParseADFast(b) | |||||
| event := model.BeaconEvent{} | |||||
| for _, r := range adStructureIndeces { | |||||
| ad := b[r[0]:r[1]] | |||||
| if checkIngics(ad) { | |||||
| event = parseIngicsState(ad) | |||||
| event.Id = id | |||||
| event.Name = id | |||||
| break | |||||
| } else if checkEddystoneTLM(ad) { | |||||
| event = parseEddystoneState(ad) | |||||
| event.Id = id | |||||
| event.Name = id | |||||
| break | |||||
| } else if checkMinewB7(ad) { | |||||
| fmt.Println("Minew B7 vendor format") | |||||
| break | |||||
| } | |||||
| } | |||||
| beacons := &ctx.Beacons | |||||
| if event.Id != "" { | |||||
| prevEvent, ok := ctx.BeaconEvents.Beacons[id] | |||||
| ctx.BeaconEvents.Beacons[id] = event | |||||
| if ok && bytes.Equal(prevEvent.Hash(), event.Hash()) { | |||||
| return nil | |||||
| } | |||||
| beacons.Lock.Lock() | |||||
| defer beacons.Lock.Unlock() | |||||
| eMsg, err := json.Marshal(event) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| incoming = mqttclient.IncomingBeaconFilter(incoming) | |||||
| err = writer.WriteMessages(context.Background(), kafka.Message{ | |||||
| Value: eMsg, | |||||
| }) | |||||
| beacon, exists := beacons.Beacons[id] | |||||
| if !exists { | |||||
| return | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| fmt.Println("Message sent") | |||||
| } | } | ||||
| fmt.Printf("%+v\n", beacon) | |||||
| return nil | |||||
| } | |||||
| updateBeacon(&beacon, incoming) | |||||
| beacons.Beacons[id] = beacon | |||||
| func checkIngics(ad []byte) bool { | |||||
| if len(ad) >= 6 && | |||||
| ad[1] == 0xFF && | |||||
| ad[2] == 0x59 && | |||||
| ad[3] == 0x00 && | |||||
| ad[4] == 0x80 && | |||||
| ad[5] == 0xBC { | |||||
| return true | |||||
| } | |||||
| return false | |||||
| } | |||||
| func parseIngicsState(ad []byte) model.BeaconEvent { | |||||
| return model.BeaconEvent{ | |||||
| Battery: uint32(binary.LittleEndian.Uint16(ad[6:8])), | |||||
| Event: int(ad[8]), | |||||
| Type: "Ingics", | |||||
| } | |||||
| } | |||||
| func checkEddystoneTLM(ad []byte) bool { | |||||
| if len(ad) >= 4 && | |||||
| ad[1] == 0x16 && | |||||
| ad[2] == 0xAA && | |||||
| ad[3] == 0xFE && | |||||
| ad[4] == 0x20 { | |||||
| return true | |||||
| } | |||||
| return false | |||||
| } | } | ||||
| func processBeacon(hexStr string) { | |||||
| b, _ := hex.DecodeString(hexStr) | |||||
| func parseEddystoneState(ad []byte) model.BeaconEvent { | |||||
| return model.BeaconEvent{ | |||||
| Battery: uint32(binary.BigEndian.Uint16(ad[6:8])), | |||||
| Type: "Eddystone", | |||||
| } | |||||
| } | |||||
| if len(b) > 2 && b[0] == 0x02 && b[1] == 0x01 { | |||||
| b = b[2+int(b[0]):] | |||||
| // I dont think this is always true, but for testing is ok | |||||
| func checkMinewB7(ad []byte) bool { | |||||
| if len(ad) >= 4 && | |||||
| ad[1] == 0x16 && | |||||
| ad[2] == 0xE1 && | |||||
| ad[3] == 0xFF { | |||||
| return true | |||||
| } | } | ||||
| ads := ParseADFast(b) | |||||
| _ = ads | |||||
| return false | |||||
| } | } | ||||
| func ParseADFast(b []byte) [][2]int { | func ParseADFast(b []byte) [][2]int { | ||||
| @@ -1,150 +0,0 @@ | |||||
| package main | |||||
| import ( | |||||
| "context" | |||||
| "time" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | |||||
| presenseredis "github.com/AFASystems/presence/internal/pkg/redis" | |||||
| "github.com/redis/go-redis/v9" | |||||
| ) | |||||
| func main() { | |||||
| ctx := context.Background() | |||||
| client := redis.NewClient(&redis.Options{ | |||||
| Addr: "127.0.0.1:6379", | |||||
| Password: "", | |||||
| }) | |||||
| getLikelyLocations(client, ctx) | |||||
| } | |||||
| func getLikelyLocations(client *redis.Client, ctx context.Context) { | |||||
| httpRes := model.HTTPResultsList{ | |||||
| HTTPResults: model.HTTPLocationsList{ | |||||
| Beacons: make([]model.HTTPLocation, 0), | |||||
| }, | |||||
| } | |||||
| shouldPersist := false | |||||
| beaconsList := presenseredis.LoadBeaconsList(client, ctx) | |||||
| settings := presenseredis.LoadSettings(client, ctx) | |||||
| for _, beacon := range beaconsList { | |||||
| length := len(beacon.Beacon_metrics) | |||||
| if length == 0 { | |||||
| continue | |||||
| } | |||||
| if (int64(time.Now().Unix()) - (beacon.Beacon_metrics[length-1].Timestamp)) > settings.Last_seen_threshold { | |||||
| if beacon.Expired_location == "expired" { | |||||
| continue | |||||
| } | |||||
| beacon.Expired_location = "expired" // define type expired | |||||
| } else { | |||||
| beacon.Expired_location = "" | |||||
| } | |||||
| locList := make(map[string]float64) | |||||
| seenWeight := 1.5 | |||||
| rssiWeight := 0.75 | |||||
| for _, metric := range beacon.Beacon_metrics { | |||||
| weightCalc := seenWeight + (rssiWeight * (1.0 - (float64(metric.Rssi) / -100.0))) | |||||
| loc, ok := locList[metric.Location] | |||||
| if !ok { | |||||
| loc = weightCalc | |||||
| } else { | |||||
| loc = loc + weightCalc | |||||
| } | |||||
| } | |||||
| bestName := "" | |||||
| ts := 0.0 | |||||
| for name, timesSeen := range locList { | |||||
| if timesSeen > ts { | |||||
| bestName = name | |||||
| ts = timesSeen | |||||
| } | |||||
| } | |||||
| bestLocation := model.BestLocation{Name: bestName, Distance: beacon.Beacon_metrics[length-1].Distance, Last_seen: beacon.Beacon_metrics[length-1].Timestamp} | |||||
| beacon.Location_history = append(beacon.Location_history, bestName) | |||||
| if len(beacon.Location_history) > 10 { | |||||
| beacon.Location_history = beacon.Location_history[1:] | |||||
| } | |||||
| locationCounts := make(map[string]int) | |||||
| for _, loc := range beacon.Location_history { | |||||
| locationCounts[loc]++ | |||||
| } | |||||
| maxCount := 0 | |||||
| mostCommonLocation := "" | |||||
| for loc, count := range locationCounts { | |||||
| if count > maxCount { | |||||
| maxCount = count | |||||
| mostCommonLocation = loc | |||||
| } | |||||
| } | |||||
| if maxCount >= 7 { | |||||
| beacon.Previous_location = mostCommonLocation | |||||
| if mostCommonLocation == beacon.Previous_confident_location { | |||||
| beacon.Location_confidence++ | |||||
| } else { | |||||
| beacon.Location_confidence = 1 | |||||
| beacon.Previous_confident_location = mostCommonLocation | |||||
| } | |||||
| } | |||||
| r := model.HTTPLocation{ | |||||
| Distance: bestLocation.Distance, | |||||
| Name: beacon.Name, | |||||
| Beacon_name: beacon.Name, | |||||
| Beacon_id: beacon.Beacon_id, | |||||
| Beacon_type: beacon.Beacon_type, | |||||
| HB_Battery: beacon.HB_Battery, | |||||
| HB_ButtonMode: beacon.HB_ButtonMode, | |||||
| HB_ButtonCounter: beacon.HB_ButtonCounter, | |||||
| Location: bestName, | |||||
| Last_seen: bestLocation.Last_seen, | |||||
| } | |||||
| if (beacon.Location_confidence == settings.Location_confidence && beacon.Previous_confident_location != bestLocation.Name) || (beacon.Expired_location == "expired") { | |||||
| shouldPersist = true | |||||
| beacon.Location_confidence = 0 | |||||
| location := "" | |||||
| if beacon.Expired_location == "expired" { | |||||
| location = "expired" | |||||
| } else { | |||||
| location = bestName | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| // 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 | |||||
| */ | |||||
| @@ -1,188 +0,0 @@ | |||||
| package main | |||||
| import ( | |||||
| "encoding/json" | |||||
| "fmt" | |||||
| "log" | |||||
| "os" | |||||
| "os/signal" | |||||
| "strconv" | |||||
| "strings" | |||||
| "time" | |||||
| "github.com/AFASystems/presence/internal/pkg/config" | |||||
| "github.com/AFASystems/presence/internal/pkg/httpserver" | |||||
| "github.com/AFASystems/presence/internal/pkg/model" | |||||
| "github.com/AFASystems/presence/internal/pkg/mqttclient" | |||||
| "github.com/AFASystems/presence/internal/pkg/persistence" | |||||
| "github.com/boltdb/bolt" | |||||
| "github.com/gorilla/websocket" | |||||
| "github.com/yosssi/gmq/mqtt" | |||||
| "github.com/yosssi/gmq/mqtt/client" | |||||
| ) | |||||
| func main() { | |||||
| sigc := make(chan os.Signal, 1) | |||||
| signal.Notify(sigc, os.Interrupt) | |||||
| cfg := config.Load() | |||||
| fmt.Println("hello world") | |||||
| db, err := bolt.Open("presence.db", 0644, nil) | |||||
| if err != nil { | |||||
| log.Fatal(err) | |||||
| } | |||||
| defer db.Close() | |||||
| model.Db = db | |||||
| cli := client.New(&client.Options{ | |||||
| ErrorHandler: func(err error) { | |||||
| fmt.Println(err) | |||||
| }, | |||||
| }) | |||||
| defer cli.Terminate() | |||||
| fmt.Println("host: ", cfg.MQTTHost, " Client ID: ", cfg.MQTTClientID, "user: ", cfg.MQTTUser) | |||||
| err = cli.Connect(&client.ConnectOptions{ | |||||
| Network: "tcp", | |||||
| Address: cfg.MQTTHost, | |||||
| ClientID: []byte(cfg.MQTTClientID), | |||||
| UserName: []byte(cfg.MQTTUser), | |||||
| Password: []byte(cfg.MQTTPass), | |||||
| }) | |||||
| if err != nil { | |||||
| fmt.Println("Error comes from here") | |||||
| panic(err) | |||||
| } | |||||
| ctx := &model.AppContext{ | |||||
| HTTPResults: model.HTTPResultsList{ | |||||
| HTTPResults: model.HTTPLocationsList{Beacons: []model.HTTPLocation{}}, | |||||
| }, | |||||
| Beacons: model.BeaconsList{ | |||||
| Beacons: make(map[string]model.Beacon), | |||||
| }, | |||||
| ButtonsList: make(map[string]model.Button), | |||||
| Settings: model.Settings{ | |||||
| Location_confidence: 4, | |||||
| Last_seen_threshold: 15, | |||||
| Beacon_metrics_size: 30, | |||||
| HA_send_interval: 5, | |||||
| HA_send_changes_only: false, | |||||
| }, | |||||
| Clients: make(map[*websocket.Conn]bool), | |||||
| Broadcast: make(chan model.Message, 100), | |||||
| Locations: model.LocationsList{Locations: make(map[string]model.Location)}, | |||||
| LatestList: model.LatestBeaconsList{LatestList: make(map[string]model.Beacon)}, | |||||
| } | |||||
| persistence.LoadState(model.Db, ctx) | |||||
| incomingChan := mqttclient.IncomingMQTTProcessor(1*time.Second, cli, model.Db, ctx) | |||||
| err = cli.Subscribe(&client.SubscribeOptions{ | |||||
| SubReqs: []*client.SubReq{ | |||||
| &client.SubReq{ | |||||
| TopicFilter: []byte("publish_out/#"), | |||||
| QoS: mqtt.QoS0, | |||||
| Handler: func(topicName, message []byte) { | |||||
| msgStr := string(message) | |||||
| t := strings.Split(string(topicName), "/") | |||||
| hostname := t[1] | |||||
| fmt.Println("hostname: ", hostname) | |||||
| if strings.HasPrefix(msgStr, "[") { | |||||
| var readings []model.RawReading | |||||
| err := json.Unmarshal(message, &readings) | |||||
| if err != nil { | |||||
| log.Printf("Error parsing JSON: %v", err) | |||||
| return | |||||
| } | |||||
| for _, reading := range readings { | |||||
| if reading.Type == "Gateway" { | |||||
| continue | |||||
| } | |||||
| incoming := model.Incoming_json{ | |||||
| Hostname: hostname, | |||||
| MAC: reading.MAC, | |||||
| RSSI: int64(reading.RSSI), | |||||
| Data: reading.RawData, | |||||
| HB_ButtonCounter: parseButtonState(reading.RawData), | |||||
| } | |||||
| incomingChan <- incoming | |||||
| } | |||||
| } else { | |||||
| s := strings.Split(string(message), ",") | |||||
| if len(s) < 6 { | |||||
| log.Printf("Messaggio CSV non valido: %s", msgStr) | |||||
| return | |||||
| } | |||||
| rawdata := s[4] | |||||
| buttonCounter := parseButtonState(rawdata) | |||||
| if buttonCounter > 0 { | |||||
| incoming := model.Incoming_json{} | |||||
| i, _ := strconv.ParseInt(s[3], 10, 64) | |||||
| incoming.Hostname = hostname | |||||
| incoming.Beacon_type = "hb_button" | |||||
| incoming.MAC = s[1] | |||||
| incoming.RSSI = i | |||||
| incoming.Data = rawdata | |||||
| incoming.HB_ButtonCounter = buttonCounter | |||||
| read_line := strings.TrimRight(string(s[5]), "\r\n") | |||||
| it, err33 := strconv.Atoi(read_line) | |||||
| if err33 != nil { | |||||
| fmt.Println(it) | |||||
| fmt.Println(err33) | |||||
| os.Exit(2) | |||||
| } | |||||
| incomingChan <- incoming | |||||
| } | |||||
| } | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| fmt.Println("CONNECTED TO MQTT") | |||||
| fmt.Println("\n ") | |||||
| fmt.Println("Visit http://" + cfg.HTTPAddr + " on your browser to see the web interface") | |||||
| fmt.Println("\n ") | |||||
| go httpserver.StartHTTPServer(cfg.HTTPAddr, ctx) | |||||
| <-sigc | |||||
| if err := cli.Disconnect(); err != nil { | |||||
| panic(err) | |||||
| } | |||||
| } | |||||
| func parseButtonState(raw string) int64 { | |||||
| raw = strings.ToUpper(raw) | |||||
| if strings.HasPrefix(raw, "0201060303E1FF12") && len(raw) >= 38 { | |||||
| buttonField := raw[34:38] | |||||
| if buttonValue, err := strconv.ParseInt(buttonField, 16, 64); err == nil { | |||||
| return buttonValue | |||||
| } | |||||
| } | |||||
| if strings.HasPrefix(raw, "02010612FF590") && len(raw) >= 24 { | |||||
| counterField := raw[22:24] | |||||
| buttonState, err := strconv.ParseInt(counterField, 16, 64) | |||||
| if err == nil { | |||||
| return buttonState | |||||
| } | |||||
| } | |||||
| return 0 | |||||
| } | |||||
| @@ -32,7 +32,7 @@ func HttpServer(addr string) { | |||||
| methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}) | methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}) | ||||
| // Kafka writer that relays messages | // Kafka writer that relays messages | ||||
| writer := kafkaclient.KafkaWriter("kafka:9092", "apibeacons") | |||||
| writer := kafkaclient.KafkaWriter("127.0.0.1:9092", "apibeacons") | |||||
| defer writer.Close() | defer writer.Close() | ||||
| settingsWriter := kafkaclient.KafkaWriter("kafka:9092", "settings") | settingsWriter := kafkaclient.KafkaWriter("kafka:9092", "settings") | ||||
| @@ -145,14 +145,14 @@ func beaconsAddHandler(writer *kafka.Writer) http.HandlerFunc { | |||||
| } | } | ||||
| } | } | ||||
| func beaconsListHandler(client *redis.Client) http.HandlerFunc { | |||||
| return func(w http.ResponseWriter, r *http.Request) { | |||||
| } | |||||
| } | |||||
| // func beaconsListHandler(client *redis.Client) http.HandlerFunc { | |||||
| // return func(w http.ResponseWriter, r *http.Request) { | |||||
| // } | |||||
| // } | |||||
| func settingsListHandler(client *redis.Client) http.HandlerFunc { | |||||
| return func(w http.ResponseWriter, r *http.Request) {} | |||||
| } | |||||
| // func settingsListHandler(client *redis.Client) http.HandlerFunc { | |||||
| // return func(w http.ResponseWriter, r *http.Request) {} | |||||
| // } | |||||
| func settingsEditHandler(writer *kafka.Writer) http.HandlerFunc { | func settingsEditHandler(writer *kafka.Writer) http.HandlerFunc { | ||||
| return func(w http.ResponseWriter, r *http.Request) { | return func(w http.ResponseWriter, r *http.Request) { | ||||
| @@ -0,0 +1,16 @@ | |||||
| package model | |||||
| import ( | |||||
| "crypto/sha256" | |||||
| "fmt" | |||||
| ) | |||||
| func (b BeaconEvent) Hash() []byte { | |||||
| rBatt := (b.Battery / 10) * 10 | |||||
| c := fmt.Sprintf("%d%d%s%s%s", rBatt, b.Event, b.Id, b.Name, b.Type) | |||||
| h := sha256.New() | |||||
| h.Write([]byte(c)) | |||||
| bs := h.Sum(nil) | |||||
| return bs | |||||
| } | |||||
| @@ -127,6 +127,14 @@ type Beacon struct { | |||||
| HB_ButtonMode string `json:"hb_button_mode"` | HB_ButtonMode string `json:"hb_button_mode"` | ||||
| } | } | ||||
| type BeaconEvent struct { | |||||
| Name string | |||||
| Id string | |||||
| Type string | |||||
| Battery uint32 | |||||
| Event int | |||||
| } | |||||
| // Button represents a hardware button beacon device. | // Button represents a hardware button beacon device. | ||||
| type Button struct { | type Button struct { | ||||
| Name string `json:"name"` | Name string `json:"name"` | ||||
| @@ -149,6 +157,11 @@ type BeaconsList struct { | |||||
| Lock sync.RWMutex | Lock sync.RWMutex | ||||
| } | } | ||||
| type BeaconEventList struct { | |||||
| Beacons map[string]BeaconEvent | |||||
| Lock sync.RWMutex | |||||
| } | |||||
| // LocationsList holds all known locations with concurrency protection. | // LocationsList holds all known locations with concurrency protection. | ||||
| type LocationsList struct { | type LocationsList struct { | ||||
| Locations map[string]Location | Locations map[string]Location | ||||
| @@ -186,14 +199,16 @@ type HTTPResultsList struct { | |||||
| } | } | ||||
| type AppContext struct { | type AppContext struct { | ||||
| HTTPResults HTTPResultsList | |||||
| Beacons BeaconsList | |||||
| ButtonsList map[string]Button | |||||
| Settings Settings | |||||
| Broadcast chan Message | |||||
| Locations LocationsList | |||||
| LatestList LatestBeaconsList | |||||
| Clients map[*websocket.Conn]bool | |||||
| HTTPResults HTTPResultsList | |||||
| Beacons BeaconsList | |||||
| ButtonsList map[string]Button | |||||
| Settings Settings | |||||
| Broadcast chan Message | |||||
| Locations LocationsList | |||||
| LatestList LatestBeaconsList | |||||
| Clients map[*websocket.Conn]bool | |||||
| BeaconsLookup map[string]struct{} | |||||
| BeaconEvents BeaconEventList | |||||
| } | } | ||||
| type ApiUpdate struct { | type ApiUpdate struct { | ||||
| @@ -1,6 +1,6 @@ | |||||
| #!/bin/bash | #!/bin/bash | ||||
| URL="http://127.0.0.1:1902/api/beacons" | URL="http://127.0.0.1:1902/api/beacons" | ||||
| BEACON_ID="E017085443A7" | |||||
| BEACON_ID="C83F8F17DB35" | |||||
| echo "POST (create)" | echo "POST (create)" | ||||
| curl -s -X POST $URL \ | curl -s -X POST $URL \ | ||||
| @@ -10,32 +10,6 @@ echo -e "\n" | |||||
| sleep 1 | 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" | echo "DELETE" | ||||
| curl -s -X DELETE "$URL/$BEACON_ID" | curl -s -X DELETE "$URL/$BEACON_ID" | ||||
| echo -e "\n" | echo -e "\n" | ||||
| sleep 1 | |||||
| echo "GET (list after delete)" | |||||
| curl -s -X GET $URL | |||||
| echo -e "\n" | |||||