Compare commits
8 Commits
2ea9f99db6
...
v0.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 552f19676b | |||
| 9d58ea0ae7 | |||
| 76875db7ea | |||
| be5bdc625b | |||
| 0d0db949af | |||
| 0d164aef3d | |||
| 28610d9ebb | |||
| dff34b484d |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
76
cmd/itctl/watch/battery.go
Normal file
76
cmd/itctl/watch/battery.go
Normal 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
75
cmd/itctl/watch/heart.go
Normal 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
86
cmd/itctl/watch/motion.go
Normal 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
75
cmd/itctl/watch/steps.go
Normal 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
34
cmd/itctl/watch/watch.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
105
cmd/itgui/motion.go
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
103
socket.go
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user