You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

178 lines
3.7 KiB

package lifx
import (
"context"
"errors"
"git.aiterp.net/lucifer/new-server/models"
"log"
"math/rand"
"net"
"sync"
"time"
)
type Client struct {
mu sync.Mutex
seq uint8
buf []byte
lastN int
conn *net.UDPConn
source uint32
debug bool
addrMap map[string]*net.UDPAddr
}
// Send sends the payload to the hardware addr specified. If it cannot find a mapped
// IP address from a past message, it will be broadcast and may be slow to use. If the
// address is left blank, the package is marked as tagged and broadcast.
//
// ErrInvalidAddress can be returned by this, and indicates a badly configured device.
// It should probably be logged.
func (c *Client) Send(addr string, payload Payload) (seq uint8, err error) {
c.mu.Lock()
seq = c.seq
c.seq += 1
c.mu.Unlock()
packet := createPacket()
packet.SetSource(c.source)
packet.SetSequence(seq)
sendAddr := &net.UDPAddr{IP: net.IPv4(255, 255, 255, 255), Port: 56700}
if addr == "" {
packet.SetTagged(true)
if c.debug {
log.Println("Broadcasting", payload, "seq", seq)
}
} else {
err = packet.SetTarget(addr)
if err != nil {
return
}
c.mu.Lock()
if udpAddr, ok := c.addrMap[addr]; ok {
sendAddr = udpAddr
}
c.mu.Unlock()
if c.debug {
log.Println("Sending", payload, "to", addr, "seq", seq)
}
}
packet.SetPayload(payload)
_, err = c.conn.WriteToUDP(packet, sendAddr)
if err != nil {
return
}
return
}
// LastPacket gets the last read packet. This data is valid until the next call to Recv. The Packet may
// not be valid, however!
func (c *Client) LastPacket() Packet {
if c.lastN < 36 {
return nil
}
return c.buf[:c.lastN]
}
// Recv reads a message from the UDP socket. The data returned is decoded and will always be valid.
//
// However, these should be handled specifically:
// - ErrPayloadTooShort, ErrInvalidPacketSize: Garbage was received, please ignore.
// - ErrUnrecognizedPacketType: Log these and see what's up
// - ErrReadTimeout: The connection gets a 50ms read deadline. It should be used to do other things than wait
func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload Payload, err error) {
if c.buf == nil {
c.buf = make([]byte, 2048)
}
if timeout > 0 {
err = c.conn.SetReadDeadline(time.Now().Add(timeout))
} else {
err = c.conn.SetReadDeadline(time.Time{})
}
if err != nil {
return
}
n, addr, err := c.conn.ReadFromUDP(c.buf)
if n > 0 {
c.lastN = n
}
if err != nil {
if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
err = models.ErrReadTimeout
return
}
return
}
packet := Packet(c.buf[:n])
if n < 2 || packet.Size() != n && packet.Protocol() != 1024 {
err = models.ErrInvalidAddress
return
}
seq = packet.Sequence()
target = packet.Target().String()
payload, err = packet.Payload()
if err != nil {
return
}
// Learn the IP address from state service or ack messages.
if service, ok := payload.(*StateService); ok && service.Service == 1 {
c.mu.Lock()
if c.addrMap == nil {
c.addrMap = make(map[string]*net.UDPAddr)
}
c.addrMap[packet.Target().String()] = addr
c.mu.Unlock()
}
if c.debug {
log.Println("Received", payload, "from", target, "seq", seq)
}
return
}
// createClient creates a client that will last as long as the context.
func createClient(ctx context.Context, bindAddr string, debug bool) (*Client, error) {
addr := net.ParseIP(bindAddr)
if addr == nil {
return nil, errors.New("invalid addr")
}
source := uint32(rand.Uint64())
if source < 2 {
source = 2
}
conn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: addr,
Port: 0,
Zone: "",
})
if err != nil {
return nil, err
}
go func() {
<-ctx.Done()
_ = conn.Close()
}()
return &Client{conn: conn, source: source, debug: debug}, nil
}