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.

446 lines
9.6 KiB

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