8 Commits

10 changed files with 332 additions and 17 deletions

95
api/fs.go Normal file
View File

@@ -0,0 +1,95 @@
package api
import (
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/itd/internal/types"
)
func (c *Client) Rename(old, new string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeMove,
Files: []string{old, new},
},
})
if err != nil {
return err
}
return nil
}
func (c *Client) Remove(path string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeDelete,
Files: []string{path},
},
})
if err != nil {
return err
}
return nil
}
func (c *Client) Mkdir(path string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeMkdir,
Files: []string{path},
},
})
if err != nil {
return err
}
return nil
}
func (c *Client) ReadDir(path string) ([]types.FileInfo, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeList,
Files: []string{path},
},
})
if err != nil {
return nil, err
}
var out []types.FileInfo
err = mapstructure.Decode(res.Value, &out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *Client) ReadFile(path string) (string, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeRead,
Files: []string{path},
},
})
if err != nil {
return "", err
}
return res.Value.(string), nil
}
func (c *Client) WriteFile(path, data string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeRead,
Files: []string{path},
},
})
if err != nil {
return err
}
return nil
}

View File

@@ -19,6 +19,9 @@
package firmware
import (
"fmt"
"time"
"github.com/cheggaaa/pb/v3"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
@@ -38,6 +41,8 @@ var upgradeCmd = &cobra.Command{
Short: "Upgrade InfiniTime firmware using files or archive",
Aliases: []string{"upg"},
Run: func(cmd *cobra.Command, args []string) {
start := time.Now()
client := viper.Get("client").(*api.Client)
var upgType api.UpgradeType
@@ -79,6 +84,9 @@ var upgradeCmd = &cobra.Command{
}
// Finish progress bar
bar.Finish()
fmt.Printf("Transferred %d B in %s.\n", bar.Total(), time.Since(start))
fmt.Println("Remember to validate the new firmware in the InfiniTime settings.")
},
}

View File

@@ -134,8 +134,6 @@ func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container {
// Show progress dialog
progressDlg.Show()
// Hide progress dialog after completion
defer progressDlg.Hide()
for event := range progress {
// Set label text to received / total B
@@ -150,6 +148,24 @@ func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container {
break
}
}
// Hide progress dialog after completion
progressDlg.Hide()
// Reset screen to default
upgradeTypeSelect.SetSelectedIndex(0)
firmwareBtn.SetText("Select firmware (.bin)")
initPktBtn.SetText("Select init packet (.dat)")
archiveBtn.SetText("Select archive (.zip)")
firmwarePath = ""
initPktPath = ""
archivePath = ""
dialog.NewInformation(
"Upgrade Complete",
"The firmware was transferred successfully.\nRemember to validate the firmware in InfiniTime settings.",
parent,
).Show()
})
// Return container containing all elements

View File

@@ -28,17 +28,21 @@ func init() {
}
func setCfgDefaults() {
viper.SetDefault("cfg.version", 2)
viper.SetDefault("socket.path", "/tmp/itd/socket")
viper.SetDefault("conn.reconnect", true)
viper.SetDefault("conn.whitelist.enabled", false)
viper.SetDefault("conn.whitelist.devices", []string{})
viper.SetDefault("on.connect.notify", true)
viper.SetDefault("on.reconnect.notify", true)
viper.SetDefault("on.reconnect.setTime", true)
viper.SetDefault("notifs.translit.use", []string{"eASCII"})
viper.SetDefault("notifs.translit.custom", []string{})
viper.SetDefault("notifs.ignore.sender", []string{})
viper.SetDefault("notifs.ignore.summary", []string{"InfiniTime"})
viper.SetDefault("notifs.ignore.body", []string{})

2
go.mod
View File

@@ -25,7 +25,7 @@ require (
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe // indirect
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
github.com/yuin/goldmark v1.4.1 // indirect
go.arsenm.dev/infinitime v0.0.0-20211023042633-53aa6f8a0c72
go.arsenm.dev/infinitime v0.0.0-20211123052001-ec1548ec0f30
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 // indirect
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect

8
go.sum
View File

@@ -287,8 +287,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mozillazg/go-pinyin v0.18.0 h1:hQompXO23/0ohH8YNjvfsAITnCQImCiR/Fny8EhIeW0=
github.com/mozillazg/go-pinyin v0.18.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d h1:EG/xyWjHT19rkUpwsWSkyiCCmyqNwFovr9m10rhyOxU=
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a h1:KxRXeSWoBM5FCPAnSUYxt1qwEzmoH/K7upb4fiSDwdc=
github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -367,8 +367,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.arsenm.dev/infinitime v0.0.0-20211023042633-53aa6f8a0c72 h1:e8kOuL6Jj8ZjJzkGwJ3xqpGG9EhUzfvZk9AlSsm3X1U=
go.arsenm.dev/infinitime v0.0.0-20211023042633-53aa6f8a0c72/go.mod h1:gaepaueUz4J5FfxuV19B4w5pi+V3mD0LTef50ryxr/Q=
go.arsenm.dev/infinitime v0.0.0-20211123052001-ec1548ec0f30 h1:nhuJfwWxBxfolMU5go3rBLhvl59HMRIMqEKi405Ja9w=
go.arsenm.dev/infinitime v0.0.0-20211123052001-ec1548ec0f30/go.mod h1:kNBKxQfqeLUfi13GM6tB1kSvLm8HlZ7PM47AYeJQIiw=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=

View File

@@ -1,5 +1,10 @@
package types
import (
"fmt"
"strconv"
)
const (
ReqTypeHeartRate = iota
ReqTypeBattLevel
@@ -15,6 +20,7 @@ const (
ReqTypeStepCount
ReqTypeWatchStepCount
ReqTypeCancel
ReqTypeFS
)
const (
@@ -22,6 +28,21 @@ const (
UpgradeTypeFiles
)
const (
FSTypeWrite = iota
FSTypeRead
FSTypeMove
FSTypeDelete
FSTypeList
FSTypeMkdir
)
type ReqDataFS struct {
Type int `json:"type"`
Files []string `json:"files"`
Data string `json:"data,omitempty"`
}
type ReqDataFwUpgrade struct {
Type int
Files []string
@@ -56,3 +77,66 @@ type MotionValues struct {
Y int16
Z int16
}
type FileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"isDir"`
}
func (fi FileInfo) String() string {
var isDirChar rune
if fi.IsDir {
isDirChar = 'd'
} else {
isDirChar = '-'
}
// Get human-readable value for file size
val, unit := bytesHuman(fi.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,
fi.Name,
)
}
// bytesHuman returns a human-readable string for
// the amount of bytes inputted.
func bytesHuman(b int64) (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 := int64(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
}

View File

@@ -1,13 +1,13 @@
# This is temporary, it is to show a notice
# to people still using the old config
cfg.version = 2
[socket]
path = "/tmp/itd/socket"
[conn]
reconnect = true
[conn.whitelist]
enabled = false
devices = []
[on.connect]
notify = true

View File

@@ -29,16 +29,15 @@ import (
var firmwareUpdating = false
func main() {
if viper.GetInt("cfg.version") != 2 {
log.Fatal().Msg("Please update your config to the newest format, only v2 configs supported.")
}
infinitime.Init()
// Cleanly exit after function
defer infinitime.Exit()
// Connect to InfiniTime with default options
dev, err := infinitime.Connect(&infinitime.Options{
AttemptReconnect: viper.GetBool("conn.reconnect"),
WhitelistEnabled: viper.GetBool("conn.whitelist.enabled"),
Whitelist: viper.GetStringSlice("conn.whitelist.devices"),
})
if err != nil {
log.Error().Err(err).Msg("Error connecting to InfiniTime")

109
socket.go
View File

@@ -22,6 +22,7 @@ import (
"bufio"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
@@ -100,6 +101,11 @@ func startSocket(dev *infinitime.Device) error {
func handleConnection(conn net.Conn, dev *infinitime.Device) {
defer conn.Close()
fs, err := dev.FS()
if err != nil {
connErr(conn, 0, nil, "Error getting device filesystem")
}
// Create new scanner on connection
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
@@ -431,6 +437,109 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
break
}
firmwareUpdating = false
case types.ReqTypeFS:
// If no data, return error
if req.Data == nil {
connErr(conn, req.Type, nil, "Data required for firmware upgrade request")
break
}
var reqData types.ReqDataFS
// Decode data map to firmware upgrade request data
err = mapstructure.Decode(req.Data, &reqData)
if err != nil {
connErr(conn, req.Type, err, "Error decoding request data")
break
}
switch reqData.Type {
case types.FSTypeDelete:
for _, file := range reqData.Files {
err := fs.Remove(file)
if err != nil {
connErr(conn, req.Type, err, "Error removing file")
break
}
}
case types.FSTypeMove:
if len(reqData.Files) != 2 {
connErr(conn, req.Type, nil, "Move FS command requires an old path and new path in the files list")
break
}
err := fs.Rename(reqData.Files[0], reqData.Files[1])
if err != nil {
connErr(conn, req.Type, err, "Error moving file")
break
}
case types.FSTypeMkdir:
for _, file := range reqData.Files {
err := fs.Mkdir(file)
if err != nil {
connErr(conn, req.Type, err, "Error creating directory")
break
}
}
case types.FSTypeList:
if len(reqData.Files) != 1 {
connErr(conn, req.Type, nil, "List FS command requires a path to list in the files list")
break
}
entries, err := fs.ReadDir(reqData.Files[0])
if err != nil {
connErr(conn, req.Type, err, "Error reading directory")
break
}
var out []types.FileInfo
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
connErr(conn, req.Type, err, "Error getting file info")
break
}
out = append(out, types.FileInfo{
Name: info.Name(),
Size: info.Size(),
IsDir: info.IsDir(),
})
}
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: out,
})
case types.FSTypeWrite:
if len(reqData.Files) != 1 {
connErr(conn, req.Type, nil, "Write FS command requires a path to the file to write")
break
}
file, err := fs.Create(reqData.Files[0], uint32(len(reqData.Data)))
if err != nil {
connErr(conn, req.Type, err, "Error creating file")
break
}
_, err = file.WriteString(reqData.Data)
if err != nil {
connErr(conn, req.Type, err, "Error writing to file")
break
}
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.FSTypeRead:
if len(reqData.Files) != 1 {
connErr(conn, req.Type, nil, "Read FS command requires a path to the file to read")
break
}
file, err := fs.Open(reqData.Files[0])
if err != nil {
connErr(conn, req.Type, err, "Error opening file")
break
}
data, err := io.ReadAll(file)
if err != nil {
connErr(conn, req.Type, err, "Error reading from file")
break
}
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: string(data),
})
}
case types.ReqTypeCancel:
if req.Data == nil {
connErr(conn, req.Type, nil, "No data provided. Cancel request requires request ID string as data.")