Compare commits

...

6 Commits

23 changed files with 318 additions and 442 deletions

View File

@ -18,11 +18,11 @@ const DefaultAddr = "/tmp/itd/socket"
type Client struct { type Client struct {
conn net.Conn conn net.Conn
respCh chan types.Response respCh chan types.Response
heartRateCh chan uint8 heartRateCh chan types.Response
battLevelCh chan uint8 battLevelCh chan types.Response
stepCountCh chan uint32 stepCountCh chan types.Response
motionCh chan infinitime.MotionValues motionCh chan types.Response
dfuProgressCh chan DFUProgress dfuProgressCh chan types.Response
} }
// New creates a new client and sets it up // New creates a new client and sets it up
@ -91,27 +91,43 @@ func (c *Client) requestNoRes(req types.Request) error {
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.ResTypeWatchHeartRate:
c.heartRateCh <- uint8(res.Value.(float64)) c.heartRateCh <- res
case types.ResTypeWatchBattLevel: case types.ResTypeWatchBattLevel:
c.battLevelCh <- uint8(res.Value.(float64)) c.battLevelCh <- res
case types.ResTypeWatchStepCount: case types.ResTypeWatchStepCount:
c.stepCountCh <- uint32(res.Value.(float64)) c.stepCountCh <- res
case types.ResTypeWatchMotion: case types.ResTypeWatchMotion:
out := infinitime.MotionValues{} c.motionCh <- res
err := mapstructure.Decode(res.Value, &out)
if err != nil {
return err
}
c.motionCh <- out
case types.ResTypeDFUProgress: case types.ResTypeDFUProgress:
out := DFUProgress{} c.dfuProgressCh <- res
err := mapstructure.Decode(res.Value, &out)
if err != nil {
return err
}
c.dfuProgressCh <- out
default: default:
c.respCh <- res c.respCh <- res
} }
return nil return nil
} }
func decodeUint8(val interface{}) uint8 {
return uint8(val.(float64))
}
func decodeUint32(val interface{}) uint32 {
return uint32(val.(float64))
}
func decodeMotion(val interface{}) (infinitime.MotionValues, error) {
out := infinitime.MotionValues{}
err := mapstructure.Decode(val, &out)
if err != nil {
return out, err
}
return out, nil
}
func decodeDFUProgress(val interface{}) (DFUProgress, error) {
out := DFUProgress{}
err := mapstructure.Decode(val, &out)
if err != nil {
return out, err
}
return out, nil
}

View File

@ -1,8 +1,6 @@
package api package api
import ( import (
"reflect"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"go.arsenm.dev/infinitime" "go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/types" "go.arsenm.dev/itd/internal/types"
@ -48,15 +46,27 @@ func (c *Client) BatteryLevel() (uint8, error) {
// new battery level values as they update. Do not use after // new battery level values as they update. Do not use after
// calling cancellation function // calling cancellation function
func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) { func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) {
c.battLevelCh = make(chan uint8, 2) c.battLevelCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{ err := c.requestNoRes(types.Request{
Type: types.ReqTypeBattLevel, Type: types.ReqTypeBattLevel,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
cancel := c.cancelFn(types.ReqTypeCancelBattLevel, c.battLevelCh) res := <-c.battLevelCh
return c.battLevelCh, cancel, nil done, cancel := c.cancelFn(res.ID, c.battLevelCh)
out := make(chan uint8, 2)
go func() {
for res := range c.battLevelCh {
select {
case <-done:
return
default:
out <- decodeUint8(res.Value)
}
}
}()
return out, cancel, nil
} }
// HeartRate gets the heart rate from the connected device // HeartRate gets the heart rate from the connected device
@ -68,33 +78,46 @@ func (c *Client) HeartRate() (uint8, error) {
return 0, err return 0, err
} }
return uint8(res.Value.(float64)), nil return decodeUint8(res.Value), nil
} }
// WatchHeartRate returns a channel which will contain // WatchHeartRate returns a channel which will contain
// new heart rate values as they update. Do not use after // new heart rate values as they update. Do not use after
// calling cancellation function // calling cancellation function
func (c *Client) WatchHeartRate() (<-chan uint8, func(), error) { func (c *Client) WatchHeartRate() (<-chan uint8, func(), error) {
c.heartRateCh = make(chan uint8, 2) c.heartRateCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{ err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchHeartRate, Type: types.ReqTypeWatchHeartRate,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
cancel := c.cancelFn(types.ReqTypeCancelHeartRate, c.heartRateCh) res := <-c.heartRateCh
return c.heartRateCh, cancel, nil done, cancel := c.cancelFn(res.ID, c.heartRateCh)
out := make(chan uint8, 2)
go func() {
for res := range c.heartRateCh {
select {
case <-done:
return
default:
out <- decodeUint8(res.Value)
}
}
}()
return out, cancel, nil
} }
// cancelFn generates a cancellation function for the given // cancelFn generates a cancellation function for the given
// request type and channel // request type and channel
func (c *Client) cancelFn(reqType int, ch interface{}) func() { func (c *Client) cancelFn(reqID string, ch chan types.Response) (chan struct{}, func()) {
return func() { done := make(chan struct{}, 1)
reflectCh := reflect.ValueOf(ch) return done, func() {
reflectCh.Close() done <- struct{}{}
reflectCh.Set(reflect.Zero(reflectCh.Type())) close(ch)
c.requestNoRes(types.Request{ c.requestNoRes(types.Request{
Type: reqType, Type: types.ReqTypeCancel,
Data: reqID,
}) })
} }
} }
@ -115,15 +138,27 @@ func (c *Client) StepCount() (uint32, error) {
// new step count values as they update. Do not use after // new step count values as they update. Do not use after
// calling cancellation function // calling cancellation function
func (c *Client) WatchStepCount() (<-chan uint32, func(), error) { func (c *Client) WatchStepCount() (<-chan uint32, func(), error) {
c.stepCountCh = make(chan uint32, 2) c.stepCountCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{ err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchStepCount, Type: types.ReqTypeWatchStepCount,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
cancel := c.cancelFn(types.ReqTypeCancelStepCount, c.stepCountCh) res := <-c.stepCountCh
return c.stepCountCh, cancel, nil done, cancel := c.cancelFn(res.ID, c.stepCountCh)
out := make(chan uint32, 2)
go func() {
for res := range c.stepCountCh {
select {
case <-done:
return
default:
out <- decodeUint32(res.Value)
}
}
}()
return out, cancel, nil
} }
// Motion gets the motion values from the connected device // Motion gets the motion values from the connected device
@ -146,13 +181,29 @@ func (c *Client) Motion() (infinitime.MotionValues, error) {
// new motion values as they update. Do not use after // new motion values as they update. Do not use after
// calling cancellation function // calling cancellation function
func (c *Client) WatchMotion() (<-chan infinitime.MotionValues, func(), error) { func (c *Client) WatchMotion() (<-chan infinitime.MotionValues, func(), error) {
c.motionCh = make(chan infinitime.MotionValues, 2) c.motionCh = make(chan types.Response, 5)
err := c.requestNoRes(types.Request{ err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchMotion, Type: types.ReqTypeWatchMotion,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
cancel := c.cancelFn(types.ReqTypeCancelMotion, c.motionCh) res := <-c.motionCh
return c.motionCh, cancel, nil done, cancel := c.cancelFn(res.ID, c.motionCh)
out := make(chan infinitime.MotionValues, 5)
go func() {
for res := range c.motionCh {
select {
case <-done:
return
default:
motion, err := decodeMotion(res.Value)
if err != nil {
continue
}
out <- motion
}
}
}()
return out, cancel, nil
} }

14
api/notify.go Normal file
View File

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

View File

@ -31,7 +31,18 @@ func (c *Client) FirmwareUpgrade(upgType UpgradeType, files ...string) (<-chan D
return nil, err return nil, err
} }
c.dfuProgressCh = make(chan DFUProgress, 5) c.dfuProgressCh = make(chan types.Response, 5)
return c.dfuProgressCh, nil out := make(chan DFUProgress, 5)
go func() {
for res := range c.dfuProgressCh {
progress, err := decodeDFUProgress(res.Value)
if err != nil {
continue
}
out <- progress
}
}()
return out, nil
} }

View File

@ -1,77 +0,0 @@
/*
* 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 cmd
import (
"bufio"
"encoding/json"
"fmt"
"net"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/internal/types"
)
// heartCmd represents the heart command
var heartCmd = &cobra.Command{
Use: "heart",
Short: "Get heart rate from InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
// Connect to itd UNIX socket
conn, err := net.Dial("unix", viper.GetString("sockPath"))
if err != nil {
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
}
defer conn.Close()
// Encode request into connection
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeHeartRate,
})
if err != nil {
log.Fatal().Err(err).Msg("Error making request")
}
// Read one line from connection
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
log.Fatal().Err(err).Msg("Error reading line from connection")
}
var res types.Response
// Decode line into response
err = json.Unmarshal(line, &res)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding JSON data")
}
if res.Error {
log.Fatal().Msg(res.Message)
}
// Print returned BPM
fmt.Printf("%d BPM\n", int(res.Value.(float64)))
},
}
func init() {
getCmd.AddCommand(heartCmd)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,9 +16,34 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package cmd package get
type DFUProgress struct { import (
Received int64 `mapstructure:"recvd"` "fmt"
Total int64 `mapstructure:"total"`
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// heartCmd represents the heart command
var heartCmd = &cobra.Command{
Use: "heart",
Short: "Get heart rate from InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
client := viper.Get("client").(*api.Client)
heartRate, err := client.HeartRate()
if err != nil {
log.Fatal().Err(err).Msg("Error getting heart rate")
}
// Print returned BPM
fmt.Printf("%d BPM\n", heartRate)
},
}
func init() {
getCmd.AddCommand(heartCmd)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 // indirect github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect
github.com/godbus/dbus/v5 v5.0.5 github.com/godbus/dbus/v5 v5.0.5
github.com/google/uuid v1.1.2
github.com/mattn/go-colorable v0.1.11 // indirect github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.4.2 github.com/mitchellh/mapstructure v1.4.2

1
go.sum
View File

@ -193,6 +193,7 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=

View File

@ -9,15 +9,12 @@ const (
ReqTypeNotify ReqTypeNotify
ReqTypeSetTime ReqTypeSetTime
ReqTypeWatchHeartRate ReqTypeWatchHeartRate
ReqTypeCancelHeartRate
ReqTypeWatchBattLevel ReqTypeWatchBattLevel
ReqTypeCancelBattLevel
ReqTypeMotion ReqTypeMotion
ReqTypeWatchMotion ReqTypeWatchMotion
ReqTypeCancelMotion
ReqTypeStepCount ReqTypeStepCount
ReqTypeWatchStepCount ReqTypeWatchStepCount
ReqTypeCancelStepCount ReqTypeCancel
) )
const ( const (
@ -29,15 +26,12 @@ const (
ResTypeNotify ResTypeNotify
ResTypeSetTime ResTypeSetTime
ResTypeWatchHeartRate ResTypeWatchHeartRate
ResTypeCancelHeartRate
ResTypeWatchBattLevel ResTypeWatchBattLevel
ResTypeCancelBattLevel
ResTypeMotion ResTypeMotion
ResTypeWatchMotion ResTypeWatchMotion
ResTypeCancelMotion
ResTypeStepCount ResTypeStepCount
ResTypeWatchStepCount ResTypeWatchStepCount
ResTypeCancelStepCount ResTypeCancel
) )
const ( const (
@ -54,6 +48,7 @@ type Response struct {
Type int `json:"type"` Type int `json:"type"`
Value interface{} `json:"value,omitempty"` Value interface{} `json:"value,omitempty"`
Message string `json:"msg,omitempty"` Message string `json:"msg,omitempty"`
ID string `json:"id,omitempty"`
Error bool `json:"error"` Error bool `json:"error"`
} }
@ -70,6 +65,7 @@ type ReqDataNotify struct {
type DFUProgress struct { type DFUProgress struct {
Received int64 `mapstructure:"recvd"` Received int64 `mapstructure:"recvd"`
Total int64 `mapstructure:"total"` Total int64 `mapstructure:"total"`
Sent int64 `mapstructure:"sent"`
} }
type MotionValues struct { type MotionValues struct {

View File

@ -27,6 +27,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -35,6 +36,29 @@ import (
"go.arsenm.dev/itd/translit" "go.arsenm.dev/itd/translit"
) )
type DoneMap map[string]chan struct{}
func (dm DoneMap) Exists(key string) bool {
_, ok := dm[key]
return ok
}
func (dm DoneMap) Done(key string) {
ch := dm[key]
ch <- struct{}{}
}
func (dm DoneMap) Create(key string) {
dm[key] = make(chan struct{}, 1)
}
func (dm DoneMap) Remove(key string) {
close(dm[key])
delete(dm, key)
}
var done = DoneMap{}
func startSocket(dev *infinitime.Device) error { func startSocket(dev *infinitime.Device) error {
// Make socket directory if non-existant // Make socket directory if non-existant
err := os.MkdirAll(filepath.Dir(viper.GetString("socket.path")), 0755) err := os.MkdirAll(filepath.Dir(viper.GetString("socket.path")), 0755)
@ -81,11 +105,6 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
return return
} }
heartRateDone := make(chan struct{})
battLevelDone := make(chan struct{})
stepCountDone := make(chan struct{})
motionDone := make(chan struct{})
// Create new scanner on connection // Create new scanner on connection
scanner := bufio.NewScanner(conn) scanner := bufio.NewScanner(conn)
for scanner.Scan() { for scanner.Scan() {
@ -116,27 +135,27 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
connErr(conn, err, "Error getting heart rate channel") connErr(conn, err, "Error getting heart rate channel")
break break
} }
reqID := uuid.New().String()
go func() { go func() {
done.Create(reqID)
// For every heart rate value // For every heart rate value
for heartRate := range heartRateCh { for heartRate := range heartRateCh {
select { select {
case <-heartRateDone: case <-done[reqID]:
// Stop notifications if done signal received // Stop notifications if done signal received
cancel() cancel()
done.Remove(reqID)
return return
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: types.ResTypeWatchHeartRate,
ID: reqID,
Value: heartRate, Value: heartRate,
}) })
} }
} }
}() }()
case types.ReqTypeCancelHeartRate:
// Stop heart rate notifications
heartRateDone <- struct{}{}
json.NewEncoder(conn).Encode(types.Response{})
case types.ReqTypeBattLevel: case types.ReqTypeBattLevel:
// Get battery level from watch // Get battery level from watch
battLevel, err := dev.BatteryLevel() battLevel, err := dev.BatteryLevel()
@ -155,27 +174,27 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
connErr(conn, err, "Error getting battery level channel") connErr(conn, err, "Error getting battery level channel")
break break
} }
reqID := uuid.New().String()
go func() { go func() {
done.Create(reqID)
// For every battery level value // For every battery level value
for battLevel := range battLevelCh { for battLevel := range battLevelCh {
select { select {
case <-battLevelDone: case <-done[reqID]:
// Stop notifications if done signal received // Stop notifications if done signal received
cancel() cancel()
done.Remove(reqID)
return return
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: types.ResTypeWatchBattLevel,
ID: reqID,
Value: battLevel, Value: battLevel,
}) })
} }
} }
}() }()
case types.ReqTypeCancelBattLevel:
// Stop battery level notifications
battLevelDone <- struct{}{}
json.NewEncoder(conn).Encode(types.Response{})
case types.ReqTypeMotion: case types.ReqTypeMotion:
// Get battery level from watch // Get battery level from watch
motionVals, err := dev.Motion() motionVals, err := dev.Motion()
@ -194,27 +213,28 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
connErr(conn, err, "Error getting heart rate channel") connErr(conn, err, "Error getting heart rate channel")
break break
} }
reqID := uuid.New().String()
go func() { go func() {
done.Create(reqID)
// For every motion event // For every motion event
for motionVals := range motionValCh { for motionVals := range motionValCh {
select { select {
case <-motionDone: case <-done[reqID]:
// Stop notifications if done signal received // Stop notifications if done signal received
cancel() cancel()
done.Remove(reqID)
return return
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: types.ResTypeWatchMotion,
ID: reqID,
Value: motionVals, Value: motionVals,
}) })
} }
} }
}() }()
case types.ReqTypeCancelMotion:
// Stop motion notifications
motionDone <- struct{}{}
json.NewEncoder(conn).Encode(types.Response{})
case types.ReqTypeStepCount: case types.ReqTypeStepCount:
// Get battery level from watch // Get battery level from watch
stepCount, err := dev.StepCount() stepCount, err := dev.StepCount()
@ -233,27 +253,27 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
connErr(conn, err, "Error getting heart rate channel") connErr(conn, err, "Error getting heart rate channel")
break break
} }
reqID := uuid.New().String()
go func() { go func() {
done.Create(reqID)
// For every step count value // For every step count value
for stepCount := range stepCountCh { for stepCount := range stepCountCh {
select { select {
case <-stepCountDone: case <-done[reqID]:
// Stop notifications if done signal received // Stop notifications if done signal received
cancel() cancel()
done.Remove(reqID)
return return
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: types.ResTypeWatchStepCount,
ID: reqID,
Value: stepCount, Value: stepCount,
}) })
} }
} }
}() }()
case types.ReqTypeCancelStepCount:
// Stop step count notifications
stepCountDone <- struct{}{}
json.NewEncoder(conn).Encode(types.Response{})
case types.ReqTypeFwVersion: case types.ReqTypeFwVersion:
// Get firmware version from watch // Get firmware version from watch
version, err := dev.Version() version, err := dev.Version()
@ -397,6 +417,7 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
Value: event, Value: event,
}) })
} }
firmwareUpdating = false
}() }()
// Set firmwareUpdating // Set firmwareUpdating
@ -409,6 +430,18 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
break break
} }
firmwareUpdating = false firmwareUpdating = false
case types.ReqTypeCancel:
if req.Data == nil {
connErr(conn, nil, "No data provided. Cancel request requires request ID string as data.")
continue
}
reqID, ok := req.Data.(string)
if !ok {
connErr(conn, nil, "Invalid data. Cancel request required request ID string as data.")
}
// Stop notifications
done.Done(reqID)
json.NewEncoder(conn).Encode(types.Response{Type: types.ResTypeCancel})
} }
} }
} }