go-catprinter/ble.go
2024-09-10 22:45:30 +02:00

136 lines
3.4 KiB
Go

package catprinter
import (
"context"
"github.com/go-ble/ble"
"github.com/pkg/errors"
"strconv"
"strings"
"time"
)
// For some reason, bleak reports the 0xaf30 service on my macOS, while it reports
// 0xae30 (which I believe is correct) on my Raspberry Pi. This hacky workaround
// should cover both cases.
var possibleServiceUuids = []string{
"ae30",
"af30",
}
const txCharacteristicUuid = "ae01"
const scanTimeout = 10 * time.Second
const waitAfterEachChunkS = 20 * time.Millisecond
const waitAfterDataSentS = 30 * time.Second
func chunkify(data []byte, chunkSize int) [][]byte {
var chunks [][]byte
for i := 0; i < len(data); i += chunkSize {
end := i + chunkSize
if end > len(data) {
end = len(data)
}
chunks = append(chunks, data[i:end])
}
return chunks
}
func (c *Client) writeData(data []byte) error {
chunks := chunkify(data, c.chunkSize)
c.log("Sending %d chunks of size %d...", len(chunks), c.chunkSize)
for i, chunk := range chunks {
err := c.printer.WriteCharacteristic(c.characteristic, chunk, true)
if err != nil {
return errors.Wrap(err, "writing to characteristic, chunk "+strconv.Itoa(i))
}
time.Sleep(waitAfterEachChunkS)
}
c.log("All sent.")
return nil
}
// ScanDevices scans for a device with the given name, or auto discovers it based on characteristics (not implemented yet).
// Returns a map with MACs as key, and device names as values.
func (c *Client) ScanDevices(name string) (map[string]string, error) {
var devices = make(map[string]string)
var found []string
c.log("Looking for a BLE device named %s", name)
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), c.Timeout))
err := ble.Scan(ctx, true, func(a ble.Advertisement) {
if strings.Contains(strings.Join(found, " "), a.Addr().String()) {
return
}
found = append(found, a.Addr().String())
if strings.Contains(strings.ToLower(a.LocalName()), strings.ToLower(name)) {
devices[a.Addr().String()] = a.LocalName()
c.log("Matches %s %s", a.Addr().String(), a.LocalName())
return
}
c.log("No match %s %s", a.Addr().String(), a.LocalName())
}, nil)
switch errors.Cause(err) {
case nil:
case context.DeadlineExceeded:
c.log("Timeout: scan completed")
case context.Canceled:
c.log("Scan was canceled")
default:
return nil, errors.Wrap(err, "failed scan")
}
if len(devices) < 0 {
return nil, ErrPrinterNotFound
}
return devices, nil
}
// Connect establishes a BLE connection to a printer by MAC address.
func (c *Client) Connect(mac string) error {
ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), c.Timeout))
connect, err := ble.Dial(ctx, ble.NewAddr(mac))
if err != nil {
return err
}
profile, err := connect.DiscoverProfile(true)
if err != nil {
return errors.Wrap(err, "discovering profile")
}
var char *ble.Characteristic
for _, service := range profile.Services {
c.log("service %s", service.UUID.String())
for _, characteristic := range service.Characteristics {
c.log(" %s", characteristic.UUID.String())
if characteristic.UUID.Equal(ble.MustParse(txCharacteristicUuid)) &&
strings.Contains(strings.Join(possibleServiceUuids, " "), service.UUID.String()) {
c.log(" found characteristic!")
char = characteristic
break
}
}
}
if char == nil {
return ErrMissingCharacteristic
}
c.characteristic = char
c.printer = connect
c.chunkSize = c.printer.Conn().RxMTU() - 3
return nil
}