// BLE Scanner - scans for BLE advertisements via BlueZ D-Bus and publishes events via ZMQ package main import ( "context" "encoding/binary" "encoding/hex" "encoding/json" "flag" "log" "os" "os/signal" "strings" "syscall" "time" "mybeacon/internal/protocol" "github.com/go-zeromq/zmq4" "github.com/godbus/dbus/v5" ) const ( defaultZMQAddr = "tcp://127.0.0.1:5555" // D-Bus constants bluezBus = "org.bluez" adapterInterface = "org.bluez.Adapter1" deviceInterface = "org.bluez.Device1" objectManager = "org.freedesktop.DBus.ObjectManager" propertiesIface = "org.freedesktop.DBus.Properties" ) func main() { var ( zmqAddr = flag.String("zmq", defaultZMQAddr, "ZMQ PUB address") adapter = flag.String("adapter", "hci0", "Bluetooth adapter") debug = flag.Bool("debug", false, "Enable debug logging") ) flag.Parse() log.SetFlags(log.Ltime) log.Printf("BLE Scanner starting (adapter=%s, zmq=%s)", *adapter, *zmqAddr) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Create ZMQ publisher socket publisher := zmq4.NewPub(ctx) defer publisher.Close() if err := publisher.Listen(*zmqAddr); err != nil { log.Fatalf("ZMQ listen: %v", err) } log.Printf("ZMQ PUB listening on %s", *zmqAddr) time.Sleep(100 * time.Millisecond) // Connect to system D-Bus conn, err := dbus.SystemBus() if err != nil { log.Fatalf("D-Bus connection: %v", err) } defer conn.Close() adapterPath := dbus.ObjectPath("/org/bluez/" + *adapter) // Power on adapter adapterObj := conn.Object(bluezBus, adapterPath) if err := adapterObj.Call(propertiesIface+".Set", 0, adapterInterface, "Powered", dbus.MakeVariant(true)).Err; err != nil { log.Printf("Warning: power on adapter: %v", err) } // Set discovery filter for LE devices filter := map[string]interface{}{ "Transport": "le", "DuplicateData": true, } if err := adapterObj.Call(adapterInterface+".SetDiscoveryFilter", 0, filter).Err; err != nil { log.Printf("Warning: set discovery filter: %v", err) } // Subscribe to InterfacesAdded and PropertiesChanged signals if err := conn.AddMatchSignal( dbus.WithMatchObjectPath("/"), dbus.WithMatchInterface(objectManager), dbus.WithMatchMember("InterfacesAdded"), ); err != nil { log.Fatalf("Add InterfacesAdded match: %v", err) } if err := conn.AddMatchSignal( dbus.WithMatchInterface(propertiesIface), dbus.WithMatchMember("PropertiesChanged"), ); err != nil { log.Fatalf("Add PropertiesChanged match: %v", err) } // Start discovery if err := adapterObj.Call(adapterInterface+".StartDiscovery", 0).Err; err != nil { log.Fatalf("Start discovery: %v", err) } log.Printf("BLE discovery started on %s", *adapter) // Handle shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan log.Println("Shutting down...") adapterObj.Call(adapterInterface+".StopDiscovery", 0) cancel() os.Exit(0) }() // Process D-Bus signals signals := make(chan *dbus.Signal, 100) conn.Signal(signals) var eventCount uint64 for sig := range signals { var ev interface{} if *debug { log.Printf("Signal: %s path=%s", sig.Name, sig.Path) } switch sig.Name { case objectManager + ".InterfacesAdded": if len(sig.Body) < 2 { continue } ifaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant) if !ok { continue } props, ok := ifaces[deviceInterface] if !ok { continue } if *debug { log.Printf("InterfacesAdded: device props=%v", props) } ev = parseDeviceProperties(props, *debug) case propertiesIface + ".PropertiesChanged": if len(sig.Body) < 2 { continue } iface, ok := sig.Body[0].(string) if !ok || iface != deviceInterface { continue } props, ok := sig.Body[1].(map[string]dbus.Variant) if !ok { continue } // Get full device properties devicePath := sig.Path deviceObj := conn.Object(bluezBus, devicePath) var allProps map[string]dbus.Variant if err := deviceObj.Call(propertiesIface+".GetAll", 0, deviceInterface).Store(&allProps); err != nil { // Use partial props if *debug { log.Printf("PropertiesChanged (partial): %v", props) } ev = parseDeviceProperties(props, *debug) } else { if *debug { log.Printf("PropertiesChanged (full): addr=%v mfg=%v", allProps["Address"], allProps["ManufacturerData"]) } ev = parseDeviceProperties(allProps, *debug) } } if ev == nil { continue } jsonData, err := json.Marshal(ev) if err != nil { continue } var topic string switch ev.(type) { case *protocol.IBeaconEvent: topic = "ble.ibeacon" case *protocol.AccelEvent: topic = "ble.acc" case *protocol.RelayEvent: topic = "ble.relay" default: topic = "ble.unknown" } msg := zmq4.NewMsgString(topic + " " + string(jsonData)) if err := publisher.Send(msg); err != nil { log.Printf("ZMQ send error: %v", err) continue } eventCount++ if *debug { log.Printf("[%s] %s", topic, string(jsonData)) } else if eventCount%100 == 0 { log.Printf("[ble-scanner] %d events sent to daemon via ZMQ", eventCount) } } } func parseDeviceProperties(props map[string]dbus.Variant, debug bool) interface{} { var mac string var rssi int16 manufacturerData := make(map[uint16][]byte) if v, ok := props["Address"]; ok { mac, _ = v.Value().(string) } if v, ok := props["RSSI"]; ok { rssi, _ = v.Value().(int16) } if v, ok := props["ManufacturerData"]; ok { // D-Bus returns map[uint16]dbus.Variant where each Variant contains []byte if mfgVariant, ok := v.Value().(map[uint16]dbus.Variant); ok { for companyID, dataVariant := range mfgVariant { if data, ok := dataVariant.Value().([]byte); ok { manufacturerData[companyID] = data } } } } if debug { log.Printf(" parseDeviceProperties: mac=%s rssi=%d mfgData=%v", mac, rssi, manufacturerData) } if mac == "" { return nil } // Normalize MAC address mac = strings.ToLower(strings.ReplaceAll(mac, "-", ":")) ts := time.Now().UnixMilli() if len(manufacturerData) == 0 { return nil } // Check for iBeacon (Apple company ID 0x004C) if data, ok := manufacturerData[0x004C]; ok { if debug { log.Printf(" Found Apple mfg data: %x", data) } if ev := parseIBeacon(mac, int(rssi), ts, data); ev != nil { return ev } } // Check for Nordic/custom (0x0059) - my-beacon_acc and rt_mybeacon if data, ok := manufacturerData[0x0059]; ok { if debug { log.Printf(" Found Nordic mfg data: %x", data) } // Check for acc (0x01 0x15) or relay (0x02 0x15) if len(data) >= 2 { if data[0] == 0x01 && data[1] == 0x15 { if ev := parseAccelBeacon(mac, int(rssi), ts, data); ev != nil { return ev } } else if data[0] == 0x02 && data[1] == 0x15 { if ev := parseRelayBeacon(mac, int(rssi), ts, data); ev != nil { return ev } } } } return nil } func parseIBeacon(mac string, rssi int, ts int64, data []byte) *protocol.IBeaconEvent { // iBeacon format: 0x02 0x15 [UUID 16 bytes] [Major 2 bytes] [Minor 2 bytes] [TX Power 1 byte] if len(data) < 23 || data[0] != 0x02 || data[1] != 0x15 { return nil } uuid := hex.EncodeToString(data[2:18]) major := binary.BigEndian.Uint16(data[18:20]) minor := binary.BigEndian.Uint16(data[20:22]) return &protocol.IBeaconEvent{ BLEEvent: protocol.BLEEvent{ Type: protocol.EventIBeacon, MAC: mac, RSSI: int8(rssi), TsMs: ts, }, UUID: uuid, Major: major, Minor: minor, } } func parseAccelBeacon(mac string, rssi int, ts int64, data []byte) *protocol.AccelEvent { // my-beacon_acc format: 0x01 0x15 [x s8] [y s8] [z s8] [bat u8] [temp s8] [ff u8] // Caller already verified data[0]=0x01, data[1]=0x15 if len(data) < 8 { return nil } evType := protocol.EventAccel ff := data[7] == 0xff if ff { evType = protocol.EventAccelFF } return &protocol.AccelEvent{ BLEEvent: protocol.BLEEvent{ Type: evType, MAC: mac, RSSI: int8(rssi), TsMs: ts, }, X: int8(data[2]), Y: int8(data[3]), Z: int8(data[4]), Bat: data[5], Temp: int8(data[6]), FF: ff, } } func parseRelayBeacon(mac string, rssi int, ts int64, data []byte) *protocol.RelayEvent { // rt_mybeacon format: 0x02 0x15 DE AD BE EF [mac 6] [maj 2] [min 2] [rssi 1] [bat 1] [ib_maj 2] [ib_min 2] // Caller already verified data[0]=0x02, data[1]=0x15 // Total: 22 bytes if len(data) < 22 { return nil } // Verify DEADBEEF magic if data[2] != 0xDE || data[3] != 0xAD || data[4] != 0xBE || data[5] != 0xEF { return nil } origMAC := formatMAC(data[6:12]) relayMaj := binary.BigEndian.Uint16(data[12:14]) relayMin := binary.BigEndian.Uint16(data[14:16]) relayRSSI := int8(data[16]) relayBat := data[17] ibMajor := binary.BigEndian.Uint16(data[18:20]) ibMinor := binary.BigEndian.Uint16(data[20:22]) return &protocol.RelayEvent{ BLEEvent: protocol.BLEEvent{ Type: protocol.EventRelay, MAC: origMAC, RSSI: relayRSSI, TsMs: ts, }, RelayMAC: mac, RelayMaj: relayMaj, RelayMin: relayMin, RelayRSSI: int8(rssi), RelayBat: relayBat, IBMajor: ibMajor, IBMinor: ibMinor, } } func formatMAC(b []byte) string { if len(b) < 6 { return "" } return strings.ToLower(hex.EncodeToString(b[0:1]) + ":" + hex.EncodeToString(b[1:2]) + ":" + hex.EncodeToString(b[2:3]) + ":" + hex.EncodeToString(b[3:4]) + ":" + hex.EncodeToString(b[4:5]) + ":" + hex.EncodeToString(b[5:6])) }