package ble import ( "encoding/binary" "fmt" "time" "mybeacon/internal/protocol" ) // Advertisement data types (EIR/AD) const ( ADTypeFlags = 0x01 ADTypeIncompleteUUID16 = 0x02 ADTypeCompleteUUID16 = 0x03 ADTypeShortenedName = 0x08 ADTypeCompleteName = 0x09 ADTypeManufacturerData = 0xFF ) // Manufacturer IDs const ( MfgApple = 0x004C MfgNordic = 0x0059 ) // AdvReport represents a parsed LE Advertising Report type AdvReport struct { EventType byte AddrType byte Addr [6]byte Data []byte RSSI int8 } // ParseLEAdvertisingReport parses LE Meta Event sub-event 0x02 // Format: subevent(1) + num_reports(1) + reports... func ParseLEAdvertisingReport(data []byte) ([]AdvReport, error) { if len(data) < 2 { return nil, fmt.Errorf("data too short") } subevent := data[0] if subevent != EVT_LE_ADVERTISING_REPORT { return nil, nil // not an advertising report } numReports := int(data[1]) if numReports == 0 { return nil, nil } reports := make([]AdvReport, 0, numReports) offset := 2 // Parse each report for i := 0; i < numReports && offset < len(data); i++ { if offset+9 > len(data) { break } report := AdvReport{ EventType: data[offset], AddrType: data[offset+1], } copy(report.Addr[:], data[offset+2:offset+8]) dataLen := int(data[offset+8]) offset += 9 if offset+dataLen > len(data) { break } report.Data = make([]byte, dataLen) copy(report.Data, data[offset:offset+dataLen]) offset += dataLen if offset < len(data) { report.RSSI = int8(data[offset]) offset++ } reports = append(reports, report) } return reports, nil } // MACString converts a Bluetooth address to string (reversed byte order) func MACString(addr [6]byte) string { return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) } // ParseManufacturerData extracts manufacturer data from advertisement // Returns: manufacturer ID, payload (without ID), found func ParseManufacturerData(advData []byte) (uint16, []byte, bool) { offset := 0 for offset < len(advData) { if offset+1 >= len(advData) { break } length := int(advData[offset]) if length == 0 { break } if offset+1+length > len(advData) { break } adType := advData[offset+1] adData := advData[offset+2 : offset+1+length] if adType == ADTypeManufacturerData && len(adData) >= 2 { mfgID := binary.LittleEndian.Uint16(adData[0:2]) return mfgID, adData[2:], true } offset += 1 + length } return 0, nil, false } // ParseIBeacon parses Apple iBeacon manufacturer data // Format: 0x02 0x15 + UUID(16) + Major(2) + Minor(2) + TxPower(1) func ParseIBeacon(payload []byte) (*protocol.IBeaconEvent, bool) { if len(payload) < 23 { return nil, false } if payload[0] != 0x02 || payload[1] != 0x15 { return nil, false } uuid := fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", payload[2], payload[3], payload[4], payload[5], payload[6], payload[7], payload[8], payload[9], payload[10], payload[11], payload[12], payload[13], payload[14], payload[15], payload[16], payload[17]) major := binary.BigEndian.Uint16(payload[18:20]) minor := binary.BigEndian.Uint16(payload[20:22]) return &protocol.IBeaconEvent{ BLEEvent: protocol.BLEEvent{ Type: protocol.EventIBeacon, TsMs: time.Now().UnixMilli(), }, UUID: uuid, Major: major, Minor: minor, }, true } // ParseMyBeaconAcc parses my-beacon accelerometer data (Nordic 0x0059) // Format: 0x01 0x15 + X(s8) + Y(s8) + Z(s8) + Bat(u8) + Temp(s8) + FF(u8) func ParseMyBeaconAcc(payload []byte) (*protocol.AccelEvent, bool) { if len(payload) < 8 { return nil, false } if payload[0] != 0x01 || payload[1] != 0x15 { return nil, false } ff := payload[7] != 0 evType := protocol.EventAccel if ff { evType = protocol.EventAccelFF } return &protocol.AccelEvent{ BLEEvent: protocol.BLEEvent{ Type: evType, TsMs: time.Now().UnixMilli(), }, X: int8(payload[2]), Y: int8(payload[3]), Z: int8(payload[4]), Bat: payload[5], Temp: int8(payload[6]), FF: ff, }, true } // ParseMyBeaconRelay parses rt_mybeacon relay data (Nordic 0x0059) // Format: 0x02 0x15 + DEADBEEF(4) + RelayMAC(6) + RelayMaj(2) + RelayMin(2) + RelayRSSI(1) + RelayBat(1) + IBMaj(2) + IBMin(2) func ParseMyBeaconRelay(payload []byte) (*protocol.RelayEvent, bool) { if len(payload) < 22 { return nil, false } if payload[0] != 0x02 || payload[1] != 0x15 { return nil, false } // Check magic DEADBEEF if payload[2] != 0xDE || payload[3] != 0xAD || payload[4] != 0xBE || payload[5] != 0xEF { return nil, false } relayMAC := fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", payload[6], payload[7], payload[8], payload[9], payload[10], payload[11]) return &protocol.RelayEvent{ BLEEvent: protocol.BLEEvent{ Type: protocol.EventRelay, TsMs: time.Now().UnixMilli(), }, RelayMAC: relayMAC, RelayMaj: binary.BigEndian.Uint16(payload[12:14]), RelayMin: binary.BigEndian.Uint16(payload[14:16]), RelayRSSI: int8(payload[16]), RelayBat: payload[17], IBMajor: binary.BigEndian.Uint16(payload[18:20]), IBMinor: binary.BigEndian.Uint16(payload[20:22]), }, true } // ParseAdvertisement parses a BLE advertisement and returns any recognized events func ParseAdvertisement(report AdvReport) interface{} { mac := MACString(report.Addr) mfgID, payload, found := ParseManufacturerData(report.Data) if !found { return nil } switch mfgID { case MfgApple: if ev, ok := ParseIBeacon(payload); ok { ev.MAC = mac ev.RSSI = report.RSSI return ev } case MfgNordic: // Try accelerometer first if ev, ok := ParseMyBeaconAcc(payload); ok { ev.MAC = mac ev.RSSI = report.RSSI return ev } // Try relay if ev, ok := ParseMyBeaconRelay(payload); ok { ev.MAC = mac ev.RSSI = report.RSSI return ev } } return nil }