forked from Elara6331/infinitime
Compare commits
3 Commits
5fb5cf4b92
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2da80044b3 | |||
| d20e8af00e | |||
| 0c369dc5df |
@@ -33,6 +33,7 @@ const (
|
|||||||
MotionValChar = "00030002-78fc-48fe-8e23-433b3a1942d0"
|
MotionValChar = "00030002-78fc-48fe-8e23-433b3a1942d0"
|
||||||
FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb"
|
FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb"
|
||||||
CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
|
CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
|
||||||
|
LocalTimeChar = "00002a0f-0000-1000-8000-00805f9b34fb"
|
||||||
BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb"
|
BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb"
|
||||||
HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb"
|
HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb"
|
||||||
FSTransferChar = "adaf0200-4669-6c65-5472-616e73666572"
|
FSTransferChar = "adaf0200-4669-6c65-5472-616e73666572"
|
||||||
@@ -47,6 +48,7 @@ var charNames = map[string]string{
|
|||||||
MotionValChar: "Motion Values",
|
MotionValChar: "Motion Values",
|
||||||
FirmwareVerChar: "Firmware Version",
|
FirmwareVerChar: "Firmware Version",
|
||||||
CurrentTimeChar: "Current Time",
|
CurrentTimeChar: "Current Time",
|
||||||
|
LocalTimeChar: "Local Time",
|
||||||
BatteryLvlChar: "Battery Level",
|
BatteryLvlChar: "Battery Level",
|
||||||
HeartRateChar: "Heart Rate",
|
HeartRateChar: "Heart Rate",
|
||||||
FSTransferChar: "Filesystem Transfer",
|
FSTransferChar: "Filesystem Transfer",
|
||||||
@@ -66,6 +68,7 @@ type Device struct {
|
|||||||
motionValChar *gatt.GattCharacteristic1
|
motionValChar *gatt.GattCharacteristic1
|
||||||
fwVersionChar *gatt.GattCharacteristic1
|
fwVersionChar *gatt.GattCharacteristic1
|
||||||
currentTimeChar *gatt.GattCharacteristic1
|
currentTimeChar *gatt.GattCharacteristic1
|
||||||
|
localTimeChar *gatt.GattCharacteristic1
|
||||||
battLevelChar *gatt.GattCharacteristic1
|
battLevelChar *gatt.GattCharacteristic1
|
||||||
heartRateChar *gatt.GattCharacteristic1
|
heartRateChar *gatt.GattCharacteristic1
|
||||||
fsVersionChar *gatt.GattCharacteristic1
|
fsVersionChar *gatt.GattCharacteristic1
|
||||||
@@ -421,6 +424,8 @@ func (i *Device) resolveChars() error {
|
|||||||
i.fwVersionChar = char
|
i.fwVersionChar = char
|
||||||
case CurrentTimeChar:
|
case CurrentTimeChar:
|
||||||
i.currentTimeChar = char
|
i.currentTimeChar = char
|
||||||
|
case LocalTimeChar:
|
||||||
|
i.localTimeChar = char
|
||||||
case BatteryLvlChar:
|
case BatteryLvlChar:
|
||||||
i.battLevelChar = char
|
i.battLevelChar = char
|
||||||
case HeartRateChar:
|
case HeartRateChar:
|
||||||
@@ -708,7 +713,9 @@ func (i *Device) WatchMotion(ctx context.Context) (<-chan MotionValues, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTime sets the watch's time using the Current Time Service
|
// SetTime sets the watch's
|
||||||
|
// * time using the Current Time Service's current time characteristic
|
||||||
|
// * timezone information using the CTS's local time characteristic
|
||||||
func (i *Device) SetTime(t time.Time) error {
|
func (i *Device) SetTime(t time.Time) error {
|
||||||
if err := i.checkStatus(i.currentTimeChar, CurrentTimeChar); err != nil {
|
if err := i.checkStatus(i.currentTimeChar, CurrentTimeChar); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -723,7 +730,38 @@ func (i *Device) SetTime(t time.Time) error {
|
|||||||
binary.Write(buf, binary.LittleEndian, uint8(t.Weekday()))
|
binary.Write(buf, binary.LittleEndian, uint8(t.Weekday()))
|
||||||
binary.Write(buf, binary.LittleEndian, uint8((t.Nanosecond()/1000)/1e6*256))
|
binary.Write(buf, binary.LittleEndian, uint8((t.Nanosecond()/1000)/1e6*256))
|
||||||
binary.Write(buf, binary.LittleEndian, uint8(0b0001))
|
binary.Write(buf, binary.LittleEndian, uint8(0b0001))
|
||||||
return i.currentTimeChar.WriteValue(buf.Bytes(), nil)
|
if err := i.currentTimeChar.WriteValue(buf.Bytes(), nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.checkStatus(i.localTimeChar, LocalTimeChar); err != nil {
|
||||||
|
// If the characteristic is unavailable,
|
||||||
|
// fail silently, as many people may be on
|
||||||
|
// older InfiniTime versions. A warning
|
||||||
|
// may be added later.
|
||||||
|
if _, ok := err.(ErrCharNotAvail); ok {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, offset := t.Zone()
|
||||||
|
dst := 0
|
||||||
|
|
||||||
|
// Local time expects two values: the timezone offset and the dst offset, both
|
||||||
|
// expressed in quarters of an hour.
|
||||||
|
// Timezone offset is to be constant over DST, with dst offset holding the offset != 0
|
||||||
|
// when DST is in effect.
|
||||||
|
// As there is no standard way in go to get the actual dst offset, we assume it to be 1h
|
||||||
|
// when DST is in effect
|
||||||
|
if t.IsDST() {
|
||||||
|
dst = 3600
|
||||||
|
offset -= 3600
|
||||||
|
}
|
||||||
|
bufTz := &bytes.Buffer{}
|
||||||
|
binary.Write(bufTz, binary.LittleEndian, uint8(offset / 3600 * 4))
|
||||||
|
binary.Write(bufTz, binary.LittleEndian, uint8(dst / 3600 * 4))
|
||||||
|
return i.localTimeChar.WriteValue(bufTz.Bytes(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify sends a notification to InfiniTime via
|
// Notify sends a notification to InfiniTime via
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
package player
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
|
||||||
"go.arsenm.dev/infinitime/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
method, monitor *dbus.Conn
|
|
||||||
monitorCh chan *dbus.Message
|
|
||||||
onChangeOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init makes required connections to DBis and
|
|
||||||
// initializes change monitoring channel
|
|
||||||
func Init() error {
|
|
||||||
// Connect to session bus for monitoring
|
|
||||||
monitorConn, err := utils.NewSessionBusConn()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Add match rule for PropertiesChanged on media player
|
|
||||||
monitorConn.AddMatchSignal(
|
|
||||||
dbus.WithMatchObjectPath("/org/mpris/MediaPlayer2"),
|
|
||||||
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
|
|
||||||
dbus.WithMatchMember("PropertiesChanged"),
|
|
||||||
)
|
|
||||||
monitorCh = make(chan *dbus.Message, 10)
|
|
||||||
monitorConn.Eavesdrop(monitorCh)
|
|
||||||
|
|
||||||
// Connect to session bus for method calls
|
|
||||||
methodConn, err := utils.NewSessionBusConn()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
method, monitor = methodConn, monitorConn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit closes all connections and channels
|
|
||||||
func Exit() {
|
|
||||||
close(monitorCh)
|
|
||||||
method.Close()
|
|
||||||
monitor.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play uses MPRIS to play media
|
|
||||||
func Play() error {
|
|
||||||
player, err := getPlayerObj()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if player != nil {
|
|
||||||
call := player.Call("org.mpris.MediaPlayer2.Player.Play", 0)
|
|
||||||
if call.Err != nil {
|
|
||||||
return call.Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause uses MPRIS to pause media
|
|
||||||
func Pause() error {
|
|
||||||
player, err := getPlayerObj()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if player != nil {
|
|
||||||
call := player.Call("org.mpris.MediaPlayer2.Player.Pause", 0)
|
|
||||||
if call.Err != nil {
|
|
||||||
return call.Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next uses MPRIS to skip to next media
|
|
||||||
func Next() error {
|
|
||||||
player, err := getPlayerObj()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if player != nil {
|
|
||||||
call := player.Call("org.mpris.MediaPlayer2.Player.Next", 0)
|
|
||||||
if call.Err != nil {
|
|
||||||
return call.Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prev uses MPRIS to skip to previous media
|
|
||||||
func Prev() error {
|
|
||||||
player, err := getPlayerObj()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if player != nil {
|
|
||||||
call := player.Call("org.mpris.MediaPlayer2.Player.Previous", 0)
|
|
||||||
if call.Err != nil {
|
|
||||||
return call.Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func VolUp(percent uint) error {
|
|
||||||
|
|
||||||
player, err := getPlayerObj()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if player != nil {
|
|
||||||
currentVal, err := player.GetProperty("org.mpris.MediaPlayer2.Player.Volume")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newVal := currentVal.Value().(float64) + (float64(percent) / 100)
|
|
||||||
err = player.SetProperty("org.mpris.MediaPlayer2.Player.Volume", newVal)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func VolDown(percent uint) error {
|
|
||||||
|
|
||||||
player, err := getPlayerObj()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if player != nil {
|
|
||||||
currentVal, err := player.GetProperty("org.mpris.MediaPlayer2.Player.Volume")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newVal := currentVal.Value().(float64) - (float64(percent) / 100)
|
|
||||||
err = player.SetProperty("org.mpris.MediaPlayer2.Player.Volume", newVal)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangeType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChangeTypeTitle ChangeType = iota
|
|
||||||
ChangeTypeArtist
|
|
||||||
ChangeTypeAlbum
|
|
||||||
ChangeTypeStatus
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ct ChangeType) String() string {
|
|
||||||
switch ct {
|
|
||||||
case ChangeTypeTitle:
|
|
||||||
return "Title"
|
|
||||||
case ChangeTypeAlbum:
|
|
||||||
return "Album"
|
|
||||||
case ChangeTypeArtist:
|
|
||||||
return "Artist"
|
|
||||||
case ChangeTypeStatus:
|
|
||||||
return "Status"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnChange runs cb when a value changes
|
|
||||||
func OnChange(cb func(ChangeType, string)) {
|
|
||||||
go onChangeOnce.Do(func() {
|
|
||||||
// For every message on channel
|
|
||||||
for msg := range monitorCh {
|
|
||||||
// Parse PropertiesChanged
|
|
||||||
iface, changed, ok := parsePropertiesChanged(msg)
|
|
||||||
if !ok || iface != "org.mpris.MediaPlayer2.Player" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// For every property changed
|
|
||||||
for name, val := range changed {
|
|
||||||
// If metadata changed
|
|
||||||
if name == "Metadata" {
|
|
||||||
// Get fields
|
|
||||||
fields := val.Value().(map[string]dbus.Variant)
|
|
||||||
// For every field
|
|
||||||
for name, val := range fields {
|
|
||||||
// Handle each field appropriately
|
|
||||||
if strings.HasSuffix(name, "title") {
|
|
||||||
title := val.Value().(string)
|
|
||||||
if title == "" {
|
|
||||||
title = "Unknown " + ChangeTypeTitle.String()
|
|
||||||
}
|
|
||||||
cb(ChangeTypeTitle, title)
|
|
||||||
} else if strings.HasSuffix(name, "album") {
|
|
||||||
album := val.Value().(string)
|
|
||||||
if album == "" {
|
|
||||||
album = "Unknown " + ChangeTypeAlbum.String()
|
|
||||||
}
|
|
||||||
cb(ChangeTypeAlbum, album)
|
|
||||||
} else if strings.HasSuffix(name, "artist") {
|
|
||||||
var artists string
|
|
||||||
switch artistVal := val.Value().(type) {
|
|
||||||
case string:
|
|
||||||
artists = artistVal
|
|
||||||
case []string:
|
|
||||||
artists = strings.Join(artistVal, ", ")
|
|
||||||
}
|
|
||||||
if artists == "" {
|
|
||||||
artists = "Unknown " + ChangeTypeArtist.String()
|
|
||||||
}
|
|
||||||
cb(ChangeTypeArtist, artists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if name == "PlaybackStatus" {
|
|
||||||
// Handle status change
|
|
||||||
cb(ChangeTypeStatus, val.Value().(string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPlayerNames gets all DBus MPRIS player bus names
|
|
||||||
func getPlayerNames(conn *dbus.Conn) ([]string, error) {
|
|
||||||
var names []string
|
|
||||||
err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var players []string
|
|
||||||
for _, name := range names {
|
|
||||||
if strings.HasPrefix(name, "org.mpris.MediaPlayer2") {
|
|
||||||
players = append(players, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return players, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPlayerObj gets the object corresponding to the first
|
|
||||||
// bus name found in DBus
|
|
||||||
func getPlayerObj() (dbus.BusObject, error) {
|
|
||||||
players, err := getPlayerNames(method)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(players) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return method.Object(players[0], "/org/mpris/MediaPlayer2"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parsePropertiesChanged parses a DBus PropertiesChanged signal
|
|
||||||
func parsePropertiesChanged(msg *dbus.Message) (iface string, changed map[string]dbus.Variant, ok bool) {
|
|
||||||
if len(msg.Body) != 3 {
|
|
||||||
return "", nil, false
|
|
||||||
}
|
|
||||||
iface, ok = msg.Body[0].(string)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
changed, ok = msg.Body[1].(map[string]dbus.Variant)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user