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 // handleResp handles the received response as needed
func (c *Client) handleResp(res types.Response) error { func (c *Client) handleResp(res types.Response) error {
switch res.Type { switch res.Type {
case types.ResTypeWatchHeartRate: case types.ReqTypeWatchHeartRate:
c.heartRateCh <- res c.heartRateCh <- res
case types.ResTypeWatchBattLevel: case types.ReqTypeWatchBattLevel:
c.battLevelCh <- res c.battLevelCh <- res
case types.ResTypeWatchStepCount: case types.ReqTypeWatchStepCount:
c.stepCountCh <- res c.stepCountCh <- res
case types.ResTypeWatchMotion: case types.ReqTypeWatchMotion:
c.motionCh <- res c.motionCh <- res
case types.ResTypeDFUProgress: case types.ReqTypeFwUpgrade:
c.dfuProgressCh <- res c.dfuProgressCh <- res
default: default:
c.respCh <- res c.respCh <- res

View File

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

View File

@@ -24,6 +24,7 @@ import (
_ "go.arsenm.dev/itd/cmd/itctl/notify" _ "go.arsenm.dev/itd/cmd/itctl/notify"
"go.arsenm.dev/itd/cmd/itctl/root" "go.arsenm.dev/itd/cmd/itctl/root"
_ "go.arsenm.dev/itd/cmd/itctl/set" _ "go.arsenm.dev/itd/cmd/itctl/set"
_ "go.arsenm.dev/itd/cmd/itctl/watch"
"os" "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 package main
import ( import (
"bufio"
"errors"
"fmt" "fmt"
"image/color" "image/color"
"net"
"encoding/json"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme" "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( infoLayout := container.NewVBox(
// Add rectangle for a bit of padding // Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent), canvas.NewRectangle(color.Transparent),
@@ -25,20 +20,50 @@ func infoTab(parent fyne.Window) *fyne.Container {
// Create label for heart rate // Create label for heart rate
heartRateLbl := newText("0 BPM", 24) heartRateLbl := newText("0 BPM", 24)
// Creae container to store heart rate section // Creae container to store heart rate section
heartRate := container.NewVBox( heartRateSect := container.NewVBox(
newText("Heart Rate", 12), newText("Heart Rate", 12),
heartRateLbl, heartRateLbl,
canvas.NewLine(theme.ShadowColor()), canvas.NewLine(theme.ShadowColor()),
) )
infoLayout.Add(heartRate) infoLayout.Add(heartRateSect)
// Watch for heart rate updates heartRateCh, cancel, err := client.WatchHeartRate()
go watch(types.ReqTypeWatchHeartRate, func(data interface{}) { if err != nil {
// Change text of heart rate label guiErr(err, "Error getting heart rate channel", true, parent)
heartRateLbl.Text = fmt.Sprintf("%d BPM", int(data.(float64))) }
// Refresh label onClose = append(onClose, cancel)
heartRateLbl.Refresh() go func() {
}, parent) 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 // Create label for battery level
battLevelLbl := newText("0%", 24) battLevelLbl := newText("0%", 24)
@@ -50,32 +75,40 @@ func infoTab(parent fyne.Window) *fyne.Container {
) )
infoLayout.Add(battLevel) infoLayout.Add(battLevel)
// Watch for changes in battery level battLevelCh, cancel, err := client.WatchBatteryLevel()
go watch(types.ReqTypeWatchBattLevel, func(data interface{}) { if err != nil {
battLevelLbl.Text = fmt.Sprintf("%d%%", int(data.(float64))) guiErr(err, "Error getting battery level channel", true, parent)
battLevelLbl.Refresh() }
}, 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 { if err != nil {
guiErr(err, "Error getting firmware string", true, parent) guiErr(err, "Error getting firmware string", true, parent)
} }
fwVer := container.NewVBox( fwVer := container.NewVBox(
newText("Firmware Version", 12), newText("Firmware Version", 12),
newText(fwVerString.(string), 24), newText(fwVerString, 24),
canvas.NewLine(theme.ShadowColor()), canvas.NewLine(theme.ShadowColor()),
) )
infoLayout.Add(fwVer) infoLayout.Add(fwVer)
btAddrString, err := get(types.ReqTypeBtAddress) btAddrString, err := client.Address()
if err != nil { if err != nil {
panic(err) panic(err)
} }
btAddr := container.NewVBox( btAddr := container.NewVBox(
newText("Bluetooth Address", 12), newText("Bluetooth Address", 12),
newText(btAddrString.(string), 24), newText(btAddrString, 24),
canvas.NewLine(theme.ShadowColor()), canvas.NewLine(theme.ShadowColor()),
) )
infoLayout.Add(btAddr) infoLayout.Add(btAddr)
@@ -83,65 +116,6 @@ func infoTab(parent fyne.Window) *fyne.Container {
return infoLayout 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 { func newText(t string, size float32) *canvas.Text {
text := canvas.NewText(t, theme.ForegroundColor()) text := canvas.NewText(t, theme.ForegroundColor())
text.TextSize = size text.TextSize = size

View File

@@ -1,31 +1,39 @@
package main package main
import ( import (
"net"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"go.arsenm.dev/itd/api"
) )
var SockPath = "/tmp/itd/socket" var onClose []func()
func main() { func main() {
// Create new app // Create new app
a := app.New() a := app.New()
// Create new window with title "itgui" // Create new window with title "itgui"
window := a.NewWindow("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 { 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 // Create new app tabs container
tabs := container.NewAppTabs( tabs := container.NewAppTabs(
container.NewTabItem("Info", infoTab(window)), container.NewTabItem("Info", infoTab(window, client)),
container.NewTabItem("Notify", notifyTab(window)), container.NewTabItem("Motion", motionTab(window, client)),
container.NewTabItem("Set Time", timeTab(window)), container.NewTabItem("Notify", notifyTab(window, client)),
container.NewTabItem("Upgrade", upgradeTab(window)), container.NewTabItem("Set Time", timeTab(window, client)),
container.NewTabItem("Upgrade", upgradeTab(window, client)),
) )
// Set tabs as window content // 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 package main
import ( import (
"encoding/json"
"net"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "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 // Create new entry for notification title
titleEntry := widget.NewEntry() titleEntry := widget.NewEntry()
titleEntry.SetPlaceHolder("Title") titleEntry.SetPlaceHolder("Title")
@@ -22,20 +19,11 @@ func notifyTab(parent fyne.Window) *fyne.Container {
// Create new button to send notification // Create new button to send notification
sendBtn := widget.NewButton("Send", func() { sendBtn := widget.NewButton("Send", func() {
// Dial itd UNIX socket err := client.Notify(titleEntry.Text, bodyEntry.Text)
conn, err := net.Dial("unix", SockPath)
if err != nil { if err != nil {
guiErr(err, "Error dialing socket", false, parent) guiErr(err, "Error sending notification", false, parent)
return 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 // Return new container containing all elements

View File

@@ -1,18 +1,16 @@
package main package main
import ( import (
"encoding/json"
"net"
"time" "time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "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 // Create new entry for time string
timeEntry := widget.NewEntry() timeEntry := widget.NewEntry()
// Set text to current time formatter properly // Set text to current time formatter properly
@@ -21,7 +19,7 @@ func timeTab(parent fyne.Window) *fyne.Container {
// Create button to set current time // Create button to set current time
currentBtn := widget.NewButton("Set Current", func() { currentBtn := widget.NewButton("Set Current", func() {
timeEntry.SetText(time.Now().Format(time.RFC1123)) timeEntry.SetText(time.Now().Format(time.RFC1123))
setTime(true) setTime(client, true)
}) })
// Create button to set time inside entry // Create button to set time inside entry
@@ -33,7 +31,7 @@ func timeTab(parent fyne.Window) *fyne.Container {
return return
} }
// Set time to parsed time // Set time to parsed time
setTime(false, parsedTime) setTime(client, false, parsedTime)
}) })
// Return new container with all elements centered // 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 // setTime sets the first element in the variadic parameter
// if current is false, otherwise, it sets the current time. // if current is false, otherwise, it sets the current time.
func setTime(current bool, t ...time.Time) error { func setTime(client *api.Client, current bool, t ...time.Time) error {
// Dial UNIX socket var err error
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.
if current { if current {
data = "now" err = client.SetTimeNow()
} else { } 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 { if err != nil {
return err return err
} }

View File

@@ -1,10 +1,7 @@
package main package main
import ( import (
"bufio"
"encoding/json"
"fmt" "fmt"
"net"
"path/filepath" "path/filepath"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
@@ -13,11 +10,11 @@ import (
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/mitchellh/mapstructure" "go.arsenm.dev/itd/api"
"go.arsenm.dev/itd/internal/types" "go.arsenm.dev/itd/internal/types"
) )
func upgradeTab(parent fyne.Window) *fyne.Container { func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container {
var ( var (
archivePath string archivePath string
firmwarePath string firmwarePath string
@@ -117,7 +114,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
// Resize modal to 300x100 // Resize modal to 300x100
progressDlg.Resize(fyne.NewSize(300, 100)) progressDlg.Resize(fyne.NewSize(300, 100))
var fwUpgType int var fwUpgType api.UpgradeType
var files []string var files []string
// Get appropriate upgrade type and file paths // Get appropriate upgrade type and file paths
switch upgradeTypeSelect.Selected { switch upgradeTypeSelect.Selected {
@@ -129,48 +126,18 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
files = append(files, initPktPath, firmwarePath) files = append(files, initPktPath, firmwarePath)
} }
// Dial itd UNIX socket progress, err := client.FirmwareUpgrade(fwUpgType, files...)
conn, err := net.Dial("unix", SockPath)
if err != nil { if err != nil {
guiErr(err, "Error dialing socket", false, parent) guiErr(err, "Error initiating DFU", false, parent)
return 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 // Show progress dialog
progressDlg.Show() progressDlg.Show()
// Hide progress dialog after completion // Hide progress dialog after completion
defer progressDlg.Hide() defer progressDlg.Hide()
scanner := bufio.NewScanner(conn) for event := range progress {
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
}
// Set label text to received / total B // Set label text to received / total B
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total)) progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
// Set progress bar values // Set progress bar values
@@ -179,7 +146,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
// Refresh progress bar // Refresh progress bar
progressBar.Refresh() progressBar.Refresh()
// If transfer finished, break // If transfer finished, break
if event.Received == event.Total { if event.Sent == event.Total {
break break
} }
} }

View File

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

103
socket.go
View File

@@ -99,11 +99,6 @@ func startSocket(dev *infinitime.Device) error {
func handleConnection(conn net.Conn, dev *infinitime.Device) { func handleConnection(conn net.Conn, dev *infinitime.Device) {
defer conn.Close() defer conn.Close()
// If firmware is updating, return error
if firmwareUpdating {
connErr(conn, nil, "Firmware update in progress")
return
}
// Create new scanner on connection // Create new scanner on connection
scanner := bufio.NewScanner(conn) scanner := bufio.NewScanner(conn)
@@ -112,27 +107,33 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Decode scanned message into types.Request // Decode scanned message into types.Request
err := json.Unmarshal(scanner.Bytes(), &req) err := json.Unmarshal(scanner.Bytes(), &req)
if err != nil { if err != nil {
connErr(conn, err, "Error decoding JSON input") connErr(conn, req.Type, err, "Error decoding JSON input")
continue continue
} }
// If firmware is updating, return error
if firmwareUpdating {
connErr(conn, req.Type, nil, "Firmware update in progress")
return
}
switch req.Type { switch req.Type {
case types.ReqTypeHeartRate: case types.ReqTypeHeartRate:
// Get heart rate from watch // Get heart rate from watch
heartRate, err := dev.HeartRate() heartRate, err := dev.HeartRate()
if err != nil { if err != nil {
connErr(conn, err, "Error getting heart rate") connErr(conn, req.Type, err, "Error getting heart rate")
break break
} }
// Encode heart rate to connection // Encode heart rate to connection
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeHeartRate, Type: req.Type,
Value: heartRate, Value: heartRate,
}) })
case types.ReqTypeWatchHeartRate: case types.ReqTypeWatchHeartRate:
heartRateCh, cancel, err := dev.WatchHeartRate() heartRateCh, cancel, err := dev.WatchHeartRate()
if err != nil { if err != nil {
connErr(conn, err, "Error getting heart rate channel") connErr(conn, req.Type, err, "Error getting heart rate channel")
break break
} }
reqID := uuid.New().String() reqID := uuid.New().String()
@@ -149,7 +150,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default: default:
// Encode response to connection if no done signal received // Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchHeartRate, Type: req.Type,
ID: reqID, ID: reqID,
Value: heartRate, Value: heartRate,
}) })
@@ -160,18 +161,18 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Get battery level from watch // Get battery level from watch
battLevel, err := dev.BatteryLevel() battLevel, err := dev.BatteryLevel()
if err != nil { if err != nil {
connErr(conn, err, "Error getting battery level") connErr(conn, req.Type, err, "Error getting battery level")
break break
} }
// Encode battery level to connection // Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeBattLevel, Type: req.Type,
Value: battLevel, Value: battLevel,
}) })
case types.ReqTypeWatchBattLevel: case types.ReqTypeWatchBattLevel:
battLevelCh, cancel, err := dev.WatchBatteryLevel() battLevelCh, cancel, err := dev.WatchBatteryLevel()
if err != nil { if err != nil {
connErr(conn, err, "Error getting battery level channel") connErr(conn, req.Type, err, "Error getting battery level channel")
break break
} }
reqID := uuid.New().String() reqID := uuid.New().String()
@@ -188,7 +189,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default: default:
// Encode response to connection if no done signal received // Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchBattLevel, Type: req.Type,
ID: reqID, ID: reqID,
Value: battLevel, Value: battLevel,
}) })
@@ -199,18 +200,18 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Get battery level from watch // Get battery level from watch
motionVals, err := dev.Motion() motionVals, err := dev.Motion()
if err != nil { if err != nil {
connErr(conn, err, "Error getting motion values") connErr(conn, req.Type, err, "Error getting motion values")
break break
} }
// Encode battery level to connection // Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeMotion, Type: req.Type,
Value: motionVals, Value: motionVals,
}) })
case types.ReqTypeWatchMotion: case types.ReqTypeWatchMotion:
motionValCh, cancel, err := dev.WatchMotion() motionValCh, cancel, err := dev.WatchMotion()
if err != nil { if err != nil {
connErr(conn, err, "Error getting heart rate channel") connErr(conn, req.Type, err, "Error getting heart rate channel")
break break
} }
reqID := uuid.New().String() reqID := uuid.New().String()
@@ -228,7 +229,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default: default:
// Encode response to connection if no done signal received // Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchMotion, Type: req.Type,
ID: reqID, ID: reqID,
Value: motionVals, Value: motionVals,
}) })
@@ -239,18 +240,18 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Get battery level from watch // Get battery level from watch
stepCount, err := dev.StepCount() stepCount, err := dev.StepCount()
if err != nil { if err != nil {
connErr(conn, err, "Error getting step count") connErr(conn, req.Type, err, "Error getting step count")
break break
} }
// Encode battery level to connection // Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeStepCount, Type: req.Type,
Value: stepCount, Value: stepCount,
}) })
case types.ReqTypeWatchStepCount: case types.ReqTypeWatchStepCount:
stepCountCh, cancel, err := dev.WatchStepCount() stepCountCh, cancel, err := dev.WatchStepCount()
if err != nil { if err != nil {
connErr(conn, err, "Error getting heart rate channel") connErr(conn, req.Type, err, "Error getting heart rate channel")
break break
} }
reqID := uuid.New().String() reqID := uuid.New().String()
@@ -267,7 +268,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
default: default:
// Encode response to connection if no done signal received // Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeWatchStepCount, Type: req.Type,
ID: reqID, ID: reqID,
Value: stepCount, Value: stepCount,
}) })
@@ -278,31 +279,31 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Get firmware version from watch // Get firmware version from watch
version, err := dev.Version() version, err := dev.Version()
if err != nil { if err != nil {
connErr(conn, err, "Error getting firmware version") connErr(conn, req.Type, err, "Error getting firmware version")
break break
} }
// Encode version to connection // Encode version to connection
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeFwVersion, Type: req.Type,
Value: version, Value: version,
}) })
case types.ReqTypeBtAddress: case types.ReqTypeBtAddress:
// Encode bluetooth address to connection // Encode bluetooth address to connection
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeBtAddress, Type: req.Type,
Value: dev.Address(), Value: dev.Address(),
}) })
case types.ReqTypeNotify: case types.ReqTypeNotify:
// If no data, return error // If no data, return error
if req.Data == nil { if req.Data == nil {
connErr(conn, nil, "Data required for notify request") connErr(conn, req.Type, nil, "Data required for notify request")
break break
} }
var reqData types.ReqDataNotify var reqData types.ReqDataNotify
// Decode data map to notify request data // Decode data map to notify request data
err = mapstructure.Decode(req.Data, &reqData) err = mapstructure.Decode(req.Data, &reqData)
if err != nil { if err != nil {
connErr(conn, err, "Error decoding request data") connErr(conn, req.Type, err, "Error decoding request data")
break break
} }
maps := viper.GetStringSlice("notifs.translit.use") maps := viper.GetStringSlice("notifs.translit.use")
@@ -312,21 +313,21 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Send notification to watch // Send notification to watch
err = dev.Notify(title, body) err = dev.Notify(title, body)
if err != nil { if err != nil {
connErr(conn, err, "Error sending notification") connErr(conn, req.Type, err, "Error sending notification")
break break
} }
// Encode empty types.Response to connection // 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: case types.ReqTypeSetTime:
// If no data, return error // If no data, return error
if req.Data == nil { if req.Data == nil {
connErr(conn, nil, "Data required for settime request") connErr(conn, req.Type, nil, "Data required for settime request")
break break
} }
// Get string from data or return error // Get string from data or return error
reqTimeStr, ok := req.Data.(string) reqTimeStr, ok := req.Data.(string)
if !ok { 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 break
} }
@@ -337,29 +338,29 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Parse time as RFC3339/ISO8601 // Parse time as RFC3339/ISO8601
reqTime, err = time.Parse(time.RFC3339, reqTimeStr) reqTime, err = time.Parse(time.RFC3339, reqTimeStr)
if err != nil { 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 break
} }
} }
// Set time on watch // Set time on watch
err = dev.SetTime(reqTime) err = dev.SetTime(reqTime)
if err != nil { if err != nil {
connErr(conn, err, "Error setting device time") connErr(conn, req.Type, err, "Error setting device time")
break break
} }
// Encode empty types.Response to connection // 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: case types.ReqTypeFwUpgrade:
// If no data, return error // If no data, return error
if req.Data == nil { 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 break
} }
var reqData types.ReqDataFwUpgrade var reqData types.ReqDataFwUpgrade
// Decode data map to firmware upgrade request data // Decode data map to firmware upgrade request data
err = mapstructure.Decode(req.Data, &reqData) err = mapstructure.Decode(req.Data, &reqData)
if err != nil { if err != nil {
connErr(conn, err, "Error decoding request data") connErr(conn, req.Type, err, "Error decoding request data")
break break
} }
// Reset DFU to prepare for next update // Reset DFU to prepare for next update
@@ -368,40 +369,40 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
case types.UpgradeTypeArchive: case types.UpgradeTypeArchive:
// If less than one file, return error // If less than one file, return error
if len(reqData.Files) < 1 { 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 break
} }
// If file is not zip archive, return error // If file is not zip archive, return error
if filepath.Ext(reqData.Files[0]) != ".zip" { 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 break
} }
// Load DFU archive // Load DFU archive
err := dev.DFU.LoadArchive(reqData.Files[0]) err := dev.DFU.LoadArchive(reqData.Files[0])
if err != nil { if err != nil {
connErr(conn, err, "Error loading archive file") connErr(conn, req.Type, err, "Error loading archive file")
break break
} }
case types.UpgradeTypeFiles: case types.UpgradeTypeFiles:
// If less than two files, return error // If less than two files, return error
if len(reqData.Files) < 2 { 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 break
} }
// If first file is not init packet, return error // If first file is not init packet, return error
if filepath.Ext(reqData.Files[0]) != ".dat" { 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 break
} }
// If second file is not firmware image, return error // If second file is not firmware image, return error
if filepath.Ext(reqData.Files[1]) != ".bin" { 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 break
} }
// Load individual DFU files // Load individual DFU files
err := dev.DFU.LoadFiles(reqData.Files[0], reqData.Files[1]) err := dev.DFU.LoadFiles(reqData.Files[0], reqData.Files[1])
if err != nil { if err != nil {
connErr(conn, err, "Error loading firmware files") connErr(conn, req.Type, err, "Error loading firmware files")
break break
} }
} }
@@ -413,7 +414,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
for event := range progress { for event := range progress {
// Encode event on connection // Encode event on connection
json.NewEncoder(conn).Encode(types.Response{ json.NewEncoder(conn).Encode(types.Response{
Type: types.ResTypeDFUProgress, Type: req.Type,
Value: event, Value: event,
}) })
} }
@@ -425,28 +426,30 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
// Start DFU // Start DFU
err = dev.DFU.Start() err = dev.DFU.Start()
if err != nil { if err != nil {
connErr(conn, err, "Error performing upgrade") connErr(conn, req.Type, err, "Error performing upgrade")
firmwareUpdating = false firmwareUpdating = false
break break
} }
firmwareUpdating = false firmwareUpdating = false
case types.ReqTypeCancel: case types.ReqTypeCancel:
if req.Data == nil { 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 continue
} }
reqID, ok := req.Data.(string) reqID, ok := req.Data.(string)
if !ok { 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 // Stop notifications
done.Done(reqID) 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 var res types.Response
// If error exists, add to types.Response, otherwise don't // If error exists, add to types.Response, otherwise don't
if err != nil { 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)} res = types.Response{Message: fmt.Sprintf("%s: %s", msg, err)}
} else { } else {
log.Error().Msg(msg) log.Error().Msg(msg)
res = types.Response{Message: msg} res = types.Response{Message: msg, Type: resType}
} }
res.Error = true res.Error = true