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.

448 lines
9.7 KiB

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