Compare commits

..

No commits in common. "a9ef386883e09b0793d130ceaadf507b0fac3a54" and "3a877c41a4b890f594512fd145ebc8850d5e1524" have entirely different histories.

19 changed files with 172 additions and 863 deletions

View File

@ -177,7 +177,7 @@ To cross compile, simply set the go environment variables. For example, for Pine
make GOOS=linux GOARCH=arm64
```
This will compile `itd` and `itctl` for Linux aarch64 which is what runs on the PinePhone. This daemon only runs on Linux due to the library's dependencies (`dbus`, and `bluez` specifically).
This will compile `itd` and `itctl` for Linux aarch64 which is what runs on the PinePhone. This daemon only runs on Linux due to the library's dependencies (`dbus`, `bluez`, and `playerctl` specifically).
---

View File

@ -1,96 +0,0 @@
package api
import (
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/itd/internal/types"
)
func (c *Client) Rename(old, new string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeMove,
Files: []string{old, new},
},
})
if err != nil {
return err
}
return nil
}
func (c *Client) Remove(paths ...string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeDelete,
Files: paths,
},
})
if err != nil {
return err
}
return nil
}
func (c *Client) Mkdir(paths ...string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeMkdir,
Files: paths,
},
})
if err != nil {
return err
}
return nil
}
func (c *Client) ReadDir(path string) ([]types.FileInfo, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeList,
Files: []string{path},
},
})
if err != nil {
return nil, err
}
var out []types.FileInfo
err = mapstructure.Decode(res.Value, &out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *Client) ReadFile(path string) (string, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeRead,
Files: []string{path},
},
})
if err != nil {
return "", err
}
return res.Value.(string), nil
}
func (c *Client) WriteFile(path, data string) error {
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeWrite,
Files: []string{path},
Data: data,
},
})
if err != nil {
return err
}
return nil
}

222
calls.go
View File

@ -1,90 +1,85 @@
package main
import (
"sync"
"bufio"
"encoding/json"
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/godbus/dbus/v5"
"github.com/rs/zerolog/log"
"go.arsenm.dev/infinitime"
)
func initCallNotifs(dev *infinitime.Device) error {
// Connect to system bus. This connection is for method calls.
conn, err := newSystemBusConn()
// Define rule to filter dbus messages
rule := "type='signal',sender='org.freedesktop.ModemManager1',interface='org.freedesktop.ModemManager1.Modem.Voice',member='CallAdded'"
// Use dbus-monitor command with profiling output as a workaround
// because go-bluetooth seems to monopolize the system bus connection
// which makes monitoring show only bluez-related messages.
cmd := exec.Command("dbus-monitor", "--system", "--profile", rule)
// Get command output pipe
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
// Run command asynchronously
err = cmd.Start()
if err != nil {
return err
}
// Check if modem manager interface exists
exists, err := modemManagerExists(conn)
if err != nil {
return err
}
// If it does not exist, stop function
if !exists {
conn.Close()
return nil
}
// Connect to system bus. This connection is for monitoring.
monitorConn, err := newSystemBusConn()
if err != nil {
return err
}
// Add match for new calls to monitor connection
err = monitorConn.AddMatchSignal(
dbus.WithMatchSender("org.freedesktop.ModemManager1"),
dbus.WithMatchInterface("org.freedesktop.ModemManager1.Modem.Voice"),
dbus.WithMatchMember("CallAdded"),
)
if err != nil {
return err
}
// Create channel to receive calls
callCh := make(chan *dbus.Message, 5)
// Notify channel upon received message
monitorConn.Eavesdrop(callCh)
var respHandlerOnce sync.Once
var callObj dbus.BusObject
// Create new scanner for command output
scanner := bufio.NewScanner(stdout)
go func() {
// For every message received
for event := range callCh {
// Get path to call object
callPath := event.Body[0].(dbus.ObjectPath)
// Get call object
callObj = conn.Object("org.freedesktop.ModemManager1", callPath)
// For each line in output
for scanner.Scan() {
// Get line as string
text := scanner.Text()
// Get phone number from call object using method call connection
phoneNum, err := getPhoneNum(conn, callObj)
if err != nil {
log.Error().Err(err).Msg("Error getting phone number")
// If line starts with "#", it is part of
// the field format, skip it.
if strings.HasPrefix(text, "#") {
continue
}
// Send call notification to InfiniTime
resCh, err := dev.NotifyCall(phoneNum)
if err != nil {
continue
}
go respHandlerOnce.Do(func() {
// Wait for PineTime response
for res := range resCh {
// Split line into fields. The order is as follows:
// type timestamp serial sender destination path interface member
fields := strings.Fields(text)
// Field 7 is Member. Make sure it is "CallAdded".
if fields[7] == "CallAdded" {
// Get Modem ID from modem path
modemID := parseModemID(fields[5])
// Get call ID of current call
callID, err := getCurrentCallID(modemID)
if err != nil {
continue
}
// Get phone number of current call
phoneNum, err := getPhoneNum(callID)
if err != nil {
continue
}
// Send call notification to PineTime
resCh, err := dev.NotifyCall(phoneNum)
if err != nil {
continue
}
go func() {
// Wait for PineTime response
res := <-resCh
switch res {
case infinitime.CallStatusAccepted:
// Attempt to accept call
err = acceptCall(conn, callObj)
err = acceptCall(callID)
if err != nil {
log.Warn().Err(err).Msg("Error accepting call")
}
case infinitime.CallStatusDeclined:
// Attempt to decline call
err = declineCall(conn, callObj)
err = declineCall(callID)
if err != nil {
log.Warn().Err(err).Msg("Error declining call")
}
@ -92,51 +87,90 @@ func initCallNotifs(dev *infinitime.Device) error {
// Warn about unimplemented muting
log.Warn().Msg("Muting calls is not implemented")
}
}
})
}()
}
}
}()
log.Info().Msg("Relaying calls to InfiniTime")
return nil
}
func modemManagerExists(conn *dbus.Conn) (bool, error) {
var names []string
err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
if err != nil {
return false, err
}
return strSlcContains(names, "org.freedesktop.ModemManager1"), nil
func parseModemID(modemPath string) int {
// Split path by "/"
splitPath := strings.Split(modemPath, "/")
// Get last element and convert to integer
id, _ := strconv.Atoi(splitPath[len(splitPath)-1])
return id
}
// getPhoneNum gets a phone number from a call object using a DBus connection
func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
var out string
// Get number property on DBus object and store return value in out
err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Number", &out)
func getCurrentCallID(modemID int) (int, error) {
// Create mmcli command
cmd := exec.Command("mmcli", "--voice-list-calls", "-m", fmt.Sprint(modemID), "-J")
// Run command and get output
data, err := cmd.Output()
if err != nil {
return 0, err
}
var calls map[string][]string
// Decode JSON from command output
err = json.Unmarshal(data, &calls)
if err != nil {
return 0, err
}
// Get first call in output
firstCall := calls["modem.voice.call"][0]
// Split path by "/"
splitCall := strings.Split(firstCall, "/")
// Return last element converted to integer
return strconv.Atoi(splitCall[len(splitCall)-1])
}
func getPhoneNum(callID int) (string, error) {
// Create dbus-send command
cmd := exec.Command("dbus-send",
"--dest=org.freedesktop.ModemManager1",
"--system",
"--print-reply=literal",
"--type=method_call",
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
"org.freedesktop.DBus.Properties.Get",
"string:org.freedesktop.ModemManager1.Call",
"string:Number",
)
// Run command and get output
numData, err := cmd.Output()
if err != nil {
return "", err
}
return out, nil
// Split output into fields
num := strings.Fields(string(numData))
// Return last field
return num[len(num)-1], nil
}
// getPhoneNum accepts a call using a DBus connection
func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error {
// Call Accept() method on DBus object
call := callObj.Call("org.freedesktop.ModemManager1.Call.Accept", 0)
if call.Err != nil {
return call.Err
}
return nil
func acceptCall(callID int) error {
// Create dbus-send command
cmd := exec.Command("dbus-send",
"--dest=org.freedesktop.ModemManager1",
"--print-reply",
"--system",
"--type=method_call",
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
"org.freedesktop.ModemManager1.Call.Accept",
)
// Run command and return errpr
return cmd.Run()
}
// getPhoneNum declines a call using a DBus connection
func declineCall(conn *dbus.Conn, callObj dbus.BusObject) error {
// Call Hangup() method on DBus object
call := callObj.Call("org.freedesktop.ModemManager1.Call.Hangup", 0)
if call.Err != nil {
return call.Err
}
return nil
func declineCall(callID int) error {
// Create dbus-send command
cmd := exec.Command("dbus-send",
"--dest=org.freedesktop.ModemManager1",
"--print-reply",
"--system",
"--type=method_call",
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
"org.freedesktop.ModemManager1.Call.Hangup",
)
// Run command and return errpr
return cmd.Run()
}

View File

@ -1,35 +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 filesystem
import (
"github.com/spf13/cobra"
"go.arsenm.dev/itd/cmd/itctl/root"
)
// filesystemCmd represents the filesystem command
var filesystemCmd = &cobra.Command{
Use: "filesystem",
Aliases: []string{"fs"},
Short: "Perform filesystem operations on the PineTime",
}
func init() {
root.RootCmd.AddCommand(filesystemCmd)
}

View File

@ -1,56 +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 filesystem
import (
"fmt"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// listCmd represents the list command
var listCmd = &cobra.Command{
Use: "list [path]",
Aliases: []string{"ls"},
Short: "List a directory",
Run: func(cmd *cobra.Command, args []string) {
dirPath := "/"
if len(args) > 0 {
dirPath = args[0]
}
client := viper.Get("client").(*api.Client)
listing, err := client.ReadDir(dirPath)
if err != nil {
log.Fatal().Err(err).Msg("Error getting directory listing")
}
for _, entry := range listing {
fmt.Println(entry)
}
},
}
func init() {
filesystemCmd.AddCommand(listCmd)
}

View File

@ -1,49 +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 filesystem
import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// mkdirCmd represents the mkdir command
var mkdirCmd = &cobra.Command{
Use: "mkdir <path...>",
Short: "Create a new directory",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
cmd.Usage()
log.Fatal().Msg("Command mkdir requires one or more arguments")
}
client := viper.Get("client").(*api.Client)
err := client.Mkdir(args...)
if err != nil {
log.Fatal().Err(err).Msg("Error creating directory")
}
},
}
func init() {
filesystemCmd.AddCommand(mkdirCmd)
}

View File

@ -1,50 +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 filesystem
import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// moveCmd represents the move command
var moveCmd = &cobra.Command{
Use: "move <old> <new>",
Aliases: []string{"mv"},
Short: "Move a file or directory",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
cmd.Usage()
log.Fatal().Msg("Command move requires two arguments")
}
client := viper.Get("client").(*api.Client)
err := client.Rename(args[0], args[1])
if err != nil {
log.Fatal().Err(err).Msg("Error moving file or directory")
}
},
}
func init() {
filesystemCmd.AddCommand(moveCmd)
}

View File

@ -1,73 +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 filesystem
import (
"os"
"time"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// readCmd represents the read command
var readCmd = &cobra.Command{
Use: `read <remote path> <local path | "-">`,
Short: "Read a file from InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
cmd.Usage()
log.Fatal().Msg("Command read requires two arguments")
}
start := time.Now()
client := viper.Get("client").(*api.Client)
data, err := client.ReadFile(args[0])
if err != nil {
log.Fatal().Err(err).Msg("Error moving file or directory")
}
var suffix string
var out *os.File
if args[1] == "-" {
out = os.Stdout
suffix = "\n"
} else {
out, err = os.Create(args[1])
if err != nil {
log.Fatal().Err(err).Msg("Error opening local file")
}
}
n, err := out.WriteString(data)
if err != nil {
log.Fatal().Err(err).Msg("Error writing to local file")
}
out.WriteString(suffix)
log.Info().Msgf("Read %d bytes in %s", n, time.Since(start))
},
}
func init() {
filesystemCmd.AddCommand(readCmd)
}

View File

@ -1,50 +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 filesystem
import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// removeCmd represents the remove command
var removeCmd = &cobra.Command{
Use: "remove <path...>",
Aliases: []string{"rm"},
Short: "Create a new directory",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
cmd.Usage()
log.Fatal().Msg("Command mkdir requires one or more arguments")
}
client := viper.Get("client").(*api.Client)
err := client.Remove(args...)
if err != nil {
log.Fatal().Err(err).Msg("Error removing file or directory")
}
},
}
func init() {
filesystemCmd.AddCommand(removeCmd)
}

View File

@ -1,72 +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 filesystem
import (
"io"
"os"
"time"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.arsenm.dev/itd/api"
)
// writeCmd represents the write command
var writeCmd = &cobra.Command{
Use: `write <local path | "-"> <remote path>`,
Short: "Write a file to InfiniTime",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
cmd.Usage()
log.Fatal().Msg("Command write requires two arguments")
}
start := time.Now()
client := viper.Get("client").(*api.Client)
var in *os.File
if args[0] == "-" {
in = os.Stdin
} else {
fl, err := os.Open(args[0])
if err != nil {
log.Fatal().Err(err).Msg("Error opening local file")
}
in = fl
}
data, err := io.ReadAll(in)
if err != nil {
log.Fatal().Err(err).Msg("Error moving file or directory")
}
err = client.WriteFile(args[1], string(data))
if err != nil {
log.Fatal().Err(err).Msg("Error writing to remote file")
}
log.Info().Msgf("Wrote %d bytes in %s", len(data), time.Since(start))
},
}
func init() {
filesystemCmd.AddCommand(writeCmd)
}

View File

@ -25,7 +25,6 @@ import (
"go.arsenm.dev/itd/cmd/itctl/root"
_ "go.arsenm.dev/itd/cmd/itctl/set"
_ "go.arsenm.dev/itd/cmd/itctl/watch"
_ "go.arsenm.dev/itd/cmd/itctl/filesystem"
"os"

37
dbus.go
View File

@ -1,37 +0,0 @@
package main
import "github.com/godbus/dbus/v5"
func newSystemBusConn() (*dbus.Conn, error) {
// Connect to dbus session bus
conn, err := dbus.SystemBusPrivate()
if err != nil {
return nil, err
}
err = conn.Auth(nil)
if err != nil {
return nil, err
}
err = conn.Hello()
if err != nil {
return nil, err
}
return conn, nil
}
func newSessionBusConn() (*dbus.Conn, error) {
// Connect to dbus session bus
conn, err := dbus.SessionBusPrivate()
if err != nil {
return nil, err
}
err = conn.Auth(nil)
if err != nil {
return nil, err
}
err = conn.Hello()
if err != nil {
return nil, err
}
return conn, nil
}

2
go.mod
View File

@ -25,7 +25,7 @@ require (
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe // indirect
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
github.com/yuin/goldmark v1.4.1 // indirect
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 // indirect
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect

4
go.sum
View File

@ -367,8 +367,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7 h1:m6BVtAiWMRbfUgBZVthXq2eZhSwDxdtOi8Dh/59hXik=
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7/go.mod h1:TzAhsz7TAqEm/vWhgMvmIxHS5jt46hqKkPvr6cqvVyA=
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652 h1:2Z09crdXIs+aMy1xwuD6w2kml+EObfmzgCBaOxoZIv0=
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652/go.mod h1:kNBKxQfqeLUfi13GM6tB1kSvLm8HlZ7PM47AYeJQIiw=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=

View File

@ -1,10 +1,5 @@
package types
import (
"fmt"
"strconv"
)
const (
ReqTypeHeartRate = iota
ReqTypeBattLevel
@ -20,7 +15,6 @@ const (
ReqTypeStepCount
ReqTypeWatchStepCount
ReqTypeCancel
ReqTypeFS
)
const (
@ -28,21 +22,6 @@ const (
UpgradeTypeFiles
)
const (
FSTypeWrite = iota
FSTypeRead
FSTypeMove
FSTypeDelete
FSTypeList
FSTypeMkdir
)
type ReqDataFS struct {
Type int `json:"type"`
Files []string `json:"files"`
Data string `json:"data,omitempty"`
}
type ReqDataFwUpgrade struct {
Type int
Files []string
@ -77,66 +56,3 @@ type MotionValues struct {
Y int16
Z int16
}
type FileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"isDir"`
}
func (fi FileInfo) String() string {
var isDirChar rune
if fi.IsDir {
isDirChar = 'd'
} else {
isDirChar = '-'
}
// Get human-readable value for file size
val, unit := bytesHuman(fi.Size)
prec := 0
// If value is less than 10, set precision to 1
if val < 10 {
prec = 1
}
// Convert float to string
valStr := strconv.FormatFloat(val, 'f', prec, 64)
// Return string formatted like so:
// - 10 kB file
// or:
// d 0 B .
return fmt.Sprintf(
"%c %3s %-2s %s",
isDirChar,
valStr,
unit,
fi.Name,
)
}
// bytesHuman returns a human-readable string for
// the amount of bytes inputted.
func bytesHuman(b int64) (float64, string) {
const unit = 1000
// Set possible units prefixes (PineTime flash is 4MB)
units := [2]rune{'k', 'M'}
// If amount of bytes is less than smallest unit
if b < unit {
// Return unchanged with unit "B"
return float64(b), "B"
}
div, exp := int64(unit), 0
// Get decimal values and unit prefix index
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
// Create string for full unit
unitStr := string([]rune{units[exp], 'B'})
// Return decimal with unit string
return float64(b) / float64(div), unitStr
}

View File

@ -26,11 +26,7 @@ import (
"go.arsenm.dev/infinitime"
)
var (
firmwareUpdating = false
// The FS must be updated when the watch is reconnected
updateFS = false
)
var firmwareUpdating = false
func main() {
infinitime.Init()
@ -65,8 +61,6 @@ func main() {
log.Error().Err(err).Msg("Error sending notification to InfiniTime")
}
}
updateFS = true
})
// Get firmware version

View File

@ -26,22 +26,45 @@ import (
)
func initMusicCtrl(dev *infinitime.Device) error {
player.Init()
player.OnChange(func(ct player.ChangeType, val string) {
// On player status change, set status
err := player.Status(func(newStatus bool) {
if !firmwareUpdating {
switch ct {
case player.ChangeTypeStatus:
dev.Music.SetStatus(val == "Playing")
case player.ChangeTypeTitle:
dev.Music.SetTrack(val)
case player.ChangeTypeAlbum:
dev.Music.SetAlbum(val)
case player.ChangeTypeArtist:
dev.Music.SetArtist(val)
}
dev.Music.SetStatus(newStatus)
}
})
if err != nil {
return err
}
// On player title change, set track
err = player.Metadata("title", func(newTitle string) {
if !firmwareUpdating {
dev.Music.SetTrack(newTitle)
}
})
if err != nil {
return err
}
// On player album change, set album
err = player.Metadata("album", func(newAlbum string) {
if !firmwareUpdating {
dev.Music.SetAlbum(newAlbum)
}
})
if err != nil {
return err
}
// On player artist change, set artist
err = player.Metadata("artist", func(newArtist string) {
if !firmwareUpdating {
dev.Music.SetArtist(newArtist)
}
})
if err != nil {
return err
}
// Watch for music events
musicEvtCh, err := dev.Music.WatchEvents()

View File

@ -30,7 +30,7 @@ import (
func initNotifRelay(dev *infinitime.Device) error {
// Connect to dbus session bus
bus, err := newSessionBusConn()
bus, err := dbus.SessionBus()
if err != nil {
return err
}

143
socket.go
View File

@ -22,7 +22,6 @@ import (
"bufio"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
@ -33,7 +32,6 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/infinitime/blefs"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/translit"
)
@ -80,11 +78,6 @@ func startSocket(dev *infinitime.Device) error {
return err
}
fs, err := dev.FS()
if err != nil {
log.Warn().Err(err).Msg("Error getting BLE filesystem")
}
go func() {
for {
// Accept socket connection
@ -94,7 +87,7 @@ func startSocket(dev *infinitime.Device) error {
}
// Concurrently handle connection
go handleConnection(conn, dev, fs)
go handleConnection(conn, dev)
}
}()
@ -104,23 +97,9 @@ func startSocket(dev *infinitime.Device) error {
return nil
}
func handleConnection(conn net.Conn, dev *infinitime.Device, fs *blefs.FS) {
func handleConnection(conn net.Conn, dev *infinitime.Device) {
defer conn.Close()
// If an FS update is required (reconnect ocurred)
if updateFS {
// Get new FS
newFS, err := dev.FS()
if err != nil {
fs = nil
log.Warn().Err(err).Msg("Error updating BLE filesystem")
}
// Set FS pointer to new FS
*fs = *newFS
// Reset updateFS
updateFS = false
}
// Create new scanner on connection
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
@ -452,124 +431,6 @@ func handleConnection(conn net.Conn, dev *infinitime.Device, fs *blefs.FS) {
break
}
firmwareUpdating = false
case types.ReqTypeFS:
if fs == nil {
connErr(conn, req.Type, nil, "BLE filesystem is not available")
break
}
// If no data, return error
if req.Data == nil {
connErr(conn, req.Type, nil, "Data required for filesystem operations")
break
}
var reqData types.ReqDataFS
// Decode data map to firmware upgrade request data
err = mapstructure.Decode(req.Data, &reqData)
if err != nil {
connErr(conn, req.Type, err, "Error decoding request data")
break
}
switch reqData.Type {
case types.FSTypeDelete:
if len(reqData.Files) == 0 {
connErr(conn, req.Type, nil, "Remove FS command requires at least one file")
break
}
for _, file := range reqData.Files {
err := fs.Remove(file)
if err != nil {
connErr(conn, req.Type, err, "Error removing file")
break
}
}
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.FSTypeMove:
if len(reqData.Files) != 2 {
connErr(conn, req.Type, nil, "Move FS command requires an old path and new path in the files list")
break
}
err := fs.Rename(reqData.Files[0], reqData.Files[1])
if err != nil {
connErr(conn, req.Type, err, "Error moving file")
break
}
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.FSTypeMkdir:
if len(reqData.Files) == 0 {
connErr(conn, req.Type, nil, "Mkdir FS command requires at least one file")
break
}
for _, file := range reqData.Files {
err := fs.Mkdir(file)
if err != nil {
connErr(conn, req.Type, err, "Error creating directory")
break
}
}
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.FSTypeList:
if len(reqData.Files) != 1 {
connErr(conn, req.Type, nil, "List FS command requires a path to list in the files list")
break
}
entries, err := fs.ReadDir(reqData.Files[0])
if err != nil {
connErr(conn, req.Type, err, "Error reading directory")
break
}
var out []types.FileInfo
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
connErr(conn, req.Type, err, "Error getting file info")
break
}
out = append(out, types.FileInfo{
Name: info.Name(),
Size: info.Size(),
IsDir: info.IsDir(),
})
}
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: out,
})
case types.FSTypeWrite:
if len(reqData.Files) != 1 {
connErr(conn, req.Type, nil, "Write FS command requires a path to the file to write")
break
}
file, err := fs.Create(reqData.Files[0], uint32(len(reqData.Data)))
if err != nil {
connErr(conn, req.Type, err, "Error creating file")
break
}
_, err = file.WriteString(reqData.Data)
if err != nil {
connErr(conn, req.Type, err, "Error writing to file")
break
}
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.FSTypeRead:
if len(reqData.Files) != 1 {
connErr(conn, req.Type, nil, "Read FS command requires a path to the file to read")
break
}
file, err := fs.Open(reqData.Files[0])
if err != nil {
connErr(conn, req.Type, err, "Error opening file")
break
}
data, err := io.ReadAll(file)
if err != nil {
connErr(conn, req.Type, err, "Error reading from file")
break
}
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: string(data),
})
}
case types.ReqTypeCancel:
if req.Data == nil {
connErr(conn, req.Type, nil, "No data provided. Cancel request requires request ID string as data.")