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.

431 lines
9.3 KiB

  1. package nanoleaf
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "git.aiterp.net/lucifer/new-server/models"
  8. "github.com/lucasb-eyer/go-colorful"
  9. "io"
  10. "io/ioutil"
  11. "log"
  12. "math"
  13. "net"
  14. "net/http"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "time"
  19. )
  20. type bridge struct {
  21. mu sync.Mutex
  22. externalID int
  23. host string
  24. apiKey string
  25. panels []*panel
  26. panelIDMap map[uint16]int
  27. }
  28. func (b *bridge) Devices() []models.Device {
  29. results := make([]models.Device, 0, len(b.panels))
  30. for i, panel := range b.panels {
  31. red := float64(panel.ColorRGBA[0]) / 255.0
  32. green := float64(panel.ColorRGBA[1]) / 255.0
  33. blue := float64(panel.ColorRGBA[2]) / 255.0
  34. hue, sat, value := colorful.LinearRgb(red, green, blue).Hsv()
  35. results = append(results, models.Device{
  36. ID: -1,
  37. BridgeID: b.externalID,
  38. InternalID: strconv.Itoa(int(panel.ID)),
  39. Icon: "hexagon",
  40. Name: fmt.Sprintf("Hexagon %d", i),
  41. Capabilities: []models.DeviceCapability{
  42. models.DCPower,
  43. models.DCColorHS,
  44. models.DCIntensity,
  45. models.DCButtons,
  46. },
  47. ButtonNames: []string{"Touch"},
  48. DriverProperties: map[string]string{
  49. "x": strconv.Itoa(panel.X),
  50. "y": strconv.Itoa(panel.Y),
  51. "o": strconv.Itoa(panel.O),
  52. "shapeType": shapeTypeMap[panel.ShapeType],
  53. "shapeWidth": strconv.Itoa(shapeWidthMap[panel.ShapeType]),
  54. },
  55. UserProperties: nil,
  56. State: models.DeviceState{
  57. Power: panel.ColorRGBA[3] == 0,
  58. Color: models.ColorValue{
  59. Hue: math.Mod(hue, 360),
  60. Saturation: sat,
  61. },
  62. Intensity: value,
  63. Temperature: 0,
  64. },
  65. Tags: nil,
  66. })
  67. }
  68. return results
  69. }
  70. func (b *bridge) Refresh(ctx context.Context) error {
  71. overview, err := b.Overview(ctx)
  72. if err != nil {
  73. return err
  74. }
  75. b.mu.Lock()
  76. PanelLoop:
  77. for _, panelInfo := range overview.PanelLayout.Data.PositionData {
  78. if panelInfo.PanelID == 0 {
  79. continue
  80. }
  81. for _, existingPanel := range b.panels {
  82. if existingPanel.ID == panelInfo.PanelID {
  83. existingPanel.O = panelInfo.O
  84. existingPanel.X = panelInfo.X
  85. existingPanel.Y = panelInfo.Y
  86. existingPanel.ShapeType = panelInfo.ShapeType
  87. continue PanelLoop
  88. }
  89. }
  90. b.panels = append(b.panels, &panel{
  91. ID: panelInfo.PanelID,
  92. ColorRGBA: [4]byte{0, 0, 0, 0},
  93. TransitionAt: time.Time{},
  94. O: panelInfo.O,
  95. X: panelInfo.X,
  96. Y: panelInfo.Y,
  97. ShapeType: panelInfo.ShapeType,
  98. Stale: true,
  99. SlowUpdates: 5,
  100. })
  101. }
  102. b.mu.Unlock()
  103. return nil
  104. }
  105. func (b *bridge) Overview(ctx context.Context) (*Overview, error) {
  106. req, err := http.NewRequest("GET", b.URL(), nil)
  107. if err != nil {
  108. return nil, err
  109. }
  110. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  111. if err != nil {
  112. return nil, err
  113. }
  114. defer res.Body.Close()
  115. switch res.StatusCode {
  116. case 400, 403, 500, 503:
  117. return nil, models.ErrUnexpectedResponse
  118. case 401:
  119. return nil, models.ErrIncorrectToken
  120. }
  121. overview := Overview{}
  122. err = json.NewDecoder(res.Body).Decode(&overview)
  123. if err != nil {
  124. return nil, err
  125. }
  126. return &overview, nil
  127. }
  128. func (b *bridge) URL(resource ...string) string {
  129. return fmt.Sprintf("http://%s:16021/api/v1/%s/%s", b.host, b.apiKey, strings.Join(resource, "/"))
  130. }
  131. func (b *bridge) Update(devices []models.Device) {
  132. b.mu.Lock()
  133. defer b.mu.Unlock()
  134. for _, device := range devices {
  135. id, err := strconv.Atoi(device.InternalID)
  136. if err != nil {
  137. continue
  138. }
  139. b.panelIDMap[uint16(id)] = device.ID
  140. for _, panel := range b.panels {
  141. if panel.ID == uint16(id) {
  142. if device.State.Power {
  143. color := colorful.Hsv(device.State.Color.Hue, device.State.Color.Saturation, device.State.Intensity)
  144. red, green, blue := color.RGB255()
  145. newColor := [4]byte{red, green, blue, 255}
  146. if newColor != panel.ColorRGBA {
  147. panel.update(newColor, time.Now().Add(time.Millisecond*120))
  148. }
  149. } else {
  150. panel.update([4]byte{0, 0, 0, 0}, time.Now())
  151. }
  152. break
  153. }
  154. }
  155. }
  156. }
  157. func (b *bridge) Run(ctx context.Context, info models.Bridge, ch chan<- models.Event) error {
  158. err := b.updateEffect(ctx)
  159. if err != nil {
  160. return err
  161. }
  162. conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
  163. IP: net.ParseIP(info.Address),
  164. Port: 60222,
  165. })
  166. if err != nil {
  167. return err
  168. }
  169. defer conn.Close()
  170. // Notify connections and disconnections
  171. ch <- models.Event{Name: "BridgeConnected", Payload: map[string]string{"bridgeId": strconv.Itoa(info.ID)}}
  172. defer func() {
  173. ch <- models.Event{Name: "BridgeDisconnected", Payload: map[string]string{"bridgeId": strconv.Itoa(info.ID)}}
  174. }()
  175. // Start touch listener. This one should go down together with this one, though, so it needs a new context.
  176. ctx2, cancel := context.WithCancel(ctx)
  177. defer cancel()
  178. go b.runTouchListener(ctx2, info.Address, info.Token, ch)
  179. go func() {
  180. ticker := time.NewTicker(time.Second * 5)
  181. hadErrors := false
  182. for {
  183. select {
  184. case <-ticker.C:
  185. case <-ctx2.Done():
  186. }
  187. reqTimeout, cancel := context.WithTimeout(ctx2, time.Second*4)
  188. err := b.updateEffect(reqTimeout)
  189. cancel()
  190. if err != nil {
  191. log.Println("Failed to update effects:", err, "This error is non-fatal, and it will be retried shortly.")
  192. hadErrors = true
  193. } else if hadErrors {
  194. b.mu.Lock()
  195. for _, panel := range b.panels {
  196. panel.Stale = true
  197. panel.SlowUpdates = 3
  198. panel.TicksUntilSlowUpdate = 10
  199. }
  200. b.mu.Unlock()
  201. hadErrors = false
  202. }
  203. }
  204. }()
  205. ticker := time.NewTicker(time.Millisecond * 100)
  206. defer ticker.Stop()
  207. strikes := 0
  208. for range ticker.C {
  209. if ctx.Err() != nil {
  210. break
  211. }
  212. panelUpdate := make(PanelUpdate, 2)
  213. b.mu.Lock()
  214. for _, panel := range b.panels {
  215. if !panel.Stale {
  216. if panel.SlowUpdates > 0 {
  217. panel.TicksUntilSlowUpdate -= 1
  218. if panel.TicksUntilSlowUpdate > 0 {
  219. continue
  220. }
  221. panel.TicksUntilSlowUpdate = 10
  222. panel.SlowUpdates -= 1
  223. } else {
  224. continue
  225. }
  226. } else {
  227. panel.Stale = false
  228. }
  229. panelUpdate.Add(panel.message())
  230. if panelUpdate.Len() > 150 {
  231. break
  232. }
  233. }
  234. b.mu.Unlock()
  235. if panelUpdate.Len() == 0 {
  236. continue
  237. }
  238. _, err := conn.Write(panelUpdate)
  239. if err != nil {
  240. strikes++
  241. if strikes >= 3 {
  242. return err
  243. }
  244. } else {
  245. strikes = 0
  246. }
  247. }
  248. return nil
  249. }
  250. func (b *bridge) updateEffect(ctx context.Context) error {
  251. overview, err := b.Overview(ctx)
  252. if err != nil {
  253. return err
  254. }
  255. if overview.Effects.Select == "*Dynamic*" && overview.State.ColorMode == "effect" {
  256. return nil
  257. }
  258. req, err := http.NewRequest("PUT", b.URL("effects"), bytes.NewReader(httpMessage))
  259. if err != nil {
  260. return err
  261. }
  262. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  263. if err != nil {
  264. return err
  265. }
  266. defer res.Body.Close()
  267. if res.StatusCode != 204 {
  268. return models.ErrUnexpectedResponse
  269. }
  270. b.mu.Lock()
  271. for _, panel := range b.panels {
  272. panel.Stale = true
  273. }
  274. b.mu.Unlock()
  275. return nil
  276. }
  277. func (b *bridge) runTouchListener(ctx context.Context, host, apiKey string, ch chan<- models.Event) {
  278. cooldownID := 0
  279. cooldownUntil := time.Now()
  280. message := make(PanelEventMessage, 65536)
  281. reqCloser := io.Closer(nil)
  282. for {
  283. // Set up touch event receiver
  284. touchListener, err := net.ListenUDP("udp4", &net.UDPAddr{
  285. Port: 0,
  286. IP: net.IPv4(0, 0, 0, 0),
  287. })
  288. if err != nil {
  289. log.Println("Socket error:", err)
  290. goto teardownAndRetry
  291. }
  292. {
  293. // Create touch event sender on the remote end.
  294. req, err := http.NewRequest("GET", fmt.Sprintf("http://%s:16021/api/v1/%s/events?id=4", host, apiKey), nil)
  295. if err != nil {
  296. log.Println("HTTP error:", err)
  297. goto teardownAndRetry
  298. }
  299. req.Header["TouchEventsPort"] = []string{strconv.Itoa(touchListener.LocalAddr().(*net.UDPAddr).Port)}
  300. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  301. if err != nil {
  302. log.Println("HTTP error:", err)
  303. goto teardownAndRetry
  304. }
  305. // Discard all data coming over http.
  306. reqCloser = res.Body
  307. go io.Copy(ioutil.Discard, res.Body)
  308. for {
  309. if ctx.Err() != nil {
  310. goto teardownAndRetry
  311. }
  312. // Check in with the context every so often
  313. _ = touchListener.SetReadDeadline(time.Now().Add(time.Second))
  314. n, _, err := touchListener.ReadFromUDP(message)
  315. if err != nil {
  316. if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
  317. continue
  318. } else if ctx.Err() == nil {
  319. log.Println("UDP error:", err)
  320. }
  321. goto teardownAndRetry
  322. }
  323. if !message[:n].ValidateLength() {
  324. log.Println("Bad message length field")
  325. continue
  326. }
  327. for i := 0; i < message.Count(); i += 1 {
  328. b.mu.Lock()
  329. externalID, hasExternalID := b.panelIDMap[message.PanelID(i)]
  330. swipedFromID, hasSwipedFromID := b.panelIDMap[message.SwipedFromPanelID(i)]
  331. b.mu.Unlock()
  332. if !hasExternalID || (externalID == cooldownID && time.Now().Before(cooldownUntil)) {
  333. continue
  334. }
  335. event := models.Event{
  336. Name: "ButtonPressed",
  337. Payload: map[string]string{
  338. "buttonIndex": "0",
  339. "buttonName": "Touch",
  340. "deviceId": strconv.Itoa(externalID),
  341. },
  342. }
  343. if hasSwipedFromID {
  344. event.Payload["swipedFromDeviceId"] = strconv.Itoa(swipedFromID)
  345. }
  346. ch <- event
  347. cooldownID = externalID
  348. cooldownUntil = time.Now().Add(time.Second)
  349. }
  350. }
  351. }
  352. teardownAndRetry:
  353. if touchListener != nil {
  354. _ = touchListener.Close()
  355. }
  356. if reqCloser != nil {
  357. _ = reqCloser.Close()
  358. reqCloser = nil
  359. }
  360. if ctx.Err() != nil {
  361. break
  362. }
  363. time.Sleep(time.Second * 3)
  364. }
  365. }