bluez and bluez-utils, and bluetooth.service running and enabled. On both workstations bluez is installed only as a dependency; declaring it keeps a clean-up from taking it. Nine Go tools over bluetoothctl: controller, power, devices with battery where reported, a bounded scan, connect, disconnect, trust, pair (an agent that confirms nothing, for headphones) and remove. An act whose output says it failed is an error whatever the exit status, and one bluez refuses the account is repeated through sudo -n.
136 lines
4.7 KiB
Go
136 lines
4.7 KiB
Go
// The bluetooth module's tools (novox/hq research 027/02, 026/05): the controller, the devices with
|
|
// their state and battery, scanning, and pairing, connecting, trusting and forgetting a device. A Go
|
|
// bundle the node's runtime launches over stdio (ADR 0188, ADR 0193); it runs as the operator
|
|
// account, and speaks to bluez through bluetoothctl.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
var providedBy = map[string]string{
|
|
"bluetoothctl": "the bluez-utils package, which this module installs",
|
|
}
|
|
|
|
func main() {
|
|
if err := stdio.Serve("", tools()); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
var addressArg = map[string]any{"type": "string", "description": "the device's address, such as 80:99:E7:C2:29:DA, as bluetooth_devices answers it"}
|
|
|
|
func withAddress(f func(string) (any, error)) func(map[string]any) (any, error) {
|
|
return func(args map[string]any) (any, error) {
|
|
a, err := addressOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f(a)
|
|
}
|
|
}
|
|
|
|
func tools() []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{
|
|
Name: "bluetooth_controller",
|
|
Description: "The machine's Bluetooth controller: address, name, powered, discoverable, pairable, discovering. (r)",
|
|
Input: map[string]any{},
|
|
Run: func(map[string]any) (any, error) { return Controller() },
|
|
},
|
|
{
|
|
Name: "bluetooth_power",
|
|
Description: "Whether the controller is powered; with on, switch it on or off. (r/a)",
|
|
Input: map[string]any{"on": map[string]any{"type": "boolean", "description": "power the controller on (true) or off (false)"}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
if _, given := args["on"]; !given {
|
|
c, err := Controller()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"powered": c.Powered}, nil
|
|
}
|
|
on, err := optFlag(args, "on", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Power(on)
|
|
},
|
|
},
|
|
{
|
|
Name: "bluetooth_devices",
|
|
Description: "The devices bluez knows: every one, or only the paired, connected or trusted. Each with its " +
|
|
"name, kind, paired, bonded, trusted, blocked, connected, and its battery where the device reports it. (r)",
|
|
Input: map[string]any{"which": map[string]any{"type": "string", "enum": []string{"all", "paired", "connected", "trusted"}, "description": "which devices (default all)"}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
which, err := optText(args, "which", "all")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Devices(which)
|
|
},
|
|
},
|
|
{
|
|
Name: "bluetooth_scan",
|
|
Description: "Discover devices nearby for a few seconds (default 8, at most 15), and answer the ones not " +
|
|
"paired, with their signal strength. (r)",
|
|
Input: map[string]any{"seconds": map[string]any{"type": "integer", "description": "how long to scan (default 8, at most 15)"}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
s, err := optWhole(args, "seconds", 8, 1, 15)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Scan(s)
|
|
},
|
|
},
|
|
{
|
|
Name: "bluetooth_connect",
|
|
Description: "Connect a paired device, such as headphones. Answers its state afterwards. (a)",
|
|
Input: map[string]any{"address": addressArg},
|
|
Run: withAddress(func(a string) (any, error) { return Act("connect", a) }),
|
|
},
|
|
{
|
|
Name: "bluetooth_disconnect",
|
|
Description: "Disconnect a device. (a)",
|
|
Input: map[string]any{"address": addressArg},
|
|
Run: withAddress(func(a string) (any, error) { return Act("disconnect", a) }),
|
|
},
|
|
{
|
|
Name: "bluetooth_trust",
|
|
Description: "Trust a device, so it may connect by itself; or with trusted false, stop trusting it. (a)",
|
|
Input: map[string]any{"address": addressArg, "trusted": map[string]any{"type": "boolean", "description": "trust (default) or untrust"}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
a, err := addressOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
trusted, err := optFlag(args, "trusted", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if trusted {
|
|
return Act("trust", a)
|
|
}
|
|
return Act("untrust", a)
|
|
},
|
|
},
|
|
{
|
|
Name: "bluetooth_pair",
|
|
Description: "Pair a device found by a scan, and trust it. Works for a device that needs no code to be " +
|
|
"confirmed, such as headphones; one that shows a code is paired from the desktop. (a)",
|
|
Input: map[string]any{"address": addressArg},
|
|
Run: withAddress(func(a string) (any, error) { return Pair(a) }),
|
|
},
|
|
{
|
|
Name: "bluetooth_remove",
|
|
Description: "Forget a device: unpair it and drop what bluez knows of it. (a)",
|
|
Input: map[string]any{"address": addressArg},
|
|
Run: withAddress(func(a string) (any, error) { return Act("remove", a) }),
|
|
},
|
|
}
|
|
}
|