8 Commits

16 changed files with 605 additions and 249 deletions

View File

@@ -90,15 +90,15 @@ func (c *Client) requestNoRes(req types.Request) error {
// handleResp handles the received response as needed
func (c *Client) handleResp(res types.Response) error {
switch res.Type {
case types.ResTypeWatchHeartRate:
case types.ReqTypeWatchHeartRate:
c.heartRateCh <- res
case types.ResTypeWatchBattLevel:
case types.ReqTypeWatchBattLevel:
c.battLevelCh <- res
case types.ResTypeWatchStepCount:
case types.ReqTypeWatchStepCount:
c.stepCountCh <- res
case types.ResTypeWatchMotion:
case types.ReqTypeWatchMotion:
c.motionCh <- res
case types.ResTypeDFUProgress:
case types.ReqTypeFwUpgrade:
c.dfuProgressCh <- res
default:
c.respCh <- res

View File

@@ -48,7 +48,7 @@ func (c *Client) BatteryLevel() (uint8, error) {
func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) {
c.battLevelCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeBattLevel,
Type: types.ReqTypeWatchBattLevel,
})
if err != nil {
return nil, nil, err

View File

@@ -24,6 +24,7 @@ import (
_ "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"
"os"

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")
}

34
cmd/itctl/watch/watch.go Normal file
View File

@@ -0,0 +1,34 @@
/*
* 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 (
"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{}) {
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", int(data.(float64)))
heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate)
// Refresh label
heartRateLbl.Refresh()
}, parent)
}
}()
// 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)))
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()
}, parent)
}
}()
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")
_, err := net.Dial("unix", SockPath)
if err != nil {
guiErr(err, "Error dialing itd socket", true, window)
window.SetOnClosed(func() {
for _, closeFn := range onClose {
closeFn()
}
})
client, err := api.New(api.DefaultAddr)
if err != nil {
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
}
}

View File

@@ -17,23 +17,6 @@ const (
ReqTypeCancel
)
const (
ResTypeHeartRate = iota
ResTypeBattLevel
ResTypeFwVersion
ResTypeDFUProgress
ResTypeBtAddress
ResTypeNotify
ResTypeSetTime
ResTypeWatchHeartRate
ResTypeWatchBattLevel
ResTypeMotion
ResTypeWatchMotion
ResTypeStepCount
ResTypeWatchStepCount
ResTypeCancel
)
const (
UpgradeTypeArchive = iota
UpgradeTypeFiles

103
socket.go
View File

@@ -99,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)
@@ -112,27 +107,33 @@ 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: types.ResTypeHeartRate,
Type: req.Type,
Value: heartRate,
})
case types.ReqTypeWatchHeartRate:
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()
@@ -149,7 +150,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchHeartRate,
Type: req.Type,
ID: reqID,
Value: heartRate,
})
@@ -160,18 +161,18 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// 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: types.ResTypeBattLevel,
Type: req.Type,
Value: battLevel,
})
case types.ReqTypeWatchBattLevel:
battLevelCh, cancel, err := dev.WatchBatteryLevel()
if err != nil {
connErr(conn, err, "Error getting battery level channel")
connErr(conn, req.Type, err, "Error getting battery level channel")
break
}
reqID := uuid.New().String()
@@ -188,7 +189,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchBattLevel,
Type: req.Type,
ID: reqID,
Value: battLevel,
})
@@ -199,18 +200,18 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// 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: types.ResTypeMotion,
Type: req.Type,
Value: motionVals,
})
case types.ReqTypeWatchMotion:
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()
@@ -228,7 +229,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchMotion,
Type: req.Type,
ID: reqID,
Value: motionVals,
})
@@ -239,18 +240,18 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// 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: types.ResTypeStepCount,
Type: req.Type,
Value: stepCount,
})
case types.ReqTypeWatchStepCount:
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()
@@ -267,7 +268,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchStepCount,
Type: req.Type,
ID: reqID,
Value: stepCount,
})
@@ -278,31 +279,31 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Get firmware version from watch
version, err := dev.Version()
if err != nil {
connErr(conn, err, "Error getting firmware version")
connErr(conn, req.Type, err, "Error getting firmware version")
break
}
// Encode version to connection
json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeFwVersion,
Type: req.Type,
Value: version,
})
case types.ReqTypeBtAddress:
// Encode bluetooth address to connection
json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeBtAddress,
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")
@@ -312,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{Type: types.ResTypeNotify})
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
}
@@ -337,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{Type: types.ResTypeSetTime})
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
@@ -368,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
}
}
@@ -413,7 +414,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
for event := range progress {
// Encode event on connection
json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeDFUProgress,
Type: req.Type,
Value: event,
})
}
@@ -425,28 +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, nil, "No data provided. Cancel request requires request ID string as data.")
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, nil, "Invalid data. Cancel request required request ID string as data.")
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: types.ResTypeCancel})
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 {
@@ -454,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