| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- 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
- }
|