main.go 11 KB

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