forked from Elara6331/infinitime
		
	Implement BLE filesystem (experimental and will change in the future)
This commit is contained in:
		
							
								
								
									
										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/device" | ||||
| 	"github.com/muka/go-bluetooth/bluez/profile/gatt" | ||||
| 	"go.arsenm.dev/infinitime/blefs" | ||||
| ) | ||||
|  | ||||
| const BTName = "InfiniTime" | ||||
| @@ -24,6 +25,8 @@ const ( | ||||
| 	CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb" | ||||
| 	BatteryLvlChar  = "00002a19-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 { | ||||
| @@ -37,6 +40,8 @@ type Device struct { | ||||
| 	currentTimeChar *gatt.GattCharacteristic1 | ||||
| 	battLevelChar   *gatt.GattCharacteristic1 | ||||
| 	heartRateChar   *gatt.GattCharacteristic1 | ||||
| 	fsVersionChar   *gatt.GattCharacteristic1 | ||||
| 	fsTransferChar  *gatt.GattCharacteristic1 | ||||
| 	onReconnect     func() | ||||
| 	Music           MusicCtrl | ||||
| 	DFU             DFU | ||||
| @@ -311,6 +316,10 @@ func (i *Device) resolveChars() error { | ||||
| 			i.DFU.ctrlPointChar = char | ||||
| 		case DFUPacketChar: | ||||
| 			i.DFU.packetChar = char | ||||
| 		case FSTransferChar: | ||||
| 			i.fsTransferChar = char | ||||
| 		case FSVersionChar: | ||||
| 			i.fsVersionChar = char | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| @@ -642,3 +651,8 @@ func (i *Device) NotifyCall(from string) (<-chan uint8, error) { | ||||
| 	}() | ||||
| 	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