blob: 126c5a075aae552b071d00756baf1b895733d211 [file] [log] [blame]
package main
import (
"bufio"
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/go-ble/ble"
"github.com/go-ble/ble/examples/lib"
"github.com/go-ble/ble/examples/lib/dev"
"github.com/go-ble/ble/linux"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var curr struct {
device ble.Device
client ble.Client
clients map[string]ble.Client
uuid ble.UUID
addr ble.Addr
profile *ble.Profile
}
var (
errNotConnected = fmt.Errorf("not connected")
errNoProfile = fmt.Errorf("no profile")
errNoUUID = fmt.Errorf("no UUID")
errInvalidUUID = fmt.Errorf("invalid UUID")
)
func main() {
curr.clients = make(map[string]ble.Client)
app := cli.NewApp()
app.Name = "blesh"
app.Usage = "A CLI tool for ble"
app.Version = "0.0.1"
app.Action = cli.ShowAppHelp
app.Commands = []cli.Command{
{
Name: "status",
Aliases: []string{"st"},
Usage: "Display current status",
Before: setup,
Action: cmdStatus,
},
{
Name: "adv",
Aliases: []string{"a"},
Usage: "Advertise name, UUIDs, iBeacon (TODO)",
Before: setup,
Action: cmdAdv,
Flags: []cli.Flag{flgTimeout, flgName},
},
{
Name: "serve",
Aliases: []string{"sv"},
Usage: "Start the GATT Server",
Before: setup,
Action: cmdServe,
Flags: []cli.Flag{flgTimeout, flgName},
},
{
Name: "scan",
Aliases: []string{"s"},
Usage: "Scan surrounding with specified filter",
Before: setup,
Action: cmdScan,
Flags: []cli.Flag{flgTimeout, flgName, flgAddr, flgSvc, flgAllowDup},
},
{
Name: "connect",
Aliases: []string{"c"},
Usage: "Connect to a peripheral device",
Before: setup,
Action: cmdConnect,
Flags: []cli.Flag{flgTimeout, flgName, flgAddr, flgSvc},
},
{
Name: "disconnect",
Aliases: []string{"x"},
Usage: "Disconnect a connected peripheral device",
Before: setup,
Action: cmdDisconnect,
Flags: []cli.Flag{flgAddr},
},
{
Name: "discover",
Aliases: []string{"d"},
Usage: "Discover profile on connected device",
Before: setup,
Action: cmdDiscover,
Flags: []cli.Flag{flgTimeout, flgName, flgAddr},
},
{
Name: "explore",
Aliases: []string{"e"},
Usage: "Display discovered profile",
Before: setup,
Action: cmdExplore,
Flags: []cli.Flag{flgTimeout, flgName, flgAddr},
},
{
Name: "read",
Aliases: []string{"r"},
Usage: "Read value from a characteristic or descriptor",
Before: setup,
Action: cmdRead,
Flags: []cli.Flag{flgUUID, flgTimeout, flgName, flgAddr},
},
{
Name: "write",
Aliases: []string{"w"},
Usage: "Write value to a characteristic or descriptor",
Before: setup,
Action: cmdWrite,
Flags: []cli.Flag{flgUUID, flgTimeout, flgName, flgAddr},
},
{
Name: "sub",
Usage: "Subscribe to notification (or indication)",
Before: setup,
Action: cmdSub,
Flags: []cli.Flag{flgUUID, flgInd, flgTimeout, flgName, flgAddr},
},
{
Name: "unsub",
Usage: "Unsubscribe to notification (or indication)",
Before: setup,
Action: cmdUnsub,
Flags: []cli.Flag{flgUUID, flgInd, flgAddr},
},
{
Name: "shell",
Aliases: []string{"sh"},
Usage: "Enter interactive mode",
Before: setup,
Action: func(c *cli.Context) { cmdShell(app) },
},
}
// app.Before = setup
app.Run(os.Args)
}
func setup(c *cli.Context) error {
if curr.device != nil {
return nil
}
fmt.Printf("Initializing device ...\n")
d, err := dev.NewDevice("default")
if err != nil {
return errors.Wrap(err, "can't new device")
}
ble.SetDefaultDevice(d)
curr.device = d
// Optinal. Demostrate changing HCI parameters on Linux.
if dev, ok := d.(*linux.Device); ok {
return errors.Wrap(updateLinuxParam(dev), "can't update hci parameters")
}
return nil
}
func cmdStatus(c *cli.Context) error {
m := map[bool]string{true: "yes", false: "no"}
fmt.Printf("Current status:\n")
fmt.Printf(" Initialized: %s\n", m[curr.device != nil])
if curr.addr != nil {
fmt.Printf(" Address: %s\n", curr.addr)
} else {
fmt.Printf(" Address:\n")
}
fmt.Printf(" Connected:")
for k := range curr.clients {
fmt.Printf(" %s", k)
}
fmt.Printf("\n")
fmt.Printf(" Profile:\n")
if curr.profile != nil {
fmt.Printf("\n")
explore(curr.client, curr.profile)
}
if curr.uuid != nil {
fmt.Printf(" UUID: %s\n", curr.uuid)
} else {
fmt.Printf(" UUID:\n")
}
return nil
}
func cmdAdv(c *cli.Context) error {
fmt.Printf("Advertising for %s...\n", c.Duration("tmo"))
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), c.Duration("tmo")))
return chkErr(ble.AdvertiseNameAndServices(ctx, "Gopher"))
}
func cmdScan(c *cli.Context) error {
fmt.Printf("Scanning for %s...\n", c.Duration("tmo"))
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), c.Duration("tmo")))
return chkErr(ble.Scan(ctx, c.Bool("dup"), advHandler, filter(c)))
}
func cmdServe(c *cli.Context) error {
testSvc := ble.NewService(lib.TestSvcUUID)
testSvc.AddCharacteristic(lib.NewCountChar())
testSvc.AddCharacteristic(lib.NewEchoChar())
if err := ble.AddService(testSvc); err != nil {
return errors.Wrap(err, "can't add service")
}
fmt.Printf("Serving GATT Server for %s...\n", c.Duration("tmo"))
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), c.Duration("tmo")))
return chkErr(ble.AdvertiseNameAndServices(ctx, "Gopher", testSvc.UUID))
}
func cmdConnect(c *cli.Context) error {
curr.client = nil
var cln ble.Client
var err error
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), c.Duration("tmo")))
if c.String("addr") != "" {
curr.addr = ble.NewAddr(c.String("addr"))
fmt.Printf("Dialing to specified address: %s\n", curr.addr)
cln, err = ble.Dial(ctx, curr.addr)
} else if filter(c) != nil {
fmt.Printf("Scanning with filter...\n")
if cln, err = ble.Connect(ctx, filter(c)); err == nil {
curr.addr = cln.Addr()
fmt.Printf("Connected to %s\n", curr.addr)
}
} else if curr.addr != nil {
fmt.Printf("Dialing to implicit address: %s\n", curr.addr)
cln, err = ble.Dial(ctx, curr.addr)
} else {
return fmt.Errorf("no filter specified, and cached peripheral address")
}
if err == nil {
curr.client = cln
curr.clients[cln.Addr().String()] = cln
go func() {
<-cln.Disconnected()
delete(curr.clients, cln.Addr().String())
curr.client = nil
fmt.Printf("\n%s disconnected\n", cln.Addr().String())
}()
}
return err
}
func cmdDisconnect(c *cli.Context) error {
if c.String("addr") != "" {
curr.client = curr.clients[c.String("addr")]
}
if curr.client == nil {
return errNotConnected
}
defer func() {
delete(curr.clients, curr.client.Addr().String())
curr.client = nil
curr.profile = nil
}()
fmt.Printf("Disconnecting [ %s ]... (this might take up to few seconds on OS X)\n", curr.client.Addr())
return curr.client.CancelConnection()
}
func cmdDiscover(c *cli.Context) error {
curr.profile = nil
if curr.client == nil {
if err := cmdConnect(c); err != nil {
return errors.Wrap(err, "can't connect")
}
}
fmt.Printf("Discovering profile...\n")
p, err := curr.client.DiscoverProfile(true)
if err != nil {
return errors.Wrap(err, "can't discover profile")
}
curr.profile = p
return nil
}
func cmdExplore(c *cli.Context) error {
if curr.client == nil {
if err := cmdConnect(c); err != nil {
return errors.Wrap(err, "can't connect")
}
}
if curr.profile == nil {
if err := cmdDiscover(c); err != nil {
return errors.Wrap(err, "can't discover profile")
}
}
return explore(curr.client, curr.profile)
}
func cmdRead(c *cli.Context) error {
if err := doGetUUID(c); err != nil {
return err
}
if err := doConnect(c); err != nil {
return err
}
if err := doDiscover(c); err != nil {
return err
}
if u := curr.profile.Find(ble.NewCharacteristic(curr.uuid)); u != nil {
b, err := curr.client.ReadCharacteristic(u.(*ble.Characteristic))
if err != nil {
return errors.Wrap(err, "can't read characteristic")
}
fmt.Printf(" Value %x | %q\n", b, b)
return nil
}
if u := curr.profile.Find(ble.NewDescriptor(curr.uuid)); u != nil {
b, err := curr.client.ReadDescriptor(u.(*ble.Descriptor))
if err != nil {
return errors.Wrap(err, "can't read descriptor")
}
fmt.Printf(" Value %x | %q\n", b, b)
return nil
}
return errNoUUID
}
func cmdWrite(c *cli.Context) error {
if err := doGetUUID(c); err != nil {
return err
}
if err := doConnect(c); err != nil {
return err
}
if err := doDiscover(c); err != nil {
return err
}
if u := curr.profile.Find(ble.NewCharacteristic(curr.uuid)); u != nil {
err := curr.client.WriteCharacteristic(u.(*ble.Characteristic), []byte("hello"), true)
return errors.Wrap(err, "can't write characteristic")
}
if u := curr.profile.Find(ble.NewDescriptor(curr.uuid)); u != nil {
err := curr.client.WriteDescriptor(u.(*ble.Descriptor), []byte("fixme"))
return errors.Wrap(err, "can't write descriptor")
}
return errNoUUID
}
func cmdSub(c *cli.Context) error {
if err := doGetUUID(c); err != nil {
return err
}
if err := doConnect(c); err != nil {
return err
}
if err := doDiscover(c); err != nil {
return err
}
// NotificationHandler
h := func(req []byte) { fmt.Printf("notified: %x | %q\n", req, req) }
if u := curr.profile.Find(ble.NewCharacteristic(curr.uuid)); u != nil {
err := curr.client.Subscribe(u.(*ble.Characteristic), c.Bool("ind"), h)
return errors.Wrap(err, "can't subscribe to characteristic")
}
return errNoUUID
}
func cmdUnsub(c *cli.Context) error {
if err := doGetUUID(c); err != nil {
return err
}
if err := doConnect(c); err != nil {
return err
}
if u := curr.profile.Find(ble.NewCharacteristic(curr.uuid)); u != nil {
err := curr.client.Unsubscribe(u.(*ble.Characteristic), c.Bool("ind"))
return errors.Wrap(err, "can't unsubscribe to characteristic")
}
return errNoUUID
}
func cmdShell(app *cli.App) {
cli.OsExiter = func(c int) {}
reader := bufio.NewReader(os.Stdin)
sigs := make(chan os.Signal, 1)
go func() {
for range sigs {
fmt.Printf("\n(type quit or q to exit)\n\nblesh >")
}
}()
defer close(sigs)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
for {
fmt.Print("blesh > ")
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
if text == "" {
continue
}
if text == "quit" || text == "q" {
break
}
app.Run(append(os.Args[1:], strings.Split(text, " ")...))
}
signal.Stop(sigs)
}