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

  1. package lifx
  2. import (
  3. "context"
  4. "errors"
  5. "git.aiterp.net/lucifer/new-server/models"
  6. "log"
  7. "math/rand"
  8. "net"
  9. "sync"
  10. "time"
  11. )
  12. type Client struct {
  13. mu sync.Mutex
  14. seq uint8
  15. buf []byte
  16. lastN int
  17. conn *net.UDPConn
  18. source uint32
  19. debug bool
  20. addrMap map[string]*net.UDPAddr
  21. }
  22. // Send sends the payload to the hardware addr specified. If it cannot find a mapped
  23. // IP address from a past message, it will be broadcast and may be slow to use. If the
  24. // address is left blank, the package is marked as tagged and broadcast.
  25. //
  26. // ErrInvalidAddress can be returned by this, and indicates a badly configured device.
  27. // It should probably be logged.
  28. func (c *Client) Send(addr string, payload Payload) (seq uint8, err error) {
  29. c.mu.Lock()
  30. seq = c.seq
  31. c.seq += 1
  32. c.mu.Unlock()
  33. packet := createPacket()
  34. packet.SetSource(c.source)
  35. packet.SetSequence(seq)
  36. sendAddr := &net.UDPAddr{IP: net.IPv4(255, 255, 255, 255), Port: 56700}
  37. if addr == "" {
  38. packet.SetTagged(true)
  39. if c.debug {
  40. log.Println("Broadcasting", payload, "seq", seq)
  41. }
  42. } else {
  43. err = packet.SetTarget(addr)
  44. if err != nil {
  45. return
  46. }
  47. c.mu.Lock()
  48. if udpAddr, ok := c.addrMap[addr]; ok {
  49. sendAddr = udpAddr
  50. }
  51. c.mu.Unlock()
  52. if c.debug {
  53. log.Println("Sending", payload, "to", addr, "seq", seq)
  54. }
  55. }
  56. packet.SetPayload(payload)
  57. _, err = c.conn.WriteToUDP(packet, sendAddr)
  58. if err != nil {
  59. return
  60. }
  61. return
  62. }
  63. // LastPacket gets the last read packet. This data is valid until the next call to Recv. The Packet may
  64. // not be valid, however!
  65. func (c *Client) LastPacket() Packet {
  66. if c.lastN < 36 {
  67. return nil
  68. }
  69. return c.buf[:c.lastN]
  70. }
  71. // Recv reads a message from the UDP socket. The data returned is decoded and will always be valid.
  72. //
  73. // However, these should be handled specifically:
  74. // - ErrPayloadTooShort, ErrInvalidPacketSize: Garbage was received, please ignore.
  75. // - ErrUnrecognizedPacketType: Log these and see what's up
  76. // - ErrReadTimeout: The connection gets a 50ms read deadline. It should be used to do other things than wait
  77. func (c *Client) Recv(timeout time.Duration) (target string, seq uint8, payload Payload, err error) {
  78. if c.buf == nil {
  79. c.buf = make([]byte, 2048)
  80. }
  81. if timeout > 0 {
  82. err = c.conn.SetReadDeadline(time.Now().Add(timeout))
  83. } else {
  84. err = c.conn.SetReadDeadline(time.Time{})
  85. }
  86. if err != nil {
  87. return
  88. }
  89. n, addr, err := c.conn.ReadFromUDP(c.buf)
  90. if n > 0 {
  91. c.lastN = n
  92. }
  93. if err != nil {
  94. if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
  95. err = models.ErrReadTimeout
  96. return
  97. }
  98. return
  99. }
  100. packet := Packet(c.buf[:n])
  101. if n < 2 || packet.Size() != n && packet.Protocol() != 1024 {
  102. err = models.ErrInvalidAddress
  103. return
  104. }
  105. seq = packet.Sequence()
  106. target = packet.Target().String()
  107. payload, err = packet.Payload()
  108. if err != nil {
  109. return
  110. }
  111. // Learn the IP address from state service or ack messages.
  112. if service, ok := payload.(*StateService); ok && service.Service == 1 {
  113. c.mu.Lock()
  114. if c.addrMap == nil {
  115. c.addrMap = make(map[string]*net.UDPAddr)
  116. }
  117. c.addrMap[packet.Target().String()] = addr
  118. c.mu.Unlock()
  119. }
  120. if c.debug {
  121. log.Println("Received", payload, "from", target, "seq", seq)
  122. }
  123. return
  124. }
  125. // createClient creates a client that will last as long as the context.
  126. func createClient(ctx context.Context, bindAddr string, debug bool) (*Client, error) {
  127. addr := net.ParseIP(bindAddr)
  128. if addr == nil {
  129. return nil, errors.New("invalid addr")
  130. }
  131. source := uint32(rand.Uint64())
  132. if source < 2 {
  133. source = 2
  134. }
  135. conn, err := net.ListenUDP("udp", &net.UDPAddr{
  136. IP: addr,
  137. Port: 0,
  138. Zone: "",
  139. })
  140. if err != nil {
  141. return nil, err
  142. }
  143. go func() {
  144. <-ctx.Done()
  145. _ = conn.Close()
  146. }()
  147. return &Client{conn: conn, source: source, debug: debug}, nil
  148. }