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.

462 lines
9.9 KiB

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