241 lines
5.9 KiB
Go
241 lines
5.9 KiB
Go
package blefs
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/muka/go-bluetooth/bluez"
|
|
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
|
)
|
|
|
|
var (
|
|
ErrFSUnexpectedResponse = errors.New("unexpected response returned by filesystem")
|
|
ErrFSResponseTimeout = errors.New("timed out waiting for response")
|
|
ErrFSError = errors.New("error reported by filesystem")
|
|
)
|
|
|
|
const (
|
|
FSStatusOk = 0x01
|
|
FSStatusError = 0x02
|
|
)
|
|
|
|
// Filesystem command
|
|
const (
|
|
FSCmdReadFile = 0x10
|
|
FSCmdDataReq = 0x12
|
|
FSCmdWriteFile = 0x20
|
|
FSCmdTransfer = 0x22
|
|
FSCmdDelete = 0x30
|
|
FSCmdMkdir = 0x40
|
|
FSCmdListDir = 0x50
|
|
FSCmdMove = 0x60
|
|
)
|
|
|
|
// Filesystem response
|
|
const (
|
|
FSResponseReadFile = 0x11
|
|
FSResponseWriteFile = 0x21
|
|
FSResponseDelete = 0x31
|
|
FSResponseMkdir = 0x41
|
|
FSResponseListDir = 0x51
|
|
FSResponseMove = 0x61
|
|
)
|
|
|
|
// btOptsCmd cause a write command rather than a wrire request
|
|
var btOptsCmd = map[string]interface{}{"type": "command"}
|
|
|
|
// FS implements the fs.FS interface for the Adafruit BLE FS protocol
|
|
type FS struct {
|
|
transferChar *gatt.GattCharacteristic1
|
|
transferRespCh <-chan *bluez.PropertyChanged
|
|
readOpen bool
|
|
writeOpen bool
|
|
}
|
|
|
|
// New creates a new fs given the transfer characteristic
|
|
func New(transfer *gatt.GattCharacteristic1) (*FS, error) {
|
|
// Create new FS instance
|
|
out := &FS{transferChar: transfer}
|
|
|
|
// Start notifications on transfer characteristic
|
|
err := out.transferChar.StartNotify()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Watch properties of transfer characteristic
|
|
ch, err := out.transferChar.WatchProperties()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Create buffered channel for propery change events
|
|
bufCh := make(chan *bluez.PropertyChanged, 10)
|
|
go func() {
|
|
// Relay all messages from original channel to buffered
|
|
for val := range ch {
|
|
bufCh <- val
|
|
}
|
|
}()
|
|
// Set transfer response channel to buffered channel
|
|
out.transferRespCh = bufCh
|
|
return out, nil
|
|
}
|
|
|
|
func (blefs *FS) Close() error {
|
|
return blefs.transferChar.StopNotify()
|
|
}
|
|
|
|
// request makes a request on the transfer characteristic
|
|
func (blefs *FS) request(cmd byte, padding bool, data ...interface{}) error {
|
|
// Encode data as binary
|
|
dataBin, err := encode(data...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bin := []byte{cmd}
|
|
if padding {
|
|
bin = append(bin, 0x00)
|
|
}
|
|
// Append encoded data to command with one byte of padding
|
|
bin = append(bin, dataBin...)
|
|
// Write value to characteristic
|
|
err = blefs.transferChar.WriteValue(bin, btOptsCmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// on waits for the given command to be received on
|
|
// the control point characteristic, then runs the callback.
|
|
func (blefs *FS) on(resp byte, onCmdCb func(data []byte) error) error {
|
|
// Use for loop in case of invalid property
|
|
for {
|
|
select {
|
|
case propChanged := <-blefs.transferRespCh:
|
|
// If property was invalid
|
|
if propChanged.Name != "Value" {
|
|
// Keep waiting
|
|
continue
|
|
}
|
|
// Assert propery value as byte slice
|
|
data := propChanged.Value.([]byte)
|
|
// If command has prefix of given command
|
|
if data[0] == resp {
|
|
// Return callback with data after command
|
|
return onCmdCb(data[1:])
|
|
}
|
|
return ErrFSUnexpectedResponse
|
|
case <-time.After(time.Minute):
|
|
return ErrFSResponseTimeout
|
|
}
|
|
}
|
|
}
|
|
|
|
// encode encodes go values to binary
|
|
func encode(data ...interface{}) ([]byte, error) {
|
|
// Create new buffer
|
|
buf := &bytes.Buffer{}
|
|
// For every data element
|
|
for _, elem := range data {
|
|
switch val := elem.(type) {
|
|
case string:
|
|
// Write string to buffer
|
|
if _, err := buf.WriteString(val); err != nil {
|
|
return nil, err
|
|
}
|
|
case []byte:
|
|
// Write bytes to buffer
|
|
if _, err := buf.Write(val); err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
// Encode and write value as little endian binary
|
|
if err := binary.Write(buf, binary.LittleEndian, val); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
// Return bytes from buffer
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// decode reads binary into pointers given in vals
|
|
func decode(data []byte, vals ...interface{}) error {
|
|
offset := 0
|
|
for _, elem := range vals {
|
|
// If at end of data, stop
|
|
if offset == len(data) {
|
|
break
|
|
}
|
|
switch val := elem.(type) {
|
|
case *string:
|
|
// Set val to string starting from offset
|
|
*val = string(data[offset:])
|
|
// Add string length to offset
|
|
offset += len(data) - offset
|
|
case *[]byte:
|
|
// Set val to byte slice starting from offset
|
|
*val = data[offset:]
|
|
// Add slice length to offset
|
|
offset += len(data) - offset
|
|
default:
|
|
// Create new reader for data starting from offset
|
|
reader := bytes.NewReader(data[offset:])
|
|
// Read binary into value pointer
|
|
err := binary.Read(reader, binary.LittleEndian, val)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Add size of value to offset
|
|
offset += binary.Size(val)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// maxData returns MTU-20. This is the maximum amount of data
|
|
// to send in a packet. Subtracting 20 ensures that the MTU
|
|
// is never exceeded.
|
|
func (blefs *FS) maxData() uint16 {
|
|
mtu := blefs.transferChar.Properties.MTU
|
|
// If MTU is zero, the current version of BlueZ likely
|
|
// doesn't support the MTU property, so assume 256.
|
|
if mtu == 0 {
|
|
mtu = 256
|
|
}
|
|
return mtu - 20
|
|
}
|
|
|
|
// padding returns a slice of len amount of 0x00.
|
|
func padding(len int) []byte {
|
|
return make([]byte, len)
|
|
}
|
|
|
|
// bytesHuman returns a human-readable string for
|
|
// the amount of bytes inputted.
|
|
func bytesHuman(b uint32) (float64, string) {
|
|
const unit = 1000
|
|
// Set possible units prefixes (PineTime flash is 4MB)
|
|
units := [2]rune{'k', 'M'}
|
|
// If amount of bytes is less than smallest unit
|
|
if b < unit {
|
|
// Return unchanged with unit "B"
|
|
return float64(b), "B"
|
|
}
|
|
|
|
div, exp := uint32(unit), 0
|
|
// Get decimal values and unit prefix index
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
|
|
// Create string for full unit
|
|
unitStr := string([]rune{units[exp], 'B'})
|
|
|
|
// Return decimal with unit string
|
|
return float64(b) / float64(div), unitStr
|
|
}
|