Skip to content

Instantly share code, notes, and snippets.

@idkrn123
Created November 25, 2023 02:34
Show Gist options
  • Save idkrn123/fdebf29e8d98918b0824e4c321ce49f2 to your computer and use it in GitHub Desktop.
Save idkrn123/fdebf29e8d98918b0824e4c321ce49f2 to your computer and use it in GitHub Desktop.
golang program to watch for and display nearby BLE devices, their manufacturers, and signal strength. may become a repo in the future if logic gets complex enough
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"sync"
"time"
"github.com/go-ble/ble"
"github.com/go-ble/ble/linux"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
const (
apiKeyFilename = "macvendors_api_key.txt"
deviceProcessInterval = time.Second * 5
manufacturerLookupURL = "https://api.macvendors.com/v1/lookup/"
)
var (
seenDevices = make(map[string]struct{})
mutex = &sync.Mutex{}
)
func main() {
device, err := linux.NewDevice()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create BLE device: %v\n", err)
return
}
ble.SetDefaultDevice(device)
apiKey, err := ioutil.ReadFile(apiKeyFilename)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read API key: %v\n", err)
return
}
app := tview.NewApplication()
table := tview.NewTable().SetBorders(true)
setupTable(table)
exitButton := tview.NewButton(`E["underline"]x[""]it`).SetSelectedFunc(func() { app.Stop() })
flex := setupLayout(table, exitButton)
setupExitHandler(app, exitButton)
go scanBLEDevices(app, table, string(apiKey))
app.SetRoot(flex, true).SetFocus(table)
if err := app.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to run application: %v\n", err)
}
}
func setupTable(table *tview.Table) {
headers := []string{"MAC Address", "RSSI", "Manufacturer"}
for i, header := range headers {
table.SetCell(0, i, tview.NewTableCell(header).SetAlign(tview.AlignCenter))
}
}
func setupLayout(table *tview.Table, exitButton *tview.Button) *tview.Flex {
return tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(table, 0, 1, true).
AddItem(exitButton, 1, 0, false)
}
func setupExitHandler(app *tview.Application, exitButton *tview.Button) {
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyESC || event.Rune() == 'x' || event.Rune() == 'X' {
app.Stop()
return nil
}
return event
})
exitButton.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEnter {
app.Stop()
return nil
}
return event
})
}
func scanBLEDevices(app *tview.Application, table *tview.Table, apiKey string) {
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), 10*time.Second))
ble.Scan(ctx, false, func(a ble.Advertisement) {
mac := a.Addr().String()
mutex.Lock()
if _, seen := seenDevices[mac]; seen {
mutex.Unlock()
return
}
seenDevices[mac] = struct{}{}
mutex.Unlock()
row := table.GetRowCount()
app.QueueUpdateDraw(func() {
table.SetCell(row, 0, tview.NewTableCell(mac))
table.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%d", a.RSSI())))
})
go fetchManufacturer(mac, apiKey, app, table, row)
}, nil)
}
func fetchManufacturer(mac, apiKey string, app *tview.Application, table *tview.Table, row int) {
resp, err := http.Get(manufacturerLookupURL + mac)
if err != nil {
updateTableWithManufacturer(app, table, row, "Error fetching manufacturer")
return
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
updateTableWithManufacturer(app, table, row, "Error parsing JSON")
return
}
manufacturer := "Unknown Manufacturer"
if name, ok := result["companyName"].(string); ok {
manufacturer = name
}
updateTableWithManufacturer(app, table, row, manufacturer)
}
func updateTableWithManufacturer(app *tview.Application, table *tview.Table, row int, manufacturer string) {
app.QueueUpdateDraw(func() {
table.SetCell(row, 2, tview.NewTableCell(manufacturer))
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment