diff --git a/cmd/decoder/main.go b/cmd/decoder/main.go index acfecf7..faf515f 100644 --- a/cmd/decoder/main.go +++ b/cmd/decoder/main.go @@ -3,13 +3,12 @@ package main import ( "bytes" "context" - "encoding/binary" "encoding/hex" - "encoding/json" "fmt" "strings" "github.com/AFASystems/presence/internal/pkg/common/appcontext" + "github.com/AFASystems/presence/internal/pkg/common/utils" "github.com/AFASystems/presence/internal/pkg/config" "github.com/AFASystems/presence/internal/pkg/kafkaclient" "github.com/AFASystems/presence/internal/pkg/model" @@ -73,7 +72,7 @@ func decodeBeacon(adv model.BeaconAdvertisement, appState *appcontext.AppState, beacon := strings.TrimSpace(adv.Data) id := adv.MAC if beacon == "" { - return nil // How to return error?, do I even need to return error + return nil } b, err := hex.DecodeString(beacon) @@ -81,126 +80,29 @@ func decodeBeacon(adv model.BeaconAdvertisement, appState *appcontext.AppState, 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 - } - } - - if event.ID != "" { - prevEvent, ok := appState.GetBeaconEvent(id) - appState.UpdateBeaconEvent(id, event) - if ok && bytes.Equal(prevEvent.Hash(), event.Hash()) { - return nil - } - - eMsg, err := json.Marshal(event) - if err != nil { - return err - } - - err = writer.WriteMessages(context.Background(), kafka.Message{ - Value: eMsg, - }) - - if err != nil { - return err - } - - fmt.Println("Message sent") - } - - return nil -} - -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 - } + b = utils.RemoveFlagBytes(b) - return false -} + indeces := utils.ParseADFast(b) + event := utils.LoopADStructures(b, indeces, id) -func parseIngicsState(ad []byte) model.BeaconEvent { - return model.BeaconEvent{ - Battery: uint32(binary.LittleEndian.Uint16(ad[6:8])), - Event: int(ad[8]), - Type: "Ingics", + if event.ID == "" { + return nil } -} -func checkEddystoneTLM(ad []byte) bool { - if len(ad) >= 4 && - ad[1] == 0x16 && - ad[2] == 0xAA && - ad[3] == 0xFE && - ad[4] == 0x20 { - return true + prevEvent, ok := appState.GetBeaconEvent(id) + appState.UpdateBeaconEvent(id, event) + if ok && bytes.Equal(prevEvent.Hash(), event.Hash()) { + return nil } - return false -} - -func parseEddystoneState(ad []byte) model.BeaconEvent { - return model.BeaconEvent{ - Battery: uint32(binary.BigEndian.Uint16(ad[6:8])), - Type: "Eddystone", - } -} - -// 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 + eMsg, err := event.ToJSON() + if err != nil { + return err } - return false -} - -func ParseADFast(b []byte) [][2]int { - var res [][2]int - i := 0 - - for i < len(b) { - l := int(b[i]) - if l == 0 || i+1+l > len(b) { - break - } - - res = append(res, [2]int{i, i + 1 + l}) - - i += 1 + l + if err := writer.WriteMessages(context.Background(), kafka.Message{Value: eMsg}); err != nil { + return err } - return res + return nil } diff --git a/internal/pkg/common/utils/beacons.go b/internal/pkg/common/utils/beacons.go new file mode 100644 index 0000000..81ee91a --- /dev/null +++ b/internal/pkg/common/utils/beacons.go @@ -0,0 +1,129 @@ +package utils + +import ( + "encoding/binary" + "fmt" + + "github.com/AFASystems/presence/internal/pkg/model" +) + +// ParseADFast efficiently parses Advertising Data structures +// Returns slice of [startIndex, endIndex] pairs for each AD structure +func ParseADFast(b []byte) [][2]int { + var res [][2]int + i := 0 + + for i < len(b) { + l := int(b[i]) + if l == 0 || i+1+l > len(b) { + break + } + + res = append(res, [2]int{i, i + 1 + l}) + + i += 1 + l + } + + return res +} + +// RemoveFlagBytes removes Bluetooth advertising flag bytes if present +// Some beacons include flag bytes as the first AD structure +func RemoveFlagBytes(b []byte) []byte { + if len(b) > 1 && b[1] == 0x01 { + l := int(b[0]) + if 1+l <= len(b) { + return b[1+l:] + } + } + return b +} + +// Generate event based on the Beacon type +func LoopADStructures(b []byte, i [][2]int, id string) model.BeaconEvent { + be := model.BeaconEvent{} + for _, r := range i { + ad := b[r[0]:r[1]] + if !isValidADStructure(ad) { + break + } + if checkIngics(ad) { + be = parseIngicsState(ad) + be.ID = id + be.Name = id + break + } else if checkEddystoneTLM(ad) { + be = parseEddystoneState(ad) + be.ID = id + be.Name = id + break + } else if checkMinewB7(ad) { + fmt.Println("Minew B7 vendor format") + break + } + } + + return be +} + +// IsValidADStructure validates if an AD structure is well-formed +func isValidADStructure(data []byte) bool { + if len(data) < 2 { + return false + } + + length := int(data[0]) + return length > 0 && int(length)+1 <= len(data) +} + +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 parseEddystoneState(ad []byte) model.BeaconEvent { + return model.BeaconEvent{ + Battery: uint32(binary.BigEndian.Uint16(ad[6:8])), + Type: "Eddystone", + } +} + +// 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 + } + + return false +} diff --git a/internal/pkg/model/typeMethods.go b/internal/pkg/model/typeMethods.go deleted file mode 100644 index 2c7f97c..0000000 --- a/internal/pkg/model/typeMethods.go +++ /dev/null @@ -1,16 +0,0 @@ -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 -}