main.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. // BLE Scanner - scans for BLE advertisements via BlueZ D-Bus and publishes events via ZMQ
  2. package main
  3. import (
  4. "context"
  5. "encoding/binary"
  6. "encoding/hex"
  7. "encoding/json"
  8. "flag"
  9. "log"
  10. "os"
  11. "os/signal"
  12. "strings"
  13. "syscall"
  14. "time"
  15. "mybeacon/internal/protocol"
  16. "github.com/go-zeromq/zmq4"
  17. "github.com/godbus/dbus/v5"
  18. )
  19. const (
  20. defaultZMQAddr = "tcp://127.0.0.1:5555"
  21. // D-Bus constants
  22. bluezBus = "org.bluez"
  23. adapterInterface = "org.bluez.Adapter1"
  24. deviceInterface = "org.bluez.Device1"
  25. objectManager = "org.freedesktop.DBus.ObjectManager"
  26. propertiesIface = "org.freedesktop.DBus.Properties"
  27. )
  28. func main() {
  29. var (
  30. zmqAddr = flag.String("zmq", defaultZMQAddr, "ZMQ PUB address")
  31. adapter = flag.String("adapter", "hci0", "Bluetooth adapter")
  32. debug = flag.Bool("debug", false, "Enable debug logging")
  33. )
  34. flag.Parse()
  35. log.SetFlags(log.Ltime)
  36. log.Printf("BLE Scanner starting (adapter=%s, zmq=%s)", *adapter, *zmqAddr)
  37. ctx, cancel := context.WithCancel(context.Background())
  38. defer cancel()
  39. // Create ZMQ publisher socket
  40. publisher := zmq4.NewPub(ctx)
  41. defer publisher.Close()
  42. if err := publisher.Listen(*zmqAddr); err != nil {
  43. log.Fatalf("ZMQ listen: %v", err)
  44. }
  45. log.Printf("ZMQ PUB listening on %s", *zmqAddr)
  46. time.Sleep(100 * time.Millisecond)
  47. // Connect to system D-Bus
  48. conn, err := dbus.SystemBus()
  49. if err != nil {
  50. log.Fatalf("D-Bus connection: %v", err)
  51. }
  52. defer conn.Close()
  53. adapterPath := dbus.ObjectPath("/org/bluez/" + *adapter)
  54. // Power on adapter
  55. adapterObj := conn.Object(bluezBus, adapterPath)
  56. if err := adapterObj.Call(propertiesIface+".Set", 0, adapterInterface, "Powered", dbus.MakeVariant(true)).Err; err != nil {
  57. log.Printf("Warning: power on adapter: %v", err)
  58. }
  59. // Set discovery filter for LE devices
  60. filter := map[string]interface{}{
  61. "Transport": "le",
  62. "DuplicateData": true,
  63. }
  64. if err := adapterObj.Call(adapterInterface+".SetDiscoveryFilter", 0, filter).Err; err != nil {
  65. log.Printf("Warning: set discovery filter: %v", err)
  66. }
  67. // Subscribe to InterfacesAdded and PropertiesChanged signals
  68. if err := conn.AddMatchSignal(
  69. dbus.WithMatchObjectPath("/"),
  70. dbus.WithMatchInterface(objectManager),
  71. dbus.WithMatchMember("InterfacesAdded"),
  72. ); err != nil {
  73. log.Fatalf("Add InterfacesAdded match: %v", err)
  74. }
  75. if err := conn.AddMatchSignal(
  76. dbus.WithMatchInterface(propertiesIface),
  77. dbus.WithMatchMember("PropertiesChanged"),
  78. ); err != nil {
  79. log.Fatalf("Add PropertiesChanged match: %v", err)
  80. }
  81. // Start discovery
  82. if err := adapterObj.Call(adapterInterface+".StartDiscovery", 0).Err; err != nil {
  83. log.Fatalf("Start discovery: %v", err)
  84. }
  85. log.Printf("BLE discovery started on %s", *adapter)
  86. // Handle shutdown
  87. sigChan := make(chan os.Signal, 1)
  88. signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
  89. go func() {
  90. <-sigChan
  91. log.Println("Shutting down...")
  92. adapterObj.Call(adapterInterface+".StopDiscovery", 0)
  93. cancel()
  94. os.Exit(0)
  95. }()
  96. // Process D-Bus signals
  97. signals := make(chan *dbus.Signal, 100)
  98. conn.Signal(signals)
  99. var eventCount uint64
  100. for sig := range signals {
  101. var ev interface{}
  102. if *debug {
  103. log.Printf("Signal: %s path=%s", sig.Name, sig.Path)
  104. }
  105. switch sig.Name {
  106. case objectManager + ".InterfacesAdded":
  107. if len(sig.Body) < 2 {
  108. continue
  109. }
  110. ifaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant)
  111. if !ok {
  112. continue
  113. }
  114. props, ok := ifaces[deviceInterface]
  115. if !ok {
  116. continue
  117. }
  118. if *debug {
  119. log.Printf("InterfacesAdded: device props=%v", props)
  120. }
  121. ev = parseDeviceProperties(props, *debug)
  122. case propertiesIface + ".PropertiesChanged":
  123. if len(sig.Body) < 2 {
  124. continue
  125. }
  126. iface, ok := sig.Body[0].(string)
  127. if !ok || iface != deviceInterface {
  128. continue
  129. }
  130. props, ok := sig.Body[1].(map[string]dbus.Variant)
  131. if !ok {
  132. continue
  133. }
  134. // Get full device properties
  135. devicePath := sig.Path
  136. deviceObj := conn.Object(bluezBus, devicePath)
  137. var allProps map[string]dbus.Variant
  138. if err := deviceObj.Call(propertiesIface+".GetAll", 0, deviceInterface).Store(&allProps); err != nil {
  139. // Use partial props
  140. if *debug {
  141. log.Printf("PropertiesChanged (partial): %v", props)
  142. }
  143. ev = parseDeviceProperties(props, *debug)
  144. } else {
  145. if *debug {
  146. log.Printf("PropertiesChanged (full): addr=%v mfg=%v", allProps["Address"], allProps["ManufacturerData"])
  147. }
  148. ev = parseDeviceProperties(allProps, *debug)
  149. }
  150. }
  151. if ev == nil {
  152. continue
  153. }
  154. jsonData, err := json.Marshal(ev)
  155. if err != nil {
  156. continue
  157. }
  158. var topic string
  159. switch ev.(type) {
  160. case *protocol.IBeaconEvent:
  161. topic = "ble.ibeacon"
  162. case *protocol.AccelEvent:
  163. topic = "ble.acc"
  164. case *protocol.RelayEvent:
  165. topic = "ble.relay"
  166. default:
  167. topic = "ble.unknown"
  168. }
  169. msg := zmq4.NewMsgString(topic + " " + string(jsonData))
  170. if err := publisher.Send(msg); err != nil {
  171. log.Printf("ZMQ send error: %v", err)
  172. continue
  173. }
  174. eventCount++
  175. if *debug {
  176. log.Printf("[%s] %s", topic, string(jsonData))
  177. } else if eventCount%100 == 0 {
  178. log.Printf("[ble-scanner] %d events sent to daemon via ZMQ", eventCount)
  179. }
  180. }
  181. }
  182. func parseDeviceProperties(props map[string]dbus.Variant, debug bool) interface{} {
  183. var mac string
  184. var rssi int16
  185. manufacturerData := make(map[uint16][]byte)
  186. if v, ok := props["Address"]; ok {
  187. mac, _ = v.Value().(string)
  188. }
  189. if v, ok := props["RSSI"]; ok {
  190. rssi, _ = v.Value().(int16)
  191. }
  192. if v, ok := props["ManufacturerData"]; ok {
  193. // D-Bus returns map[uint16]dbus.Variant where each Variant contains []byte
  194. if mfgVariant, ok := v.Value().(map[uint16]dbus.Variant); ok {
  195. for companyID, dataVariant := range mfgVariant {
  196. if data, ok := dataVariant.Value().([]byte); ok {
  197. manufacturerData[companyID] = data
  198. }
  199. }
  200. }
  201. }
  202. if debug {
  203. log.Printf(" parseDeviceProperties: mac=%s rssi=%d mfgData=%v", mac, rssi, manufacturerData)
  204. }
  205. if mac == "" {
  206. return nil
  207. }
  208. // Normalize MAC address
  209. mac = strings.ToLower(strings.ReplaceAll(mac, "-", ":"))
  210. ts := time.Now().UnixMilli()
  211. if len(manufacturerData) == 0 {
  212. return nil
  213. }
  214. // Check for iBeacon (Apple company ID 0x004C)
  215. if data, ok := manufacturerData[0x004C]; ok {
  216. if debug {
  217. log.Printf(" Found Apple mfg data: %x", data)
  218. }
  219. if ev := parseIBeacon(mac, int(rssi), ts, data); ev != nil {
  220. return ev
  221. }
  222. }
  223. // Check for Nordic/custom (0x0059) - my-beacon_acc and rt_mybeacon
  224. if data, ok := manufacturerData[0x0059]; ok {
  225. if debug {
  226. log.Printf(" Found Nordic mfg data: %x", data)
  227. }
  228. // Check for acc (0x01 0x15) or relay (0x02 0x15)
  229. if len(data) >= 2 {
  230. if data[0] == 0x01 && data[1] == 0x15 {
  231. if ev := parseAccelBeacon(mac, int(rssi), ts, data); ev != nil {
  232. return ev
  233. }
  234. } else if data[0] == 0x02 && data[1] == 0x15 {
  235. if ev := parseRelayBeacon(mac, int(rssi), ts, data); ev != nil {
  236. return ev
  237. }
  238. }
  239. }
  240. }
  241. return nil
  242. }
  243. func parseIBeacon(mac string, rssi int, ts int64, data []byte) *protocol.IBeaconEvent {
  244. // iBeacon format: 0x02 0x15 [UUID 16 bytes] [Major 2 bytes] [Minor 2 bytes] [TX Power 1 byte]
  245. if len(data) < 23 || data[0] != 0x02 || data[1] != 0x15 {
  246. return nil
  247. }
  248. uuid := hex.EncodeToString(data[2:18])
  249. major := binary.BigEndian.Uint16(data[18:20])
  250. minor := binary.BigEndian.Uint16(data[20:22])
  251. return &protocol.IBeaconEvent{
  252. BLEEvent: protocol.BLEEvent{
  253. Type: protocol.EventIBeacon,
  254. MAC: mac,
  255. RSSI: int8(rssi),
  256. TsMs: ts,
  257. },
  258. UUID: uuid,
  259. Major: major,
  260. Minor: minor,
  261. }
  262. }
  263. func parseAccelBeacon(mac string, rssi int, ts int64, data []byte) *protocol.AccelEvent {
  264. // my-beacon_acc format: 0x01 0x15 [x s8] [y s8] [z s8] [bat u8] [temp s8] [ff u8]
  265. // Caller already verified data[0]=0x01, data[1]=0x15
  266. if len(data) < 8 {
  267. return nil
  268. }
  269. evType := protocol.EventAccel
  270. ff := data[7] == 0xff
  271. if ff {
  272. evType = protocol.EventAccelFF
  273. }
  274. return &protocol.AccelEvent{
  275. BLEEvent: protocol.BLEEvent{
  276. Type: evType,
  277. MAC: mac,
  278. RSSI: int8(rssi),
  279. TsMs: ts,
  280. },
  281. X: int8(data[2]),
  282. Y: int8(data[3]),
  283. Z: int8(data[4]),
  284. Bat: data[5],
  285. Temp: int8(data[6]),
  286. FF: ff,
  287. }
  288. }
  289. func parseRelayBeacon(mac string, rssi int, ts int64, data []byte) *protocol.RelayEvent {
  290. // 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]
  291. // Caller already verified data[0]=0x02, data[1]=0x15
  292. // Total: 22 bytes
  293. if len(data) < 22 {
  294. return nil
  295. }
  296. // Verify DEADBEEF magic
  297. if data[2] != 0xDE || data[3] != 0xAD || data[4] != 0xBE || data[5] != 0xEF {
  298. return nil
  299. }
  300. origMAC := formatMAC(data[6:12])
  301. relayMaj := binary.BigEndian.Uint16(data[12:14])
  302. relayMin := binary.BigEndian.Uint16(data[14:16])
  303. relayRSSI := int8(data[16])
  304. relayBat := data[17]
  305. ibMajor := binary.BigEndian.Uint16(data[18:20])
  306. ibMinor := binary.BigEndian.Uint16(data[20:22])
  307. return &protocol.RelayEvent{
  308. BLEEvent: protocol.BLEEvent{
  309. Type: protocol.EventRelay,
  310. MAC: origMAC,
  311. RSSI: relayRSSI,
  312. TsMs: ts,
  313. },
  314. RelayMAC: mac,
  315. RelayMaj: relayMaj,
  316. RelayMin: relayMin,
  317. RelayRSSI: int8(rssi),
  318. RelayBat: relayBat,
  319. IBMajor: ibMajor,
  320. IBMinor: ibMinor,
  321. }
  322. }
  323. func formatMAC(b []byte) string {
  324. if len(b) < 6 {
  325. return ""
  326. }
  327. return strings.ToLower(hex.EncodeToString(b[0:1]) + ":" +
  328. hex.EncodeToString(b[1:2]) + ":" +
  329. hex.EncodeToString(b[2:3]) + ":" +
  330. hex.EncodeToString(b[3:4]) + ":" +
  331. hex.EncodeToString(b[4:5]) + ":" +
  332. hex.EncodeToString(b[5:6]))
  333. }