Implement BLE filesystem (experimental and will change in the future)
This commit is contained in:
parent
ec43bad466
commit
e9a92bac46
54
blefs/basic.go
Normal file
54
blefs/basic.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package blefs
|
||||||
|
|
||||||
|
// Rename moves or renames a file or directory
|
||||||
|
func (blefs *FS) Rename(old, new string) error {
|
||||||
|
// Create move request
|
||||||
|
err := blefs.request(
|
||||||
|
FSCmdMove,
|
||||||
|
true,
|
||||||
|
uint16(len(old)),
|
||||||
|
uint16(len(new)),
|
||||||
|
old,
|
||||||
|
byte(0x00),
|
||||||
|
new,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var status int8
|
||||||
|
// Upon receiving 0x61 (FSResponseMove)
|
||||||
|
blefs.on(FSResponseMove, func(data []byte) error {
|
||||||
|
// Read status byte
|
||||||
|
return decode(data, &status)
|
||||||
|
})
|
||||||
|
// If status is not ok, return error
|
||||||
|
if status != FSStatusOk {
|
||||||
|
return FSError{status}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes a file or directory
|
||||||
|
func (blefs *FS) Remove(path string) error {
|
||||||
|
// Create delete request
|
||||||
|
err := blefs.request(
|
||||||
|
FSCmdDelete,
|
||||||
|
true,
|
||||||
|
uint16(len(path)),
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var status int8
|
||||||
|
// Upon receiving 0x31 (FSResponseDelete)
|
||||||
|
blefs.on(FSResponseDelete, func(data []byte) error {
|
||||||
|
// Read status byte
|
||||||
|
return decode(data, &status)
|
||||||
|
})
|
||||||
|
if status == FSStatusError {
|
||||||
|
// If status is not ok, return error
|
||||||
|
return FSError{status}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
156
blefs/dir.go
Normal file
156
blefs/dir.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
package blefs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mkdir creates a directory at the given path
|
||||||
|
func (blefs *FS) Mkdir(path string) error {
|
||||||
|
// Create make directory request
|
||||||
|
err := blefs.request(
|
||||||
|
FSCmdMkdir,
|
||||||
|
true,
|
||||||
|
uint16(len(path)),
|
||||||
|
padding(4),
|
||||||
|
uint64(time.Now().UnixNano()),
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var status int8
|
||||||
|
// Upon receiving 0x41 (FSResponseMkdir), read status byte
|
||||||
|
blefs.on(FSResponseMkdir, func(data []byte) error {
|
||||||
|
return decode(data, &status)
|
||||||
|
})
|
||||||
|
// If status not ok, return error
|
||||||
|
if status != FSStatusOk {
|
||||||
|
return FSError{status}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir returns a list of directory entries from the given path
|
||||||
|
func (blefs *FS) ReadDir(path string) ([]fs.DirEntry, error) {
|
||||||
|
// Create list directory request
|
||||||
|
err := blefs.request(
|
||||||
|
FSCmdListDir,
|
||||||
|
true,
|
||||||
|
uint16(len(path)),
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var out []fs.DirEntry
|
||||||
|
for {
|
||||||
|
// Create new directory entry
|
||||||
|
listing := DirEntry{}
|
||||||
|
// Upon receiving 0x50 (FSResponseListDir)
|
||||||
|
blefs.on(FSResponseListDir, func(data []byte) error {
|
||||||
|
// Read data into listing
|
||||||
|
err := decode(
|
||||||
|
data,
|
||||||
|
&listing.status,
|
||||||
|
&listing.pathLen,
|
||||||
|
&listing.entryNum,
|
||||||
|
&listing.entries,
|
||||||
|
&listing.flags,
|
||||||
|
&listing.modtime,
|
||||||
|
&listing.size,
|
||||||
|
&listing.path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// If status is not ok, return error
|
||||||
|
if listing.status != FSStatusOk {
|
||||||
|
return nil, FSError{listing.status}
|
||||||
|
}
|
||||||
|
// Stop once entry number equals total entries
|
||||||
|
if listing.entryNum == listing.entries {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Append listing to slice
|
||||||
|
out = append(out, listing)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirEntry represents an entry from a directory listing
|
||||||
|
type DirEntry struct {
|
||||||
|
status int8
|
||||||
|
pathLen uint16
|
||||||
|
entryNum uint32
|
||||||
|
entries uint32
|
||||||
|
flags uint32
|
||||||
|
modtime uint64
|
||||||
|
size uint32
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the file described by the entry
|
||||||
|
func (de DirEntry) Name() string {
|
||||||
|
return de.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir reports whether the entry describes a directory.
|
||||||
|
func (de DirEntry) IsDir() bool {
|
||||||
|
return de.flags&0b1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type bits for the entry.
|
||||||
|
func (de DirEntry) Type() fs.FileMode {
|
||||||
|
if de.IsDir() {
|
||||||
|
return fs.ModeDir
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns the FileInfo for the file or subdirectory described by the entry.
|
||||||
|
func (de DirEntry) Info() (fs.FileInfo, error) {
|
||||||
|
return FileInfo{
|
||||||
|
name: de.path,
|
||||||
|
size: de.size,
|
||||||
|
modtime: de.modtime,
|
||||||
|
mode: de.Type(),
|
||||||
|
isDir: de.IsDir(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (de DirEntry) String() string {
|
||||||
|
var isDirChar rune
|
||||||
|
if de.IsDir() {
|
||||||
|
isDirChar = 'd'
|
||||||
|
} else {
|
||||||
|
isDirChar = '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get human-readable value for file size
|
||||||
|
val, unit := bytesHuman(de.size)
|
||||||
|
prec := 0
|
||||||
|
// If value is less than 10, set precision to 1
|
||||||
|
if val < 10 {
|
||||||
|
prec = 1
|
||||||
|
}
|
||||||
|
// Convert float to string
|
||||||
|
valStr := strconv.FormatFloat(val, 'f', prec, 64)
|
||||||
|
|
||||||
|
// Return string formatted like so:
|
||||||
|
// - 10 kB file
|
||||||
|
// or:
|
||||||
|
// d 0 B .
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%c %3s %-2s %s",
|
||||||
|
isDirChar,
|
||||||
|
valStr,
|
||||||
|
unit,
|
||||||
|
de.path,
|
||||||
|
)
|
||||||
|
}
|
61
blefs/error.go
Normal file
61
blefs/error.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package blefs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFileNotExists = errors.New("file does not exist")
|
||||||
|
ErrFileReadOnly = errors.New("file is read only")
|
||||||
|
ErrFileWriteOnly = errors.New("file is write only")
|
||||||
|
ErrInvalidOffset = errors.New("invalid file offset")
|
||||||
|
ErrOffsetChanged = errors.New("offset has already been changed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// FSError represents an error returned by BLE FS
|
||||||
|
type FSError struct {
|
||||||
|
Code int8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the string associated with the error code
|
||||||
|
func (err FSError) Error() string {
|
||||||
|
switch err.Code {
|
||||||
|
case 0x02:
|
||||||
|
return "filesystem error"
|
||||||
|
case 0x05:
|
||||||
|
return "read-only filesystem"
|
||||||
|
case 0x03:
|
||||||
|
return "no such file"
|
||||||
|
case 0x04:
|
||||||
|
return "protocol error"
|
||||||
|
case -5:
|
||||||
|
return "input/output error"
|
||||||
|
case -84:
|
||||||
|
return "filesystem is corrupted"
|
||||||
|
case -2:
|
||||||
|
return "no such directory entry"
|
||||||
|
case -17:
|
||||||
|
return "entry already exists"
|
||||||
|
case -20:
|
||||||
|
return "entry is not a directory"
|
||||||
|
case -39:
|
||||||
|
return "directory is not empty"
|
||||||
|
case -9:
|
||||||
|
return "bad file number"
|
||||||
|
case -27:
|
||||||
|
return "file is too large"
|
||||||
|
case -22:
|
||||||
|
return "invalid parameter"
|
||||||
|
case -28:
|
||||||
|
return "no space left on device"
|
||||||
|
case -12:
|
||||||
|
return "no more memory available"
|
||||||
|
case -61:
|
||||||
|
return "no attr available"
|
||||||
|
case -36:
|
||||||
|
return "file name is too long"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("unknown error (code %d)", err.Code)
|
||||||
|
}
|
||||||
|
}
|
350
blefs/file.go
Normal file
350
blefs/file.go
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
package blefs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// File represents a file on the BLE filesystem
|
||||||
|
type File struct {
|
||||||
|
fs *FS
|
||||||
|
path string
|
||||||
|
offset uint32
|
||||||
|
length uint32
|
||||||
|
amtLeft uint32
|
||||||
|
amtTferd uint32
|
||||||
|
isReadOnly bool
|
||||||
|
isWriteOnly bool
|
||||||
|
offsetChanged bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens a file and returns it as an fs.File to
|
||||||
|
// satisfy the fs.FS interface
|
||||||
|
func (blefs *FS) Open(path string) (fs.File, error) {
|
||||||
|
// Make a read file request. This opens the file for reading.
|
||||||
|
err := blefs.request(
|
||||||
|
FSCmdReadFile,
|
||||||
|
true,
|
||||||
|
uint16(len(path)),
|
||||||
|
uint32(0),
|
||||||
|
uint32(blefs.maxData()),
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &File{
|
||||||
|
fs: blefs,
|
||||||
|
path: path,
|
||||||
|
length: 0,
|
||||||
|
offset: 0,
|
||||||
|
isReadOnly: true,
|
||||||
|
isWriteOnly: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create makes a new file on the BLE file system and returns it.
|
||||||
|
func (blefs *FS) Create(path string, size uint32) (*File, error) {
|
||||||
|
// Make a write file request. This will create and open a file for writing.
|
||||||
|
err := blefs.request(
|
||||||
|
FSCmdWriteFile,
|
||||||
|
true,
|
||||||
|
uint16(len(path)),
|
||||||
|
uint32(0),
|
||||||
|
uint64(time.Now().UnixNano()),
|
||||||
|
size,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &File{
|
||||||
|
fs: blefs,
|
||||||
|
path: path,
|
||||||
|
length: size,
|
||||||
|
amtLeft: size,
|
||||||
|
offset: 0,
|
||||||
|
isReadOnly: false,
|
||||||
|
isWriteOnly: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads data from a file into b
|
||||||
|
func (fl *File) Read(b []byte) (n int, err error) {
|
||||||
|
// If file is write only (opened by FS.Create())
|
||||||
|
if fl.isWriteOnly {
|
||||||
|
return 0, ErrFileWriteOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
// If offset has been changed (Seek() called)
|
||||||
|
if fl.offsetChanged {
|
||||||
|
// Create new read file request with the specified offset to restart reading
|
||||||
|
err := fl.fs.request(
|
||||||
|
FSCmdReadFile,
|
||||||
|
true,
|
||||||
|
uint16(len(fl.path)),
|
||||||
|
fl.offset,
|
||||||
|
uint32(fl.fs.maxData()),
|
||||||
|
fl.path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Reset offsetChanged
|
||||||
|
fl.offsetChanged = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get length of b. This will be the maximum amount that can be read.
|
||||||
|
maxLen := uint32(len(b))
|
||||||
|
if maxLen == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
var buf []byte
|
||||||
|
for {
|
||||||
|
// If amount transfered equals max length
|
||||||
|
if fl.amtTferd == maxLen {
|
||||||
|
// Reset amount transfered
|
||||||
|
fl.amtTferd = 0
|
||||||
|
// Copy buffer contents to b
|
||||||
|
copy(b, buf)
|
||||||
|
// Return max length with no error
|
||||||
|
return int(maxLen), nil
|
||||||
|
}
|
||||||
|
// Create new empty fileReadResponse
|
||||||
|
resp := fileReadResponse{}
|
||||||
|
// Upon receiving 0x11 (FSResponseReadFile)
|
||||||
|
err := fl.fs.on(FSResponseReadFile, func(data []byte) error {
|
||||||
|
// Read binary data into struct
|
||||||
|
err := decode(
|
||||||
|
data,
|
||||||
|
&resp.status,
|
||||||
|
&resp.padding,
|
||||||
|
&resp.offset,
|
||||||
|
&resp.length,
|
||||||
|
&resp.chunkLen,
|
||||||
|
&resp.data,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// If status is not ok
|
||||||
|
if resp.status != FSStatusOk {
|
||||||
|
return FSError{resp.status}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// If entire file transferred, break
|
||||||
|
if fl.offset == resp.length {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append data returned in response to buffer
|
||||||
|
buf = append(buf, resp.data...)
|
||||||
|
// Set file length
|
||||||
|
fl.length = resp.length
|
||||||
|
// Add returned chunk length to offset and amount transferred
|
||||||
|
fl.offset += resp.chunkLen
|
||||||
|
fl.amtTferd += resp.chunkLen
|
||||||
|
|
||||||
|
// Calculate amount of bytes to be sent in next request
|
||||||
|
chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
|
||||||
|
// If after transferring, there will be more data than max length
|
||||||
|
if fl.amtTferd+chunkLen > maxLen {
|
||||||
|
// Set chunk length to amount left to fill max length
|
||||||
|
chunkLen = maxLen - fl.amtTferd
|
||||||
|
}
|
||||||
|
// Make data request. This will return more data from the file.
|
||||||
|
fl.fs.request(
|
||||||
|
FSCmdDataReq,
|
||||||
|
false,
|
||||||
|
byte(FSStatusOk),
|
||||||
|
padding(2),
|
||||||
|
fl.offset,
|
||||||
|
chunkLen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Copy buffer contents to b
|
||||||
|
copied := copy(b, buf)
|
||||||
|
// Return amount of bytes copied with EOF error
|
||||||
|
return copied, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes data from b into a file on the BLE filesysyem
|
||||||
|
func (fl *File) Write(b []byte) (n int, err error) {
|
||||||
|
maxLen := uint32(cap(b))
|
||||||
|
// If file is read only (opened by FS.Open())
|
||||||
|
if fl.isReadOnly {
|
||||||
|
return 0, ErrFileReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
// If offset has been changed (Seek() called)
|
||||||
|
if fl.offsetChanged {
|
||||||
|
// Create new write file request with the specified offset to restart writing
|
||||||
|
err := fl.fs.request(
|
||||||
|
FSCmdWriteFile,
|
||||||
|
true,
|
||||||
|
uint16(len(fl.path)),
|
||||||
|
fl.offset,
|
||||||
|
uint64(time.Now().UnixNano()),
|
||||||
|
fl.length,
|
||||||
|
fl.path,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Reset offsetChanged
|
||||||
|
fl.offsetChanged = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
// If amount transfered equals max length
|
||||||
|
if fl.amtTferd == maxLen {
|
||||||
|
// Reset amount transfered
|
||||||
|
fl.amtTferd = 0
|
||||||
|
// Return max length with no error
|
||||||
|
return int(maxLen), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new empty fileWriteResponse
|
||||||
|
resp := fileWriteResponse{}
|
||||||
|
// Upon receiving 0x21 (FSResponseWriteFile)
|
||||||
|
err := fl.fs.on(FSResponseWriteFile, func(data []byte) error {
|
||||||
|
// Read binary data into struct
|
||||||
|
err := decode(
|
||||||
|
data,
|
||||||
|
&resp.status,
|
||||||
|
&resp.padding,
|
||||||
|
&resp.offset,
|
||||||
|
&resp.modtime,
|
||||||
|
&resp.free,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// If status is not ok
|
||||||
|
if resp.status != FSStatusOk {
|
||||||
|
return FSError{resp.status}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// If no free space left in current file, break
|
||||||
|
if resp.free == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate amount of bytes to be transferred in next request
|
||||||
|
chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
|
||||||
|
// If after transferring, there will be more data than max length
|
||||||
|
if fl.amtTferd+chunkLen > maxLen {
|
||||||
|
// Set chunk length to amount left to fill max length
|
||||||
|
chunkLen = maxLen - fl.amtTferd
|
||||||
|
}
|
||||||
|
// Get data from b
|
||||||
|
chunk := b[fl.amtTferd : fl.amtTferd+chunkLen]
|
||||||
|
// Create transfer request. This will transfer the chunk to the file.
|
||||||
|
fl.fs.request(
|
||||||
|
FSCmdTransfer,
|
||||||
|
false,
|
||||||
|
byte(FSStatusOk),
|
||||||
|
padding(2),
|
||||||
|
fl.offset,
|
||||||
|
chunkLen,
|
||||||
|
chunk,
|
||||||
|
)
|
||||||
|
// Add chunk length to offset and amount transferred
|
||||||
|
fl.offset += chunkLen
|
||||||
|
fl.amtTferd += chunkLen
|
||||||
|
}
|
||||||
|
return int(fl.offset), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString converts the string to []byte and calls Write()
|
||||||
|
func (fl *File) WriteString(s string) (n int, err error) {
|
||||||
|
return fl.Write([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek changes the offset of the file being read/written.
|
||||||
|
// This can only be done once in between reads/writes.
|
||||||
|
func (fl *File) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if fl.offsetChanged {
|
||||||
|
return int64(fl.offset), ErrOffsetChanged
|
||||||
|
}
|
||||||
|
var newOffset int64
|
||||||
|
switch whence {
|
||||||
|
case io.SeekCurrent:
|
||||||
|
newOffset = int64(fl.offset) + offset
|
||||||
|
case io.SeekStart:
|
||||||
|
newOffset = offset
|
||||||
|
case io.SeekEnd:
|
||||||
|
newOffset = int64(fl.length) + offset
|
||||||
|
default:
|
||||||
|
newOffset = int64(fl.offset)
|
||||||
|
}
|
||||||
|
if newOffset < 0 || uint32(newOffset) > fl.length {
|
||||||
|
return int64(fl.offset), ErrInvalidOffset
|
||||||
|
}
|
||||||
|
fl.offset = uint32(newOffset)
|
||||||
|
fl.offsetChanged = true
|
||||||
|
return int64(newOffset), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the fs.File interface.
|
||||||
|
// It just returns nil.
|
||||||
|
func (fl *File) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat does a RedDir() and finds the current file in the output
|
||||||
|
func (fl *File) Stat() (fs.FileInfo, error) {
|
||||||
|
// Get directory in filepath
|
||||||
|
dir := filepath.Dir(fl.path)
|
||||||
|
// Read directory
|
||||||
|
dirEntries, err := fl.fs.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, entry := range dirEntries {
|
||||||
|
// If file name is base name of path
|
||||||
|
if entry.Name() == filepath.Base(fl.path) {
|
||||||
|
// Return file info
|
||||||
|
return entry.Info()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ErrFileNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileReadResponse represents a response for a read request
|
||||||
|
type fileReadResponse struct {
|
||||||
|
status int8
|
||||||
|
padding [2]byte
|
||||||
|
offset uint32
|
||||||
|
length uint32
|
||||||
|
chunkLen uint32
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileWriteResponse represents a response for a write request
|
||||||
|
type fileWriteResponse struct {
|
||||||
|
status int8
|
||||||
|
padding [2]byte
|
||||||
|
offset uint32
|
||||||
|
modtime uint64
|
||||||
|
free uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// min returns the smaller uint32 out of two given
|
||||||
|
func min(o, t uint32) uint32 {
|
||||||
|
if t < o {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
47
blefs/fileinfo.go
Normal file
47
blefs/fileinfo.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package blefs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileInfo implements fs.FileInfo
|
||||||
|
type FileInfo struct {
|
||||||
|
name string
|
||||||
|
size uint32
|
||||||
|
modtime uint64
|
||||||
|
mode fs.FileMode
|
||||||
|
isDir bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the base name of the file
|
||||||
|
func (fi FileInfo) Name() string {
|
||||||
|
return fi.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the total size of the file
|
||||||
|
func (fi FileInfo) Size() int64 {
|
||||||
|
return int64(fi.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns the mode of the file
|
||||||
|
func (fi FileInfo) Mode() fs.FileMode {
|
||||||
|
return fi.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime returns the modification time of the file
|
||||||
|
// As of now, this is unimplemented in InfiniTime, and
|
||||||
|
// will always return 0.
|
||||||
|
func (fi FileInfo) ModTime() time.Time {
|
||||||
|
return time.Unix(0, int64(fi.modtime))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir returns whether the file is a directory
|
||||||
|
func (fi FileInfo) IsDir() bool {
|
||||||
|
return fi.isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sys is unimplemented and returns nil
|
||||||
|
func (fi FileInfo) Sys() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
232
blefs/fs.go
Normal file
232
blefs/fs.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return blefs.transferChar.Properties.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
|
||||||
|
}
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
||||||
"github.com/muka/go-bluetooth/bluez/profile/device"
|
"github.com/muka/go-bluetooth/bluez/profile/device"
|
||||||
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
||||||
|
"go.arsenm.dev/infinitime/blefs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const BTName = "InfiniTime"
|
const BTName = "InfiniTime"
|
||||||
@ -24,6 +25,8 @@ const (
|
|||||||
CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
|
CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
|
||||||
BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb"
|
BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb"
|
||||||
HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb"
|
HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb"
|
||||||
|
FSTransferChar = "adaf0200-4669-6c65-5472-616e73666572"
|
||||||
|
FSVersionChar = "adaf0100-4669-6c65-5472-616e73666572"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
@ -37,6 +40,8 @@ type Device struct {
|
|||||||
currentTimeChar *gatt.GattCharacteristic1
|
currentTimeChar *gatt.GattCharacteristic1
|
||||||
battLevelChar *gatt.GattCharacteristic1
|
battLevelChar *gatt.GattCharacteristic1
|
||||||
heartRateChar *gatt.GattCharacteristic1
|
heartRateChar *gatt.GattCharacteristic1
|
||||||
|
fsVersionChar *gatt.GattCharacteristic1
|
||||||
|
fsTransferChar *gatt.GattCharacteristic1
|
||||||
onReconnect func()
|
onReconnect func()
|
||||||
Music MusicCtrl
|
Music MusicCtrl
|
||||||
DFU DFU
|
DFU DFU
|
||||||
@ -311,6 +316,10 @@ func (i *Device) resolveChars() error {
|
|||||||
i.DFU.ctrlPointChar = char
|
i.DFU.ctrlPointChar = char
|
||||||
case DFUPacketChar:
|
case DFUPacketChar:
|
||||||
i.DFU.packetChar = char
|
i.DFU.packetChar = char
|
||||||
|
case FSTransferChar:
|
||||||
|
i.fsTransferChar = char
|
||||||
|
case FSVersionChar:
|
||||||
|
i.fsVersionChar = char
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -642,3 +651,8 @@ func (i *Device) NotifyCall(from string) (<-chan uint8, error) {
|
|||||||
}()
|
}()
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FS creates and returns a new filesystem from the device
|
||||||
|
func (i *Device) FS() (*blefs.FS, error) {
|
||||||
|
return blefs.New(i.fsTransferChar)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user