drivers/sesame/protocol.go
raw ยท 4881 bytes
// SPDX-License-Identifier: Apache-2.0
// Protocol adapted from CANDY HOUSE's MIT-licensed Sesame Android SDK.
package sesame
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
)
const (
ServiceUUID = "0000fd81-0000-1000-8000-00805f9b34fb"
WriteUUID = "16860002-a5ae-9856-b6d3-dbb4c676993e"
NotifyUUID = "16860003-a5ae-9856-b6d3-dbb4c676993e"
maxMessage = 1024
)
type Target struct {
UUID string `json:"uuid"`
Model string `json:"model"`
Adapter string `json:"adapter"`
Address string `json:"address"`
Registered bool `json:"registered"`
}
// Credential is private state. Never include it in public configuration/status.
type Credential struct {
Secret string `json:"secret"`
}
func (c Credential) key() ([]byte, error) {
b, e := hex.DecodeString(c.Secret)
if e != nil || len(b) != 16 {
return nil, errors.New("Sesame secret must contain exactly 32 hexadecimal characters")
}
return b, nil
}
func (c Credential) Validate() error { _, e := c.key(); return e }
func NormalizeUUID(s string) (string, error) {
s = strings.ToLower(strings.TrimSpace(s))
if len(s) != 36 || s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return "", errors.New("invalid Sesame UUID")
}
b, e := hex.DecodeString(strings.ReplaceAll(s, "-", ""))
if e != nil || len(b) != 16 {
return "", errors.New("invalid Sesame UUID")
}
return formatUUID(b), nil
}
func formatUUID(b []byte) string {
s := hex.EncodeToString(b)
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
}
func model(product byte) string {
switch product {
case 5:
return "sesame_5"
case 7:
return "sesame_5_pro"
case 20:
return "sesame_6"
case 21:
return "sesame_6_pro"
}
return ""
}
func Supported(s string) bool {
for _, p := range []byte{5, 7, 20, 21} {
if model(p) == s {
return true
}
}
return false
}
func Advertisement(b []byte) (Target, bool) {
if len(b) < 19 || model(b[0]) == "" {
return Target{}, false
}
return Target{UUID: formatUUID(b[3:19]), Model: model(b[0]), Registered: b[2]&1 != 0}, true
}
type State struct {
Online bool `json:"online"`
Error string `json:"error,omitempty"`
Position *int16 `json:"position,omitempty"`
Target *int16 `json:"target,omitempty"`
Locked bool `json:"locked"`
Stopped bool `json:"stopped"`
Critical bool `json:"critical"`
BatteryCritical bool `json:"battery_critical"`
LockPosition *int16 `json:"lock_position,omitempty"`
UnlockPosition *int16 `json:"unlock_position,omitempty"`
Boundary *int16 `json:"boundary,omitempty"`
Updated time.Time `json:"updated"`
}
func i16(b []byte) *int16 { v := int16(binary.LittleEndian.Uint16(b)); return &v }
func short(v int16) []byte {
b := make([]byte, 2)
binary.LittleEndian.PutUint16(b, uint16(v))
return b
}
func (s *State) publish(item byte, b []byte) error {
switch item {
case 81:
if len(b) < 7 {
return errors.New("short mechanical status")
}
s.Position = i16(b[4:6])
s.Target = i16(b[2:4])
if *s.Target == -32768 {
s.Target = nil
}
s.Locked = b[6]&2 != 0
s.Critical = b[6]&8 != 0
s.Stopped = b[6]&16 != 0
s.BatteryCritical = b[6]&32 != 0
case 80:
if len(b) < 6 {
return errors.New("short mechanical settings")
}
s.LockPosition = i16(b[:2])
s.UnlockPosition = i16(b[2:4])
case 20:
if len(b) != 2 {
return errors.New("invalid lock boundary")
}
s.Boundary = i16(b)
}
s.Updated = time.Now()
return nil
}
type assembler struct {
data []byte
active bool
}
func (a *assembler) feed(b []byte) (byte, []byte, error) {
if len(b) < 2 || len(b) > 20 || b[0] > 5 {
return 0, nil, errors.New("invalid Sesame fragment")
}
start, kind := b[0]&1 != 0, b[0]>>1
if start {
if a.active {
return 0, nil, errors.New("interrupted Sesame message")
}
a.data = nil
a.active = true
} else if !a.active {
return 0, nil, errors.New("orphan Sesame fragment")
}
if len(a.data)+len(b)-1 > maxMessage {
return 0, nil, errors.New("Sesame message too large")
}
a.data = append(a.data, b[1:]...)
if kind == 0 {
return 0, nil, nil
}
out := a.data
a.data = nil
a.active = false
return kind, out, nil
}
func fragments(kind byte, b []byte) ([][]byte, error) {
if (kind != 1 && kind != 2) || len(b) == 0 || len(b) > maxMessage {
return nil, errors.New("invalid Sesame message")
}
var out [][]byte
start := byte(1)
for len(b) > 0 {
n := len(b)
if n > 19 {
n = 19
}
h := start
if n == len(b) {
h |= kind << 1
}
f := append([]byte{h}, b[:n]...)
out = append(out, f)
b = b[n:]
start = 0
}
return out, nil
}
type ResultError struct{ Item, Code byte }
func (e *ResultError) Error() string {
return fmt.Sprintf("Sesame rejected command 0x%02x (result %d)", e.Item, e.Code)
}