Compare commits
22 Commits
3a877c41a4
...
a9ef386883
Author | SHA1 | Date | |
---|---|---|---|
a9ef386883 | |||
24cfda82d7 | |||
655af5c446 | |||
b363a20a9d | |||
f5d326124d | |||
cb8fb2c0bc | |||
38119435f1 | |||
034a69c12f | |||
70006a3d7b | |||
8aada58d64 | |||
5d231207cd | |||
584d9426e6 | |||
7a772a5458 | |||
079c733b60 | |||
e24a8e9088 | |||
0b5d777077 | |||
75327286ef | |||
099b0cd849 | |||
c9c00e0072 | |||
b2ffb2062a | |||
2e8c825fff | |||
7b870950d1 |
@ -177,7 +177,7 @@ To cross compile, simply set the go environment variables. For example, for Pine
|
|||||||
make GOOS=linux GOARCH=arm64
|
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`, `bluez`, and `playerctl` 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`, and `bluez` specifically).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
96
api/fs.go
Normal file
96
api/fs.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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
|
||||||
|
}
|
210
calls.go
210
calls.go
@ -1,85 +1,90 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"sync"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"go.arsenm.dev/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initCallNotifs(dev *infinitime.Device) error {
|
func initCallNotifs(dev *infinitime.Device) error {
|
||||||
// Define rule to filter dbus messages
|
// Connect to system bus. This connection is for method calls.
|
||||||
rule := "type='signal',sender='org.freedesktop.ModemManager1',interface='org.freedesktop.ModemManager1.Modem.Voice',member='CallAdded'"
|
conn, err := newSystemBusConn()
|
||||||
|
|
||||||
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new scanner for command output
|
// Check if modem manager interface exists
|
||||||
scanner := bufio.NewScanner(stdout)
|
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
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// For each line in output
|
// For every message received
|
||||||
for scanner.Scan() {
|
for event := range callCh {
|
||||||
// Get line as string
|
// Get path to call object
|
||||||
text := scanner.Text()
|
callPath := event.Body[0].(dbus.ObjectPath)
|
||||||
|
// Get call object
|
||||||
|
callObj = conn.Object("org.freedesktop.ModemManager1", callPath)
|
||||||
|
|
||||||
// If line starts with "#", it is part of
|
// Get phone number from call object using method call connection
|
||||||
// the field format, skip it.
|
phoneNum, err := getPhoneNum(conn, callObj)
|
||||||
if strings.HasPrefix(text, "#") {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error getting phone number")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split line into fields. The order is as follows:
|
// Send call notification to InfiniTime
|
||||||
// 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)
|
resCh, err := dev.NotifyCall(phoneNum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
|
go respHandlerOnce.Do(func() {
|
||||||
// Wait for PineTime response
|
// Wait for PineTime response
|
||||||
res := <-resCh
|
for res := range resCh {
|
||||||
switch res {
|
switch res {
|
||||||
case infinitime.CallStatusAccepted:
|
case infinitime.CallStatusAccepted:
|
||||||
// Attempt to accept call
|
// Attempt to accept call
|
||||||
err = acceptCall(callID)
|
err = acceptCall(conn, callObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Error accepting call")
|
log.Warn().Err(err).Msg("Error accepting call")
|
||||||
}
|
}
|
||||||
case infinitime.CallStatusDeclined:
|
case infinitime.CallStatusDeclined:
|
||||||
// Attempt to decline call
|
// Attempt to decline call
|
||||||
err = declineCall(callID)
|
err = declineCall(conn, callObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Error declining call")
|
log.Warn().Err(err).Msg("Error declining call")
|
||||||
}
|
}
|
||||||
@ -87,90 +92,51 @@ func initCallNotifs(dev *infinitime.Device) error {
|
|||||||
// Warn about unimplemented muting
|
// Warn about unimplemented muting
|
||||||
log.Warn().Msg("Muting calls is not implemented")
|
log.Warn().Msg("Muting calls is not implemented")
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
log.Info().Msg("Relaying calls to InfiniTime")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseModemID(modemPath string) int {
|
func modemManagerExists(conn *dbus.Conn) (bool, error) {
|
||||||
// Split path by "/"
|
var names []string
|
||||||
splitPath := strings.Split(modemPath, "/")
|
err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
|
||||||
// Get last element and convert to integer
|
if err != nil {
|
||||||
id, _ := strconv.Atoi(splitPath[len(splitPath)-1])
|
return false, err
|
||||||
return id
|
}
|
||||||
|
return strSlcContains(names, "org.freedesktop.ModemManager1"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentCallID(modemID int) (int, error) {
|
// getPhoneNum gets a phone number from a call object using a DBus connection
|
||||||
// Create mmcli command
|
func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
|
||||||
cmd := exec.Command("mmcli", "--voice-list-calls", "-m", fmt.Sprint(modemID), "-J")
|
var out string
|
||||||
// Run command and get output
|
// Get number property on DBus object and store return value in out
|
||||||
data, err := cmd.Output()
|
err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Number", &out)
|
||||||
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Split output into fields
|
return out, nil
|
||||||
num := strings.Fields(string(numData))
|
|
||||||
// Return last field
|
|
||||||
return num[len(num)-1], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptCall(callID int) error {
|
// getPhoneNum accepts a call using a DBus connection
|
||||||
// Create dbus-send command
|
func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||||
cmd := exec.Command("dbus-send",
|
// Call Accept() method on DBus object
|
||||||
"--dest=org.freedesktop.ModemManager1",
|
call := callObj.Call("org.freedesktop.ModemManager1.Call.Accept", 0)
|
||||||
"--print-reply",
|
if call.Err != nil {
|
||||||
"--system",
|
return call.Err
|
||||||
"--type=method_call",
|
}
|
||||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
return nil
|
||||||
"org.freedesktop.ModemManager1.Call.Accept",
|
|
||||||
)
|
|
||||||
// Run command and return errpr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func declineCall(callID int) error {
|
// getPhoneNum declines a call using a DBus connection
|
||||||
// Create dbus-send command
|
func declineCall(conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||||
cmd := exec.Command("dbus-send",
|
// Call Hangup() method on DBus object
|
||||||
"--dest=org.freedesktop.ModemManager1",
|
call := callObj.Call("org.freedesktop.ModemManager1.Call.Hangup", 0)
|
||||||
"--print-reply",
|
if call.Err != nil {
|
||||||
"--system",
|
return call.Err
|
||||||
"--type=method_call",
|
}
|
||||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
return nil
|
||||||
"org.freedesktop.ModemManager1.Call.Hangup",
|
|
||||||
)
|
|
||||||
// Run command and return errpr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
35
cmd/itctl/filesystem/filesystem.go
Normal file
35
cmd/itctl/filesystem/filesystem.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
56
cmd/itctl/filesystem/list.go
Normal file
56
cmd/itctl/filesystem/list.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
49
cmd/itctl/filesystem/mkdir.go
Normal file
49
cmd/itctl/filesystem/mkdir.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
50
cmd/itctl/filesystem/move.go
Normal file
50
cmd/itctl/filesystem/move.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
73
cmd/itctl/filesystem/read.go
Normal file
73
cmd/itctl/filesystem/read.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
50
cmd/itctl/filesystem/remove.go
Normal file
50
cmd/itctl/filesystem/remove.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
72
cmd/itctl/filesystem/write.go
Normal file
72
cmd/itctl/filesystem/write.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
@ -25,6 +25,7 @@ import (
|
|||||||
"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"
|
_ "go.arsenm.dev/itd/cmd/itctl/watch"
|
||||||
|
_ "go.arsenm.dev/itd/cmd/itctl/filesystem"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
37
dbus.go
Normal file
37
dbus.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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
2
go.mod
@ -25,7 +25,7 @@ require (
|
|||||||
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe // indirect
|
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe // indirect
|
||||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
|
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
|
||||||
github.com/yuin/goldmark v1.4.1 // indirect
|
github.com/yuin/goldmark v1.4.1 // indirect
|
||||||
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652
|
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7
|
||||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||||
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 // indirect
|
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -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.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 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652 h1:2Z09crdXIs+aMy1xwuD6w2kml+EObfmzgCBaOxoZIv0=
|
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7 h1:m6BVtAiWMRbfUgBZVthXq2eZhSwDxdtOi8Dh/59hXik=
|
||||||
go.arsenm.dev/infinitime v0.0.0-20211122091416-ec43bad46652/go.mod h1:kNBKxQfqeLUfi13GM6tB1kSvLm8HlZ7PM47AYeJQIiw=
|
go.arsenm.dev/infinitime v0.0.0-20211126043306-522c10a9c0a7/go.mod h1:TzAhsz7TAqEm/vWhgMvmIxHS5jt46hqKkPvr6cqvVyA=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
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/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ReqTypeHeartRate = iota
|
ReqTypeHeartRate = iota
|
||||||
ReqTypeBattLevel
|
ReqTypeBattLevel
|
||||||
@ -15,6 +20,7 @@ const (
|
|||||||
ReqTypeStepCount
|
ReqTypeStepCount
|
||||||
ReqTypeWatchStepCount
|
ReqTypeWatchStepCount
|
||||||
ReqTypeCancel
|
ReqTypeCancel
|
||||||
|
ReqTypeFS
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -22,6 +28,21 @@ const (
|
|||||||
UpgradeTypeFiles
|
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 ReqDataFwUpgrade struct {
|
||||||
Type int
|
Type int
|
||||||
Files []string
|
Files []string
|
||||||
@ -56,3 +77,66 @@ type MotionValues struct {
|
|||||||
Y int16
|
Y int16
|
||||||
Z 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
|
||||||
|
}
|
||||||
|
8
main.go
8
main.go
@ -26,7 +26,11 @@ import (
|
|||||||
"go.arsenm.dev/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var firmwareUpdating = false
|
var (
|
||||||
|
firmwareUpdating = false
|
||||||
|
// The FS must be updated when the watch is reconnected
|
||||||
|
updateFS = false
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
infinitime.Init()
|
infinitime.Init()
|
||||||
@ -61,6 +65,8 @@ func main() {
|
|||||||
log.Error().Err(err).Msg("Error sending notification to InfiniTime")
|
log.Error().Err(err).Msg("Error sending notification to InfiniTime")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFS = true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get firmware version
|
// Get firmware version
|
||||||
|
47
music.go
47
music.go
@ -26,45 +26,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func initMusicCtrl(dev *infinitime.Device) error {
|
func initMusicCtrl(dev *infinitime.Device) error {
|
||||||
// On player status change, set status
|
player.Init()
|
||||||
err := player.Status(func(newStatus bool) {
|
|
||||||
if !firmwareUpdating {
|
|
||||||
dev.Music.SetStatus(newStatus)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// On player title change, set track
|
player.OnChange(func(ct player.ChangeType, val string) {
|
||||||
err = player.Metadata("title", func(newTitle string) {
|
|
||||||
if !firmwareUpdating {
|
if !firmwareUpdating {
|
||||||
dev.Music.SetTrack(newTitle)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
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
|
// Watch for music events
|
||||||
musicEvtCh, err := dev.Music.WatchEvents()
|
musicEvtCh, err := dev.Music.WatchEvents()
|
||||||
|
@ -30,7 +30,7 @@ import (
|
|||||||
|
|
||||||
func initNotifRelay(dev *infinitime.Device) error {
|
func initNotifRelay(dev *infinitime.Device) error {
|
||||||
// Connect to dbus session bus
|
// Connect to dbus session bus
|
||||||
bus, err := dbus.SessionBus()
|
bus, err := newSessionBusConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
143
socket.go
143
socket.go
@ -22,6 +22,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -32,6 +33,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.arsenm.dev/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
|
"go.arsenm.dev/infinitime/blefs"
|
||||||
"go.arsenm.dev/itd/internal/types"
|
"go.arsenm.dev/itd/internal/types"
|
||||||
"go.arsenm.dev/itd/translit"
|
"go.arsenm.dev/itd/translit"
|
||||||
)
|
)
|
||||||
@ -78,6 +80,11 @@ func startSocket(dev *infinitime.Device) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fs, err := dev.FS()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Error getting BLE filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
// Accept socket connection
|
// Accept socket connection
|
||||||
@ -87,7 +94,7 @@ func startSocket(dev *infinitime.Device) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Concurrently handle connection
|
// Concurrently handle connection
|
||||||
go handleConnection(conn, dev)
|
go handleConnection(conn, dev, fs)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -97,9 +104,23 @@ func startSocket(dev *infinitime.Device) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConnection(conn net.Conn, dev *infinitime.Device) {
|
func handleConnection(conn net.Conn, dev *infinitime.Device, fs *blefs.FS) {
|
||||||
defer conn.Close()
|
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
|
// Create new scanner on connection
|
||||||
scanner := bufio.NewScanner(conn)
|
scanner := bufio.NewScanner(conn)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@ -431,6 +452,124 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
firmwareUpdating = false
|
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:
|
case types.ReqTypeCancel:
|
||||||
if req.Data == nil {
|
if req.Data == nil {
|
||||||
connErr(conn, req.Type, 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.")
|
||||||
|
Loading…
Reference in New Issue
Block a user