btsw/main.go
Adriano Caloiaro 62a32c456b Initial commit
2025-08-22 12:02:19 -07:00

253 lines
6.3 KiB
Go

package main
import (
"flag"
"fmt"
"log"
"os"
"path"
"sort"
"strconv"
"strings"
"time"
"github.com/godbus/dbus/v5"
)
var (
version = "dev"
commit = "none"
date = time.Now().Format(time.RFC3339)
)
const (
bluezBusName = "org.bluez"
objectManagerIface = "org.freedesktop.DBus.ObjectManager"
deviceIface = "org.bluez.Device1"
adapterIface = "org.bluez.Adapter1"
)
type deviceInfo struct {
path dbus.ObjectPath
addr string
name string
connected bool
paired bool
}
type adapterInfo struct {
name string // e.g. "hci0"
path dbus.ObjectPath
}
func main() {
executableTable := path.Base(os.Args[0])
usage := func() {
fmt.Printf(`Usage:
%s [--adapter=hciX] <command> [args]
Commands:
adapters List all available Bluetooth adapters.
devices List paired devices on the selected adapter (default: hci0).
connect <id> Connect to a paired device by its numeric ID (from 'devices').
disconnect <id> Disconnect from a paired device by its numeric ID (from 'devices').
Options:
--adapter=hciX Specify which Bluetooth adapter to use (default: hci0).
--version Print the version.
Examples:
%s adapters # List available bluetooth adapaters
%s devices # List available devices on the chosen bluetooth adapter
%s --adapter=hci1 devices # List devics on hci1
%s connect 0 # Connect to device 0 (from 'devices' list)
%s disconnect 1 # Disconnect from device 1 (from 'devics' list)
`, executableTable, executableTable, executableTable, executableTable, executableTable, executableTable)
}
flags := flag.NewFlagSet("global", flag.ContinueOnError)
flags.Usage = usage
adapter := flags.String("adapter", "hci0", "Bluetooth adapter (e.g., hci0, hci1)")
versionFlag := flags.Bool("version", false, "Print the version")
err := flags.Parse(os.Args[1:])
if err != nil {
os.Exit(1)
}
if versionFlag != nil && *versionFlag {
fmt.Fprintf(os.Stdout, "btsw version: %s built at: %s from commit: %s\n", version, date, commit)
os.Exit(0)
}
args := flags.Args()
if len(args) < 1 {
usage()
os.Exit(0)
}
conn, err := dbus.SystemBus()
if err != nil {
log.Fatalf("Failed to connect to system bus: %v", err)
}
cmd := os.Args[1]
switch cmd {
case "adapters":
adapters, err := getAdapters(conn)
if err != nil {
log.Fatalf("Error fetching bluetooth adapters: %v", err)
}
if len(adapters) == 0 {
fmt.Println("No bluetooth adapters found.")
return
}
for i, a := range adapters {
fmt.Printf("[%d] %s\n", i, a.name)
}
case "devices":
paired, err := getPairedDevices(conn, *adapter)
if err != nil {
log.Fatalf("Error fetching paired devices: %v", err)
}
if len(paired) == 0 {
fmt.Println("No paired devices found.")
return
}
for i, d := range paired {
fmt.Printf("[%d] %s (%s) Connected=%t\n", i, displayName(d), d.addr, d.connected)
}
case "connect", "disconnect":
if len(args) < 2 {
usage()
os.Exit(1)
}
id, err := strconv.Atoi(args[1])
if err != nil {
log.Fatalf("Invalid id: %v", err)
}
paired, err := getPairedDevices(conn, *adapter)
if err != nil {
log.Fatalf("Error fetching paired devices: %v", err)
}
if id < 0 || id >= len(paired) {
log.Fatalf("Invalid device id %d", id)
}
d := paired[id]
devObj := conn.Object(bluezBusName, d.path)
if cmd == "connect" {
fmt.Printf("Connecting to %s on %s...\n", displayName(d), *adapter)
if call := devObj.Call(deviceIface+".Connect", 0); call.Err != nil {
log.Fatalf("Failed to connect: %v", call.Err)
}
fmt.Println("Connected.")
} else {
fmt.Printf("Disconnecting from %s on %s...\n", displayName(d), *adapter)
if call := devObj.Call(deviceIface+".Disconnect", 0); call.Err != nil {
log.Fatalf("Failed to disconnect: %v", call.Err)
}
fmt.Println("Disconnected.")
}
default:
usage()
os.Exit(0)
}
}
func getBool(props map[string]dbus.Variant, key string) bool {
if v, ok := props[key]; ok {
if b, ok := v.Value().(bool); ok {
return b
}
}
return false
}
func getString(props map[string]dbus.Variant, key string) string {
if v, ok := props[key]; ok {
if s, ok := v.Value().(string); ok {
return s
}
}
return ""
}
func displayName(d deviceInfo) string {
if d.name != "" {
return d.name
}
if d.addr != "" {
return d.addr
}
return string(d.path)
}
// getPairedDevices queries BlueZ for paired devices on a specific adapter
func getPairedDevices(conn *dbus.Conn, adapter string) ([]deviceInfo, error) {
obj := conn.Object(bluezBusName, dbus.ObjectPath("/"))
var managedObjects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err := obj.Call(objectManagerIface+".GetManagedObjects", 0).Store(&managedObjects); err != nil {
return nil, fmt.Errorf("GetManagedObjects failed: %w", err)
}
adapterPath := "/org/bluez/" + adapter
var paired []deviceInfo
for objPath, ifaces := range managedObjects {
if props, ok := ifaces[deviceIface]; ok {
// Only include devices under the chosen adapter
if !strings.HasPrefix(string(objPath), adapterPath) {
continue
}
if getBool(props, "Paired") {
paired = append(paired, deviceInfo{
path: objPath,
addr: getString(props, "Address"),
name: getString(props, "Name"),
connected: getBool(props, "Connected"),
paired: true,
})
}
}
}
// Ensure stable order by sorting on MAC address
sort.Slice(paired, func(i, j int) bool {
return paired[i].addr < paired[j].addr
})
return paired, nil
}
// getAdapters queries BlueZ for available adapters
func getAdapters(conn *dbus.Conn) ([]adapterInfo, error) {
obj := conn.Object(bluezBusName, dbus.ObjectPath("/"))
var managedObjects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err := obj.Call(objectManagerIface+".GetManagedObjects", 0).Store(&managedObjects); err != nil {
return nil, fmt.Errorf("GetManagedObjects failed: %w", err)
}
var adapters []adapterInfo
for objPath, ifaces := range managedObjects {
if _, ok := ifaces[adapterIface]; ok {
parts := strings.Split(string(objPath), "/")
if len(parts) > 0 {
name := parts[len(parts)-1] // e.g. "hci0"
adapters = append(adapters, adapterInfo{name: name, path: objPath})
}
}
}
sort.Slice(adapters, func(i, j int) bool {
return adapters[i].name < adapters[j].name
})
return adapters, nil
}