20 Commits

Author SHA1 Message Date
552f19676b Add motion service to itgui 2021-10-25 09:45:19 -07:00
9d58ea0ae7 Remove debug print and add error handling (itgui) 2021-10-25 00:11:41 -07:00
76875db7ea Use api package in itgui 2021-10-24 13:27:14 -07:00
be5bdc625b Add watch commands to itctl 2021-10-24 11:02:29 -07:00
0d0db949af Handle unknown request type 2021-10-24 01:11:57 -07:00
0d164aef3d Use request type for error response type 2021-10-24 01:09:27 -07:00
28610d9ebb Fix API package 2021-10-24 00:49:48 -07:00
dff34b484d Return request type for response type 2021-10-24 00:45:50 -07:00
2ea9f99db6 Disable firmware updating error once progress channel closed 2021-10-23 22:03:33 -07:00
44dc5f8e47 Use sent bytes to check if transfer complete 2021-10-23 19:36:23 -07:00
4d35912466 Remove test 2021-10-23 18:42:22 -07:00
ef29b9bee4 Update itctl to use api 2021-10-23 18:41:03 -07:00
e198b769f9 Generalize socket cancellation and update API accordingly 2021-10-23 18:03:17 -07:00
ef4bad94b5 Reorganize itctl structure 2021-10-23 15:11:04 -07:00
8cf2b47733 Add cancellation to api package 2021-10-22 22:30:58 -07:00
f20fdcb161 Add responses to cancellation requests 2021-10-22 22:15:35 -07:00
eeba9b2964 Add cancellation to watchable values 2021-10-22 22:14:01 -07:00
d7057e3f9c Add doc comments to api package 2021-10-22 21:01:18 -07:00
80a5867d6b Send response types in socket responses and create api package 2021-10-22 20:47:57 -07:00
f001dd6079 Update readme 2021-10-22 17:12:46 -07:00
35 changed files with 1229 additions and 558 deletions

View File

@@ -15,7 +15,7 @@
- Notification transliteration
- Call Notifications (ModemManager)
- Music control
- Get info from watch (HRM, Battery level, Firmware version)
- Get info from watch (HRM, Battery level, Firmware version, Motion)
- Set current time
- Control socket
- Firmware upgrades
@@ -29,7 +29,7 @@ This daemon creates a UNIX socket at `/tmp/itd/socket`. It allows you to directl
The socket accepts JSON requests. For example, sending a notification looks like this:
```json
{"type": "notify", "data": {"title": "title1", "body": "body1"}}
{"type": 5, "data": {"title": "title1", "body": "body1"}}
```
It will return a JSON response. A response can have 3 fields: `error`, `msg`, and `value`. Error is a boolean that signals whether an error was returned. If error is true, the msg field will contain the error. Value can contain any data and depends on what the request was.
@@ -83,10 +83,10 @@ This is the `itctl` usage screen:
Control the itd daemon for InfiniTime smartwatches
Usage:
itctl [flags]
itctl [command]
Available Commands:
completion generate the autocompletion script for the specified shell
firmware Manage InfiniTime firmware
get Get information from InfiniTime
help Help about any command
@@ -94,7 +94,8 @@ Available Commands:
set Set information on InfiniTime
Flags:
-h, --help help for itctl
-h, --help help for itctl
-s, --socket-path string Path to itd socket
Use "itctl [command] --help" for more information about a command.
```

133
api/client.go Normal file
View File

@@ -0,0 +1,133 @@
package api
import (
"bufio"
"encoding/json"
"errors"
"net"
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/types"
)
// Default socket address
const DefaultAddr = "/tmp/itd/socket"
// Client is the socket API client
type Client struct {
conn net.Conn
respCh chan types.Response
heartRateCh chan types.Response
battLevelCh chan types.Response
stepCountCh chan types.Response
motionCh chan types.Response
dfuProgressCh chan types.Response
}
// New creates a new client and sets it up
func New(addr string) (*Client, error) {
conn, err := net.Dial("unix", addr)
if err != nil {
return nil, err
}
out := &Client{
conn: conn,
respCh: make(chan types.Response, 5),
}
go func() {
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
var res types.Response
err = json.Unmarshal(scanner.Bytes(), &res)
if err != nil {
continue
}
out.handleResp(res)
}
}()
return out, err
}
func (c *Client) Close() error {
err := c.conn.Close()
if err != nil {
return err
}
close(c.respCh)
return nil
}
// request sends a request to itd and waits for and returns the response
func (c *Client) request(req types.Request) (types.Response, error) {
// Encode request into connection
err := json.NewEncoder(c.conn).Encode(req)
if err != nil {
return types.Response{}, err
}
res := <-c.respCh
if res.Error {
return res, errors.New(res.Message)
}
return res, nil
}
// requestNoRes sends a request to itd and does not wait for the response
func (c *Client) requestNoRes(req types.Request) error {
// Encode request into connection
err := json.NewEncoder(c.conn).Encode(req)
if err != nil {
return err
}
return nil
}
// handleResp handles the received response as needed
func (c *Client) handleResp(res types.Response) error {
switch res.Type {
case types.ReqTypeWatchHeartRate:
c.heartRateCh <- res
case types.ReqTypeWatchBattLevel:
c.battLevelCh <- res
case types.ReqTypeWatchStepCount:
c.stepCountCh <- res
case types.ReqTypeWatchMotion:
c.motionCh <- res
case types.ReqTypeFwUpgrade:
c.dfuProgressCh <- res
default:
c.respCh <- res
}
return nil
}
func decodeUint8(val interface{}) uint8 {
return uint8(val.(float64))
}
func decodeUint32(val interface{}) uint32 {
return uint32(val.(float64))
}
func decodeMotion(val interface{}) (infinitime.MotionValues, error) {
out := infinitime.MotionValues{}
err := mapstructure.Decode(val, &out)
if err != nil {
return out, err
}
return out, nil
}
func decodeDFUProgress(val interface{}) (DFUProgress, error) {
out := DFUProgress{}
err := mapstructure.Decode(val, &out)
if err != nil {
return out, err
}
return out, nil
}

209
api/info.go Normal file
View File

@@ -0,0 +1,209 @@
package api
import (
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/types"
)
// Address gets the bluetooth address of the connected device
func (c *Client) Address() (string, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeBtAddress,
})
if err != nil {
return "", err
}
return res.Value.(string), nil
}
// Version gets the firmware version of the connected device
func (c *Client) Version() (string, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeFwVersion,
})
if err != nil {
return "", err
}
return res.Value.(string), nil
}
// BatteryLevel gets the battery level of the connected device
func (c *Client) BatteryLevel() (uint8, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeBattLevel,
})
if err != nil {
return 0, err
}
return uint8(res.Value.(float64)), nil
}
// WatchBatteryLevel returns a channel which will contain
// new battery level values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) {
c.battLevelCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchBattLevel,
})
if err != nil {
return nil, nil, err
}
res := <-c.battLevelCh
done, cancel := c.cancelFn(res.ID, c.battLevelCh)
out := make(chan uint8, 2)
go func() {
for res := range c.battLevelCh {
select {
case <-done:
return
default:
out <- decodeUint8(res.Value)
}
}
}()
return out, cancel, nil
}
// HeartRate gets the heart rate from the connected device
func (c *Client) HeartRate() (uint8, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeHeartRate,
})
if err != nil {
return 0, err
}
return decodeUint8(res.Value), nil
}
// WatchHeartRate returns a channel which will contain
// new heart rate values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchHeartRate() (<-chan uint8, func(), error) {
c.heartRateCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchHeartRate,
})
if err != nil {
return nil, nil, err
}
res := <-c.heartRateCh
done, cancel := c.cancelFn(res.ID, c.heartRateCh)
out := make(chan uint8, 2)
go func() {
for res := range c.heartRateCh {
select {
case <-done:
return
default:
out <- decodeUint8(res.Value)
}
}
}()
return out, cancel, nil
}
// cancelFn generates a cancellation function for the given
// request type and channel
func (c *Client) cancelFn(reqID string, ch chan types.Response) (chan struct{}, func()) {
done := make(chan struct{}, 1)
return done, func() {
done <- struct{}{}
close(ch)
c.requestNoRes(types.Request{
Type: types.ReqTypeCancel,
Data: reqID,
})
}
}
// StepCount gets the step count from the connected device
func (c *Client) StepCount() (uint32, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeStepCount,
})
if err != nil {
return 0, err
}
return uint32(res.Value.(float64)), nil
}
// WatchStepCount returns a channel which will contain
// new step count values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchStepCount() (<-chan uint32, func(), error) {
c.stepCountCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchStepCount,
})
if err != nil {
return nil, nil, err
}
res := <-c.stepCountCh
done, cancel := c.cancelFn(res.ID, c.stepCountCh)
out := make(chan uint32, 2)
go func() {
for res := range c.stepCountCh {
select {
case <-done:
return
default:
out <- decodeUint32(res.Value)
}
}
}()
return out, cancel, nil
}
// Motion gets the motion values from the connected device
func (c *Client) Motion() (infinitime.MotionValues, error) {
out := infinitime.MotionValues{}
res, err := c.request(types.Request{
Type: types.ReqTypeMotion,
})
if err != nil {
return out, err
}
err = mapstructure.Decode(res.Value, &out)
if err != nil {
return out, err
}
return out, nil
}
// WatchMotion returns a channel which will contain
// new motion values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchMotion() (<-chan infinitime.MotionValues, func(), error) {
c.motionCh = make(chan types.Response, 5)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchMotion,
})
if err != nil {
return nil, nil, err
}
res := <-c.motionCh
done, cancel := c.cancelFn(res.ID, c.motionCh)
out := make(chan infinitime.MotionValues, 5)
go func() {
for res := range c.motionCh {
select {
case <-done:
return
default:
motion, err := decodeMotion(res.Value)
if err != nil {
continue
}
out <- motion
}
}
}()
return out, cancel, nil
}

14
api/notify.go Normal file
View File

@@ -0,0 +1,14 @@
package api
import "go.arsenm.dev/itd/internal/types"
func (c *Client) Notify(title string, body string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeNotify,
Data: types.ReqDataNotify{
Title: title,
Body: body,
},
})
return err
}

33
api/time.go Normal file
View File

@@ -0,0 +1,33 @@
package api
import (
"time"
"go.arsenm.dev/itd/internal/types"
)
// SetTime sets the given time on the connected device
func (c *Client) SetTime(t time.Time) error {
_, err := c.request(types.Request{
Type: types.ReqTypeSetTime,
Data: t.Format(time.RFC3339),
})
if err != nil {
return err
}
return nil
}
// SetTimeNow sets the time on the connected device to
// the current time. This is more accurate than
// SetTime(time.Now()) due to RFC3339 formatting
func (c *Client) SetTimeNow() error {
_, err := c.request(types.Request{
Type: types.ReqTypeSetTime,
Data: "now",
})
if err != nil {
return err
}
return nil
}

48
api/upgrade.go Normal file
View File

@@ -0,0 +1,48 @@
package api
import (
"encoding/json"
"go.arsenm.dev/itd/internal/types"
)
// DFUProgress stores the progress of a DFU upfate
type DFUProgress types.DFUProgress
// UpgradeType indicates the type of upgrade to be performed
type UpgradeType uint8
// Type of DFU upgrade
const (
UpgradeTypeArchive UpgradeType = iota
UpgradeTypeFiles
)
// FirmwareUpgrade initiates a DFU update and returns the progress channel
func (c *Client) FirmwareUpgrade(upgType UpgradeType, files ...string) (<-chan DFUProgress, error) {
err := json.NewEncoder(c.conn).Encode(types.Request{
Type: types.ReqTypeFwUpgrade,
Data: types.ReqDataFwUpgrade{
Type: int(upgType),
Files: files,
},
})
if err != nil {
return nil, err
}
c.dfuProgressCh = make(chan types.Response, 5)
out := make(chan DFUProgress, 5)
go func() {
for res := range c.dfuProgressCh {
progress, err := decodeDFUProgress(res.Value)
if err != nil {
continue
}
out <- progress
}
}()
return out, nil
}

View File

@@ -16,10 +16,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package firmware
import (
"github.com/spf13/cobra"
"go.arsenm.dev/itd/cmd/itctl/root"
)
// firmwareCmd represents the firmware command
@@ -30,5 +31,5 @@ var firmwareCmd = &cobra.Command{
}
func init() {
rootCmd.AddCommand(firmwareCmd)
root.RootCmd.AddCommand(firmwareCmd)
}

View File

@@ -16,61 +16,50 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package firmware
import (
"bufio"
"encoding/json"
"net"
"github.com/cheggaaa/pb/v3"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
"go.arsenm.dev/itd/internal/types"
)
type DFUProgress struct {
Received int64 `mapstructure:"recvd"`
Total int64 `mapstructure:"total"`
}
// upgradeCmd represents the upgrade command
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrade InfiniTime firmware using files or archive",
Aliases: []string{"upg"},
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
client := viper.Get("client").(*api.Client)
var data types.ReqDataFwUpgrade
var upgType api.UpgradeType
var files []string
// Get relevant data struct
if viper.GetString("archive") != "" {
// Get archive data struct
data = types.ReqDataFwUpgrade{
Type: types.UpgradeTypeArchive,
Files: []string{viper.GetString("archive")},
}
upgType = types.UpgradeTypeArchive
files = []string{viper.GetString("archive")}
} else if viper.GetString("initPkt") != "" && viper.GetString("firmware") != "" {
// Get files data struct
data = types.ReqDataFwUpgrade{
Type: types.UpgradeTypeFiles,
Files: []string{viper.GetString("initPkt"), viper.GetString("firmware")},
}
upgType = types.UpgradeTypeFiles
files = []string{viper.GetString("initPkt"), viper.GetString("firmware")}
} else {
cmd.Usage()
log.Warn().Msg("Upgrade command requires either archive or init packet and firmware.")
return
}
// Encode response into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeFwUpgrade,
Data: data,
})
progress, err := client.FirmwareUpgrade(upgType, files...)
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
log.Fatal().Err(err).Msg("Error initiating DFU")
}
// Create progress bar template
@@ -78,37 +67,18 @@ var upgradeCmd = &cobra.Command{
// Start full bar at 0 total
bar := pb.ProgressBarTemplate(barTmpl).Start(0)
// Create new scanner of connection
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
var res types.Response
// Decode scanned line into response struct
err = json.Unmarshal(scanner.Bytes(), &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON response")
}
if res.Error {
log.Fatal().Msg(res.Message)
}
var event DFUProgress
// Decode response data into progress struct
err = mapstructure.Decode(res.Value, &event)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding response data")
}
for event := range progress {
// Set total bytes in progress bar
bar.SetTotal(event.Total)
// Set amount of bytes received in progress bar
bar.SetCurrent(event.Received)
// If transfer finished, break
if event.Received == event.Total {
if event.Sent == event.Total {
break
}
}
// Finish progress bar
bar.Finish()
if scanner.Err() != nil {
log.Fatal().Err(scanner.Err()).Msg("Error while scanning output")
}
},
}

View File

@@ -16,18 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package firmware
import (
"bufio"
"encoding/json"
"fmt"
"net"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
// versionCmd represents the version command
@@ -36,40 +33,14 @@ var versionCmd = &cobra.Command{
Aliases: []string{"ver"},
Short: "Get firmware version of InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
client := viper.Get("client").(*api.Client)
version, err := client.Version()
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeFwVersion,
})
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
log.Fatal().Err(err).Msg("Error getting firmware version")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Decode line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
if res.Error {
log.Fatal().Msg(res.Message)
}
// Print returned value
fmt.Println(res.Value)
fmt.Println(version)
},
}

View File

@@ -16,18 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package get
import (
"bufio"
"encoding/json"
"fmt"
"net"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
// addressCmd represents the address command
@@ -36,40 +33,14 @@ var addressCmd = &cobra.Command{
Aliases: []string{"addr"},
Short: "Get InfiniTime's bluetooth address",
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
client := viper.Get("client").(*api.Client)
address, err := client.Address()
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeBtAddress,
})
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
log.Fatal().Err(err).Msg("Error getting bluetooth address")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Decode line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
if res.Error {
log.Fatal().Msg(res.Message)
}
// Print returned value
fmt.Println(res.Value)
fmt.Println(address)
},
}

View File

@@ -16,18 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package get
import (
"bufio"
"encoding/json"
"fmt"
"net"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
// batteryCmd represents the batt command
@@ -36,40 +33,15 @@ var batteryCmd = &cobra.Command{
Aliases: []string{"batt"},
Short: "Get battery level from InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
client := viper.Get("client").(*api.Client)
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeBattLevel,
})
battLevel, err := client.BatteryLevel()
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Deocde line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
if res.Error {
log.Fatal().Msg(res.Message)
log.Fatal().Err(err).Msg("Error getting battery level")
}
// Print returned percentage
fmt.Printf("%d%%\n", int(res.Value.(float64)))
fmt.Printf("%d%%\n", battLevel)
},
}

View File

@@ -16,10 +16,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package get
import (
"github.com/spf13/cobra"
"go.arsenm.dev/itd/cmd/itctl/root"
)
// getCmd represents the get command
@@ -29,5 +30,5 @@ var getCmd = &cobra.Command{
}
func init() {
rootCmd.AddCommand(getCmd)
root.RootCmd.AddCommand(getCmd)
}

View File

@@ -16,18 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package get
import (
"bufio"
"encoding/json"
"fmt"
"net"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
// heartCmd represents the heart command
@@ -35,40 +32,15 @@ var heartCmd = &cobra.Command{
Use: "heart",
Short: "Get heart rate from InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
client := viper.Get("client").(*api.Client)
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeHeartRate,
})
heartRate, err := client.HeartRate()
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Decode line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
if res.Error {
log.Fatal().Msg(res.Message)
log.Fatal().Err(err).Msg("Error getting heart rate")
}
// Print returned BPM
fmt.Printf("%d BPM\n", int(res.Value.(float64)))
fmt.Printf("%d BPM\n", heartRate)
},
}

View File

@@ -16,19 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package get
import (
"bufio"
"encoding/json"
"fmt"
"net"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
// steps.goCmd represents the steps.go command
@@ -36,42 +32,11 @@ var motionCmd = &cobra.Command{
Use: "motion",
Short: "Get motion values from InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
client := viper.Get("client").(*api.Client)
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeMotion,
})
motionVals, err := client.Motion()
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Decode line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
var motionVals types.MotionValues
err = mapstructure.Decode(res.Value, &motionVals)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding motion values")
}
if res.Error {
log.Fatal().Msg(res.Message)
log.Fatal().Err(err).Msg("Error getting motion values")
}
if viper.GetBool("shell") {

View File

@@ -16,18 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package get
import (
"bufio"
"encoding/json"
"fmt"
"net"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
// steps.goCmd represents the steps.go command
@@ -35,40 +32,15 @@ var stepsCmd = &cobra.Command{
Use: "steps",
Short: "Get step count from InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
client := viper.Get("client").(*api.Client)
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeStepCount,
})
stepCount, err := client.StepCount()
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Decode line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
if res.Error {
log.Fatal().Msg(res.Message)
log.Fatal().Err(err).Msg("Error getting step count")
}
// Print returned BPM
fmt.Printf("%d Steps\n", int(res.Value.(float64)))
fmt.Printf("%d Steps\n", stepCount)
},
}

View File

@@ -19,9 +19,14 @@
package main
import (
"os"
_ "go.arsenm.dev/itd/cmd/itctl/firmware"
_ "go.arsenm.dev/itd/cmd/itctl/get"
_ "go.arsenm.dev/itd/cmd/itctl/notify"
"go.arsenm.dev/itd/cmd/itctl/root"
_ "go.arsenm.dev/itd/cmd/itctl/set"
_ "go.arsenm.dev/itd/cmd/itctl/watch"
"go.arsenm.dev/itd/cmd/itctl/cmd"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -32,5 +37,5 @@ func init() {
}
func main() {
cmd.Execute()
root.Execute()
}

View File

@@ -16,17 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package notify
import (
"bufio"
"encoding/json"
"net"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
"go.arsenm.dev/itd/cmd/itctl/root"
)
// notifyCmd represents the notify command
@@ -40,44 +37,15 @@ var notifyCmd = &cobra.Command{
log.Fatal().Msg("Command notify requires two arguments")
}
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
client := viper.Get("client").(*api.Client)
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeNotify,
Data: types.ReqDataNotify{
Title: args[0],
Body: args[1],
},
})
err := client.Notify(args[0], args[1])
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Decode line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
if res.Error {
log.Fatal().Msg(res.Message)
log.Fatal().Err(err).Msg("Error sending notification")
}
},
}
func init() {
rootCmd.AddCommand(notifyCmd)
root.RootCmd.AddCommand(notifyCmd)
}

View File

@@ -16,16 +16,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package root
import (
"github.com/abiosoft/ishell"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "itctl",
Short: "Control the itd daemon for InfiniTime smartwatches",
Run: func(cmd *cobra.Command, args []string) {
@@ -61,18 +63,25 @@ var rootCmd = &cobra.Command{
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
rootCmd.CompletionOptions.DisableDefaultCmd = true
cobra.CheckErr(rootCmd.Execute())
client, err := api.New(viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error connecting to socket. Is itd running?")
}
defer client.Close()
viper.Set("client", client)
RootCmd.CompletionOptions.DisableDefaultCmd = true
cobra.CheckErr(RootCmd.Execute())
}
func init() {
// Register flag for socket path
rootCmd.Flags().StringP("socket-path", "s", "", "Path to itd socket")
RootCmd.Flags().StringP("socket-path", "s", api.DefaultAddr, "Path to itd socket")
// Bind flag and environment variable to viper key
viper.BindPFlag("sockPath", rootCmd.Flags().Lookup("socket-path"))
viper.BindPFlag("sockPath", RootCmd.Flags().Lookup("socket-path"))
viper.BindEnv("sockPath", "ITCTL_SOCKET_PATH")
// Set default value for socket path
viper.SetDefault("sockPath", "/tmp/itd/socket")
viper.SetDefault("sockPath", api.DefaultAddr)
}

View File

@@ -16,10 +16,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package set
import (
"github.com/spf13/cobra"
"go.arsenm.dev/itd/cmd/itctl/root"
)
// setCmd represents the set command
@@ -29,5 +30,5 @@ var setCmd = &cobra.Command{
}
func init() {
rootCmd.AddCommand(setCmd)
root.RootCmd.AddCommand(setCmd)
}

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package set
import (
"bufio"

View File

@@ -0,0 +1,76 @@
/*
* itd uses bluetooth low energy to communicate with InfiniTime devices
* Copyright (C) 2021 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package watch
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// heartCmd represents the address command
var batteryCmd = &cobra.Command{
Use: "battery",
Aliases: []string{"batt"},
Short: "Watch InfiniTime's battery level for changes",
Run: func(cmd *cobra.Command, args []string) {
client := viper.Get("client").(*api.Client)
battLevelCh, cancel, err := client.WatchBatteryLevel()
if err != nil {
log.Fatal().Err(err).Msg("Error getting battery level channel")
}
defer cancel()
signalCh := make(chan os.Signal, 1)
go func() {
<-signalCh
cancel()
os.Exit(0)
}()
signal.Notify(signalCh,
syscall.SIGINT,
syscall.SIGTERM,
)
for battlevel := range battLevelCh {
fmt.Printf("%d%%\n", battlevel)
}
},
}
func init() {
watchCmd.AddCommand(batteryCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

75
cmd/itctl/watch/heart.go Normal file
View File

@@ -0,0 +1,75 @@
/*
* itd uses bluetooth low energy to communicate with InfiniTime devices
* Copyright (C) 2021 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package watch
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// heartCmd represents the address command
var heartCmd = &cobra.Command{
Use: "heart",
Short: "Watch InfiniTime's heart rate for changes",
Run: func(cmd *cobra.Command, args []string) {
client := viper.Get("client").(*api.Client)
heartRateCh, cancel, err := client.WatchHeartRate()
if err != nil {
log.Fatal().Err(err).Msg("Error getting heart rate channel")
}
defer cancel()
signalCh := make(chan os.Signal, 1)
go func() {
<-signalCh
cancel()
os.Exit(0)
}()
signal.Notify(signalCh,
syscall.SIGINT,
syscall.SIGTERM,
)
for heartRate := range heartRateCh {
fmt.Println(heartRate, "BPM")
}
},
}
func init() {
watchCmd.AddCommand(heartCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

86
cmd/itctl/watch/motion.go Normal file
View File

@@ -0,0 +1,86 @@
/*
* itd uses bluetooth low energy to communicate with InfiniTime devices
* Copyright (C) 2021 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package watch
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// heartCmd represents the address command
var motionCmd = &cobra.Command{
Use: "motion",
Short: "Watch InfiniTime's motion values for changes",
Run: func(cmd *cobra.Command, args []string) {
client := viper.Get("client").(*api.Client)
motionValCh, cancel, err := client.WatchMotion()
if err != nil {
log.Fatal().Err(err).Msg("Error getting motion value channel")
}
defer cancel()
signalCh := make(chan os.Signal, 1)
go func() {
<-signalCh
cancel()
os.Exit(0)
}()
signal.Notify(signalCh,
syscall.SIGINT,
syscall.SIGTERM,
)
for motionVals := range motionValCh {
if viper.GetBool("shell") {
fmt.Printf(
"X=%d\nY=%d\nZ=%d\n",
motionVals.X,
motionVals.Y,
motionVals.Z,
)
} else {
fmt.Printf("%+v\n", motionVals)
}
}
},
}
func init() {
watchCmd.AddCommand(motionCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
motionCmd.Flags().BoolP("shell", "s", false, "Output data in shell-compatible format")
viper.BindPFlag("shell", motionCmd.Flags().Lookup("shell"))
}

75
cmd/itctl/watch/steps.go Normal file
View File

@@ -0,0 +1,75 @@
/*
* itd uses bluetooth low energy to communicate with InfiniTime devices
* Copyright (C) 2021 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package watch
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// heartCmd represents the address command
var stepsCmd = &cobra.Command{
Use: "steps",
Short: "Watch InfiniTime's step count for changes",
Run: func(cmd *cobra.Command, args []string) {
client := viper.Get("client").(*api.Client)
stepCountCh, cancel, err := client.WatchStepCount()
if err != nil {
log.Fatal().Err(err).Msg("Error getting step count channel")
}
defer cancel()
signalCh := make(chan os.Signal, 1)
go func() {
<-signalCh
cancel()
os.Exit(0)
}()
signal.Notify(signalCh,
syscall.SIGINT,
syscall.SIGTERM,
)
for stepCount := range stepCountCh {
fmt.Println(stepCount, "Steps")
}
},
}
func init() {
watchCmd.AddCommand(stepsCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@@ -16,9 +16,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
package watch
type DFUProgress struct {
Received int64 `mapstructure:"recvd"`
Total int64 `mapstructure:"total"`
import (
"github.com/spf13/cobra"
"go.arsenm.dev/itd/cmd/itctl/root"
)
// watchCmd represents the watch command
var watchCmd = &cobra.Command{
Use: "watch",
Short: "Watch values from InfiniTime for changes",
}
func init() {
root.RootCmd.AddCommand(watchCmd)
}

View File

@@ -1,22 +1,17 @@
package main
import (
"bufio"
"errors"
"fmt"
"image/color"
"net"
"encoding/json"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
func infoTab(parent fyne.Window) *fyne.Container {
func infoTab(parent fyne.Window, client *api.Client) *fyne.Container {
infoLayout := container.NewVBox(
// Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent),
@@ -25,20 +20,50 @@ func infoTab(parent fyne.Window) *fyne.Container {
// Create label for heart rate
heartRateLbl := newText("0 BPM", 24)
// Creae container to store heart rate section
heartRate := container.NewVBox(
heartRateSect := container.NewVBox(
newText("Heart Rate", 12),
heartRateLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(heartRate)
infoLayout.Add(heartRateSect)
// Watch for heart rate updates
go watch(types.ReqTypeWatchHeartRate, func(data interface{}) {
// Change text of heart rate label
heartRateLbl.Text = fmt.Sprintf("%d BPM", int(data.(float64)))
// Refresh label
heartRateLbl.Refresh()
}, parent)
heartRateCh, cancel, err := client.WatchHeartRate()
if err != nil {
guiErr(err, "Error getting heart rate channel", true, parent)
}
onClose = append(onClose, cancel)
go func() {
for heartRate := range heartRateCh {
// Change text of heart rate label
heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate)
// Refresh label
heartRateLbl.Refresh()
}
}()
// Create label for heart rate
stepCountLbl := newText("0 Steps", 24)
// Creae container to store heart rate section
stepCountSect := container.NewVBox(
newText("Step Count", 12),
stepCountLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(stepCountSect)
stepCountCh, cancel, err := client.WatchStepCount()
if err != nil {
guiErr(err, "Error getting step count channel", true, parent)
}
onClose = append(onClose, cancel)
go func() {
for stepCount := range stepCountCh {
// Change text of heart rate label
stepCountLbl.Text = fmt.Sprintf("%d Steps", stepCount)
// Refresh label
stepCountLbl.Refresh()
}
}()
// Create label for battery level
battLevelLbl := newText("0%", 24)
@@ -50,32 +75,40 @@ func infoTab(parent fyne.Window) *fyne.Container {
)
infoLayout.Add(battLevel)
// Watch for changes in battery level
go watch(types.ReqTypeWatchBattLevel, func(data interface{}) {
battLevelLbl.Text = fmt.Sprintf("%d%%", int(data.(float64)))
battLevelLbl.Refresh()
}, parent)
battLevelCh, cancel, err := client.WatchBatteryLevel()
if err != nil {
guiErr(err, "Error getting battery level channel", true, parent)
}
onClose = append(onClose, cancel)
go func() {
for battLevel := range battLevelCh {
// Change text of battery level label
battLevelLbl.Text = fmt.Sprintf("%d%%", battLevel)
// Refresh label
battLevelLbl.Refresh()
}
}()
fwVerString, err := get(types.ReqTypeFwVersion)
fwVerString, err := client.Version()
if err != nil {
guiErr(err, "Error getting firmware string", true, parent)
}
fwVer := container.NewVBox(
newText("Firmware Version", 12),
newText(fwVerString.(string), 24),
newText(fwVerString, 24),
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(fwVer)
btAddrString, err := get(types.ReqTypeBtAddress)
btAddrString, err := client.Address()
if err != nil {
panic(err)
}
btAddr := container.NewVBox(
newText("Bluetooth Address", 12),
newText(btAddrString.(string), 24),
newText(btAddrString, 24),
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(btAddr)
@@ -83,65 +116,6 @@ func infoTab(parent fyne.Window) *fyne.Container {
return infoLayout
}
func watch(req int, onRecv func(data interface{}), parent fyne.Window) error {
conn, err := net.Dial("unix", SockPath)
if err != nil {
return err
}
defer conn.Close()
err = json.NewEncoder(conn).Encode(types.Request{
Type: req,
})
if err != nil {
return err
}
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
res, err := getResp(scanner.Bytes())
if err != nil {
guiErr(err, "Error getting response from connection", false, parent)
continue
}
onRecv(res.Value)
}
return nil
}
func get(req int) (interface{}, error) {
conn, err := net.Dial("unix", SockPath)
if err != nil {
return nil, err
}
defer conn.Close()
err = json.NewEncoder(conn).Encode(types.Request{
Type: req,
})
if err != nil {
return nil, err
}
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
return nil, err
}
res, err := getResp(line)
if err != nil {
return nil, err
}
return res.Value, nil
}
func getResp(line []byte) (*types.Response, error) {
var res types.Response
err := json.Unmarshal(line, &res)
if err != nil {
return nil, err
}
if res.Error {
return nil, errors.New(res.Message)
}
return &res, nil
}
func newText(t string, size float32) *canvas.Text {
text := canvas.NewText(t, theme.ForegroundColor())
text.TextSize = size

View File

@@ -1,31 +1,39 @@
package main
import (
"net"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"go.arsenm.dev/itd/api"
)
var SockPath = "/tmp/itd/socket"
var onClose []func()
func main() {
// Create new app
a := app.New()
// Create new window with title "itgui"
window := a.NewWindow("itgui")
window.SetOnClosed(func() {
for _, closeFn := range onClose {
closeFn()
}
})
_, err := net.Dial("unix", SockPath)
client, err := api.New(api.DefaultAddr)
if err != nil {
guiErr(err, "Error dialing itd socket", true, window)
guiErr(err, "Error connecting to itd", true, window)
}
onClose = append(onClose, func() {
client.Close()
})
// Create new app tabs container
tabs := container.NewAppTabs(
container.NewTabItem("Info", infoTab(window)),
container.NewTabItem("Notify", notifyTab(window)),
container.NewTabItem("Set Time", timeTab(window)),
container.NewTabItem("Upgrade", upgradeTab(window)),
container.NewTabItem("Info", infoTab(window, client)),
container.NewTabItem("Motion", motionTab(window, client)),
container.NewTabItem("Notify", notifyTab(window, client)),
container.NewTabItem("Set Time", timeTab(window, client)),
container.NewTabItem("Upgrade", upgradeTab(window, client)),
)
// Set tabs as window content

105
cmd/itgui/motion.go Normal file
View File

@@ -0,0 +1,105 @@
package main
import (
"image/color"
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
)
func motionTab(parent fyne.Window, client *api.Client) *fyne.Container {
// Create label for heart rate
xCoordLbl := newText("0", 24)
// Creae container to store heart rate section
xCoordSect := container.NewVBox(
newText("X Coordinate", 12),
xCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
// Create label for heart rate
yCoordLbl := newText("0", 24)
// Creae container to store heart rate section
yCoordSect := container.NewVBox(
newText("Y Coordinate", 12),
yCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
// Create label for heart rate
zCoordLbl := newText("0", 24)
// Creae container to store heart rate section
zCoordSect := container.NewVBox(
newText("Z Coordinate", 12),
zCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
// Create variable to keep track of whether motion started
started := false
// Create button to stop motion
stopBtn := widget.NewButton("Stop", nil)
// Create button to start motion
startBtn := widget.NewButton("Start", func() {
// if motion is started
if started {
// Do nothing
return
}
// Set motion started
started = true
// Watch motion values
motionCh, cancel, err := client.WatchMotion()
if err != nil {
guiErr(err, "Error getting heart rate channel", true, parent)
}
// Create done channel
done := make(chan struct{}, 1)
go func() {
for {
select {
case <-done:
return
case motion := <-motionCh:
// Set labels to new values
xCoordLbl.Text = strconv.Itoa(int(motion.X))
yCoordLbl.Text = strconv.Itoa(int(motion.Y))
zCoordLbl.Text = strconv.Itoa(int(motion.Z))
// Refresh labels to display new values
xCoordLbl.Refresh()
yCoordLbl.Refresh()
zCoordLbl.Refresh()
}
}
}()
// Create stop function
stopBtn.OnTapped = func() {
done <- struct{}{}
started = false
cancel()
}
})
// Run stop button function on close if possible
onClose = append(onClose, func() {
if stopBtn.OnTapped != nil {
stopBtn.OnTapped()
}
})
// Return new container containing all elements
return container.NewVBox(
// Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent),
startBtn,
stopBtn,
xCoordSect,
yCoordSect,
zCoordSect,
)
}

View File

@@ -1,17 +1,14 @@
package main
import (
"encoding/json"
"net"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
func notifyTab(parent fyne.Window) *fyne.Container {
func notifyTab(parent fyne.Window, client *api.Client) *fyne.Container {
// Create new entry for notification title
titleEntry := widget.NewEntry()
titleEntry.SetPlaceHolder("Title")
@@ -22,20 +19,11 @@ func notifyTab(parent fyne.Window) *fyne.Container {
// Create new button to send notification
sendBtn := widget.NewButton("Send", func() {
// Dial itd UNIX socket
conn, err := net.Dial("unix", SockPath)
err := client.Notify(titleEntry.Text, bodyEntry.Text)
if err != nil {
guiErr(err, "Error dialing socket", false, parent)
guiErr(err, "Error sending notification", false, parent)
return
}
// Encode notify request on connection
json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeNotify,
Data: types.ReqDataNotify{
Title: titleEntry.Text,
Body: bodyEntry.Text,
},
})
})
// Return new container containing all elements

View File

@@ -1,18 +1,16 @@
package main
import (
"encoding/json"
"net"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/api"
)
func timeTab(parent fyne.Window) *fyne.Container {
func timeTab(parent fyne.Window, client *api.Client) *fyne.Container {
// Create new entry for time string
timeEntry := widget.NewEntry()
// Set text to current time formatter properly
@@ -21,7 +19,7 @@ func timeTab(parent fyne.Window) *fyne.Container {
// Create button to set current time
currentBtn := widget.NewButton("Set Current", func() {
timeEntry.SetText(time.Now().Format(time.RFC1123))
setTime(true)
setTime(client, true)
})
// Create button to set time inside entry
@@ -33,7 +31,7 @@ func timeTab(parent fyne.Window) *fyne.Container {
return
}
// Set time to parsed time
setTime(false, parsedTime)
setTime(client, false, parsedTime)
})
// Return new container with all elements centered
@@ -48,30 +46,13 @@ func timeTab(parent fyne.Window) *fyne.Container {
// setTime sets the first element in the variadic parameter
// if current is false, otherwise, it sets the current time.
func setTime(current bool, t ...time.Time) error {
// Dial UNIX socket
conn, err := net.Dial("unix", SockPath)
if err != nil {
return err
}
defer conn.Close()
var data string
// If current is true, use the string "now"
// otherwise, use the formatted time from the
// first element in the variadic parameter.
// "now" is more accurate than formatting
// current time as only seconds are preserved
// in that case.
func setTime(client *api.Client, current bool, t ...time.Time) error {
var err error
if current {
data = "now"
err = client.SetTimeNow()
} else {
data = t[0].Format(time.RFC3339)
err = client.SetTime(t[0])
}
// Encode SetTime request with above data
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeSetTime,
Data: data,
})
if err != nil {
return err
}

View File

@@ -1,10 +1,7 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"path/filepath"
"fyne.io/fyne/v2"
@@ -13,11 +10,11 @@ import (
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/itd/api"
"go.arsenm.dev/itd/internal/types"
)
func upgradeTab(parent fyne.Window) *fyne.Container {
func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container {
var (
archivePath string
firmwarePath string
@@ -117,7 +114,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
// Resize modal to 300x100
progressDlg.Resize(fyne.NewSize(300, 100))
var fwUpgType int
var fwUpgType api.UpgradeType
var files []string
// Get appropriate upgrade type and file paths
switch upgradeTypeSelect.Selected {
@@ -129,48 +126,18 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
files = append(files, initPktPath, firmwarePath)
}
// Dial itd UNIX socket
conn, err := net.Dial("unix", SockPath)
progress, err := client.FirmwareUpgrade(fwUpgType, files...)
if err != nil {
guiErr(err, "Error dialing socket", false, parent)
guiErr(err, "Error initiating DFU", false, parent)
return
}
defer conn.Close()
// Encode firmware upgrade request to connection
json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeFwUpgrade,
Data: types.ReqDataFwUpgrade{
Type: fwUpgType,
Files: files,
},
})
// Show progress dialog
progressDlg.Show()
// Hide progress dialog after completion
defer progressDlg.Hide()
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
var res types.Response
// Decode scanned line into response struct
err = json.Unmarshal(scanner.Bytes(), &res)
if err != nil {
guiErr(err, "Error decoding response", false, parent)
return
}
if res.Error {
guiErr(err, "Error returned in response", false, parent)
return
}
var event types.DFUProgress
// Decode response data into progress struct
err = mapstructure.Decode(res.Value, &event)
if err != nil {
guiErr(err, "Error decoding response value", false, parent)
return
}
for event := range progress {
// Set label text to received / total B
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
// Set progress bar values
@@ -179,7 +146,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
// Refresh progress bar
progressBar.Refresh()
// If transfer finished, break
if event.Received == event.Total {
if event.Sent == event.Total {
break
}
}

3
go.mod
View File

@@ -13,6 +13,7 @@ require (
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect
github.com/godbus/dbus/v5 v5.0.5
github.com/google/uuid v1.1.2
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.4.2
@@ -24,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-20211022195951-45baea10486b
go.arsenm.dev/infinitime v0.0.0-20211023042633-53aa6f8a0c72
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

5
go.sum
View File

@@ -193,6 +193,7 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@@ -366,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-20211022195951-45baea10486b h1:2VitKPwSYSWXmL5BH88nfTPLSIYPCt4yubpEJHhcQBc=
go.arsenm.dev/infinitime v0.0.0-20211022195951-45baea10486b/go.mod h1:gaepaueUz4J5FfxuV19B4w5pi+V3mD0LTef50ryxr/Q=
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.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

@@ -14,6 +14,7 @@ const (
ReqTypeWatchMotion
ReqTypeStepCount
ReqTypeWatchStepCount
ReqTypeCancel
)
const (
@@ -27,8 +28,10 @@ type ReqDataFwUpgrade struct {
}
type Response struct {
Type int `json:"type"`
Value interface{} `json:"value,omitempty"`
Message string `json:"msg,omitempty"`
ID string `json:"id,omitempty"`
Error bool `json:"error"`
}
@@ -45,6 +48,7 @@ type ReqDataNotify struct {
type DFUProgress struct {
Received int64 `mapstructure:"recvd"`
Total int64 `mapstructure:"total"`
Sent int64 `mapstructure:"sent"`
}
type MotionValues struct {

208
socket.go
View File

@@ -27,6 +27,7 @@ import (
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
@@ -35,6 +36,29 @@ import (
"go.arsenm.dev/itd/translit"
)
type DoneMap map[string]chan struct{}
func (dm DoneMap) Exists(key string) bool {
_, ok := dm[key]
return ok
}
func (dm DoneMap) Done(key string) {
ch := dm[key]
ch <- struct{}{}
}
func (dm DoneMap) Create(key string) {
dm[key] = make(chan struct{}, 1)
}
func (dm DoneMap) Remove(key string) {
close(dm[key])
delete(dm, key)
}
var done = DoneMap{}
func startSocket(dev *infinitime.Device) error {
// Make socket directory if non-existant
err := os.MkdirAll(filepath.Dir(viper.GetString("socket.path")), 0755)
@@ -75,11 +99,6 @@ func startSocket(dev *infinitime.Device) error {
func handleConnection(conn net.Conn, dev *infinitime.Device) {
defer conn.Close()
// If firmware is updating, return error
if firmwareUpdating {
connErr(conn, nil, "Firmware update in progress")
return
}
// Create new scanner on connection
scanner := bufio.NewScanner(conn)
@@ -88,134 +107,203 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Decode scanned message into types.Request
err := json.Unmarshal(scanner.Bytes(), &req)
if err != nil {
connErr(conn, err, "Error decoding JSON input")
connErr(conn, req.Type, err, "Error decoding JSON input")
continue
}
// If firmware is updating, return error
if firmwareUpdating {
connErr(conn, req.Type, nil, "Firmware update in progress")
return
}
switch req.Type {
case types.ReqTypeHeartRate:
// Get heart rate from watch
heartRate, err := dev.HeartRate()
if err != nil {
connErr(conn, err, "Error getting heart rate")
connErr(conn, req.Type, err, "Error getting heart rate")
break
}
// Encode heart rate to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: heartRate,
})
case types.ReqTypeWatchHeartRate:
heartRateCh, err := dev.WatchHeartRate()
heartRateCh, cancel, err := dev.WatchHeartRate()
if err != nil {
connErr(conn, err, "Error getting heart rate channel")
connErr(conn, req.Type, err, "Error getting heart rate channel")
break
}
reqID := uuid.New().String()
go func() {
done.Create(reqID)
// For every heart rate value
for heartRate := range heartRateCh {
json.NewEncoder(conn).Encode(types.Response{
Value: heartRate,
})
select {
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
ID: reqID,
Value: heartRate,
})
}
}
}()
case types.ReqTypeBattLevel:
// Get battery level from watch
battLevel, err := dev.BatteryLevel()
if err != nil {
connErr(conn, err, "Error getting battery level")
connErr(conn, req.Type, err, "Error getting battery level")
break
}
// Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: battLevel,
})
case types.ReqTypeWatchBattLevel:
battLevelCh, err := dev.WatchBatteryLevel()
battLevelCh, cancel, err := dev.WatchBatteryLevel()
if err != nil {
connErr(conn, err, "Error getting heart rate channel")
connErr(conn, req.Type, err, "Error getting battery level channel")
break
}
reqID := uuid.New().String()
go func() {
done.Create(reqID)
// For every battery level value
for battLevel := range battLevelCh {
json.NewEncoder(conn).Encode(types.Response{
Value: battLevel,
})
select {
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
ID: reqID,
Value: battLevel,
})
}
}
}()
case types.ReqTypeMotion:
// Get battery level from watch
motionVals, err := dev.Motion()
if err != nil {
connErr(conn, err, "Error getting motion values")
connErr(conn, req.Type, err, "Error getting motion values")
break
}
// Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: motionVals,
})
case types.ReqTypeWatchMotion:
motionValCh, _, err := dev.WatchMotion()
motionValCh, cancel, err := dev.WatchMotion()
if err != nil {
connErr(conn, err, "Error getting heart rate channel")
connErr(conn, req.Type, err, "Error getting heart rate channel")
break
}
reqID := uuid.New().String()
go func() {
done.Create(reqID)
// For every motion event
for motionVals := range motionValCh {
json.NewEncoder(conn).Encode(types.Response{
Value: motionVals,
})
select {
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
ID: reqID,
Value: motionVals,
})
}
}
}()
case types.ReqTypeStepCount:
// Get battery level from watch
stepCount, err := dev.StepCount()
if err != nil {
connErr(conn, err, "Error getting step count")
connErr(conn, req.Type, err, "Error getting step count")
break
}
// Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: stepCount,
})
case types.ReqTypeWatchStepCount:
stepCountCh, _, err := dev.WatchStepCount()
stepCountCh, cancel, err := dev.WatchStepCount()
if err != nil {
connErr(conn, err, "Error getting heart rate channel")
connErr(conn, req.Type, err, "Error getting heart rate channel")
break
}
reqID := uuid.New().String()
go func() {
done.Create(reqID)
// For every step count value
for stepCount := range stepCountCh {
json.NewEncoder(conn).Encode(types.Response{
Value: stepCount,
})
select {
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
ID: reqID,
Value: stepCount,
})
}
}
}()
case types.ReqTypeFwVersion:
// Get firmware version from watch
version, err := dev.Version()
if err != nil {
connErr(conn, err, "Error getting battery level")
connErr(conn, req.Type, err, "Error getting firmware version")
break
}
// Encode version to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: version,
})
case types.ReqTypeBtAddress:
// Encode bluetooth address to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: dev.Address(),
})
case types.ReqTypeNotify:
// If no data, return error
if req.Data == nil {
connErr(conn, nil, "Data required for notify request")
connErr(conn, req.Type, nil, "Data required for notify request")
break
}
var reqData types.ReqDataNotify
// Decode data map to notify request data
err = mapstructure.Decode(req.Data, &reqData)
if err != nil {
connErr(conn, err, "Error decoding request data")
connErr(conn, req.Type, err, "Error decoding request data")
break
}
maps := viper.GetStringSlice("notifs.translit.use")
@@ -225,21 +313,21 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Send notification to watch
err = dev.Notify(title, body)
if err != nil {
connErr(conn, err, "Error sending notification")
connErr(conn, req.Type, err, "Error sending notification")
break
}
// Encode empty types.Response to connection
json.NewEncoder(conn).Encode(types.Response{})
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.ReqTypeSetTime:
// If no data, return error
if req.Data == nil {
connErr(conn, nil, "Data required for settime request")
connErr(conn, req.Type, nil, "Data required for settime request")
break
}
// Get string from data or return error
reqTimeStr, ok := req.Data.(string)
if !ok {
connErr(conn, nil, "Data for settime request must be RFC3339 formatted time string")
connErr(conn, req.Type, nil, "Data for settime request must be RFC3339 formatted time string")
break
}
@@ -250,29 +338,29 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Parse time as RFC3339/ISO8601
reqTime, err = time.Parse(time.RFC3339, reqTimeStr)
if err != nil {
connErr(conn, err, "Invalid time format. Time string must be formatted as ISO8601 or the word `now`")
connErr(conn, req.Type, err, "Invalid time format. Time string must be formatted as ISO8601 or the word `now`")
break
}
}
// Set time on watch
err = dev.SetTime(reqTime)
if err != nil {
connErr(conn, err, "Error setting device time")
connErr(conn, req.Type, err, "Error setting device time")
break
}
// Encode empty types.Response to connection
json.NewEncoder(conn).Encode(types.Response{})
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.ReqTypeFwUpgrade:
// If no data, return error
if req.Data == nil {
connErr(conn, nil, "Data required for firmware upgrade request")
connErr(conn, req.Type, nil, "Data required for firmware upgrade request")
break
}
var reqData types.ReqDataFwUpgrade
// Decode data map to firmware upgrade request data
err = mapstructure.Decode(req.Data, &reqData)
if err != nil {
connErr(conn, err, "Error decoding request data")
connErr(conn, req.Type, err, "Error decoding request data")
break
}
// Reset DFU to prepare for next update
@@ -281,40 +369,40 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
case types.UpgradeTypeArchive:
// If less than one file, return error
if len(reqData.Files) < 1 {
connErr(conn, nil, "Archive upgrade requires one file with .zip extension")
connErr(conn, req.Type, nil, "Archive upgrade requires one file with .zip extension")
break
}
// If file is not zip archive, return error
if filepath.Ext(reqData.Files[0]) != ".zip" {
connErr(conn, nil, "Archive upgrade file must be a zip archive")
connErr(conn, req.Type, nil, "Archive upgrade file must be a zip archive")
break
}
// Load DFU archive
err := dev.DFU.LoadArchive(reqData.Files[0])
if err != nil {
connErr(conn, err, "Error loading archive file")
connErr(conn, req.Type, err, "Error loading archive file")
break
}
case types.UpgradeTypeFiles:
// If less than two files, return error
if len(reqData.Files) < 2 {
connErr(conn, nil, "Files upgrade requires two files. First with .dat and second with .bin extension.")
connErr(conn, req.Type, nil, "Files upgrade requires two files. First with .dat and second with .bin extension.")
break
}
// If first file is not init packet, return error
if filepath.Ext(reqData.Files[0]) != ".dat" {
connErr(conn, nil, "First file must be a .dat file")
connErr(conn, req.Type, nil, "First file must be a .dat file")
break
}
// If second file is not firmware image, return error
if filepath.Ext(reqData.Files[1]) != ".bin" {
connErr(conn, nil, "Second file must be a .bin file")
connErr(conn, req.Type, nil, "Second file must be a .bin file")
break
}
// Load individual DFU files
err := dev.DFU.LoadFiles(reqData.Files[0], reqData.Files[1])
if err != nil {
connErr(conn, err, "Error loading firmware files")
connErr(conn, req.Type, err, "Error loading firmware files")
break
}
}
@@ -326,9 +414,11 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
for event := range progress {
// Encode event on connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: event,
})
}
firmwareUpdating = false
}()
// Set firmwareUpdating
@@ -336,16 +426,30 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Start DFU
err = dev.DFU.Start()
if err != nil {
connErr(conn, err, "Error performing upgrade")
connErr(conn, req.Type, err, "Error performing upgrade")
firmwareUpdating = false
break
}
firmwareUpdating = false
case types.ReqTypeCancel:
if req.Data == nil {
connErr(conn, req.Type, nil, "No data provided. Cancel request requires request ID string as data.")
continue
}
reqID, ok := req.Data.(string)
if !ok {
connErr(conn, req.Type, nil, "Invalid data. Cancel request required request ID string as data.")
}
// Stop notifications
done.Done(reqID)
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
default:
connErr(conn, req.Type, nil, fmt.Sprintf("Unknown request type %d", req.Type))
}
}
}
func connErr(conn net.Conn, err error, msg string) {
func connErr(conn net.Conn, resType int, err error, msg string) {
var res types.Response
// If error exists, add to types.Response, otherwise don't
if err != nil {
@@ -353,7 +457,7 @@ func connErr(conn net.Conn, err error, msg string) {
res = types.Response{Message: fmt.Sprintf("%s: %s", msg, err)}
} else {
log.Error().Msg(msg)
res = types.Response{Message: msg}
res = types.Response{Message: msg, Type: resType}
}
res.Error = true