| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- // 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]))
- }
|