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.

455 lines
9.9 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. log.Printf("%#+v", panelInfo)
  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. idBytes := [2]byte{0, 0}
  99. binary.BigEndian.PutUint16(idBytes[:], panelInfo.PanelID)
  100. hexID := hex.EncodeToString(idBytes[:])
  101. fullID := fmt.Sprintf("nanoleaf:%s:%s", b.host, hexID)
  102. newIDs = append(newIDs, fullID)
  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 newIDs, 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. transitionTime := time.Now().Add(time.Millisecond * 255)
  148. b.mu.Lock()
  149. for _, panel := range b.panels {
  150. if panel.FullID == id {
  151. panel.apply(change, transitionTime)
  152. break
  153. }
  154. }
  155. b.mu.Unlock()
  156. }
  157. func (b *bridge) UpdateBatch(batch commands.SetStateBatch) {
  158. transitionTime := time.Now().Add(time.Millisecond * 255)
  159. b.mu.Lock()
  160. for _, panel := range b.panels {
  161. if change, ok := batch[panel.FullID]; ok {
  162. panel.apply(change, transitionTime)
  163. }
  164. }
  165. b.mu.Unlock()
  166. }
  167. func (b *bridge) Run(ctx context.Context, bus *lucifer3.EventBus) error {
  168. err := b.updateEffect(ctx)
  169. if err != nil {
  170. return err
  171. }
  172. conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
  173. IP: net.ParseIP(b.host),
  174. Port: 60222,
  175. })
  176. if err != nil {
  177. return err
  178. }
  179. defer conn.Close()
  180. // Notify connections and disconnections
  181. bus.RunEvent(events.DeviceConnected{
  182. Prefix: fmt.Sprintf("nanoleaf:%s", b.host),
  183. })
  184. newIDs, err := b.Refresh(ctx)
  185. if err != nil {
  186. return err
  187. }
  188. // Start touch listener. This one should go down together with this one, though, so it needs a new context.
  189. ctx2, cancel := context.WithCancel(ctx)
  190. defer cancel()
  191. go b.runTouchListener(ctx2, bus)
  192. go func() {
  193. ticker := time.NewTicker(time.Second * 5)
  194. hadErrors := false
  195. for {
  196. select {
  197. case <-ticker.C:
  198. case <-ctx2.Done():
  199. return
  200. }
  201. reqTimeout, cancel := context.WithTimeout(ctx2, time.Second*4)
  202. err := b.updateEffect(reqTimeout)
  203. cancel()
  204. if err != nil {
  205. log.Println("Failed to update effects:", err, "This error is non-fatal, and it will be retried shortly.")
  206. hadErrors = true
  207. } else if hadErrors {
  208. b.mu.Lock()
  209. for _, panel := range b.panels {
  210. panel.Stale = true
  211. panel.SlowUpdates = 3
  212. panel.TicksUntilSlowUpdate = 10
  213. }
  214. b.mu.Unlock()
  215. hadErrors = false
  216. }
  217. }
  218. }()
  219. ticker := time.NewTicker(time.Millisecond * 100)
  220. defer ticker.Stop()
  221. strikes := 0
  222. for _, event := range b.HardwareEvents() {
  223. bus.RunEvent(event)
  224. }
  225. for _, id := range newIDs {
  226. bus.RunEvent(events.DeviceReady{ID: id})
  227. }
  228. bus.RunEvent(events.DeviceReady{ID: fmt.Sprintf("nanoleaf:%s", b.host)})
  229. for range ticker.C {
  230. if ctx.Err() != nil {
  231. break
  232. }
  233. panelUpdate := make(PanelUpdate, 2)
  234. b.mu.Lock()
  235. for _, panel := range b.panels {
  236. if !panel.Stale {
  237. if panel.SlowUpdates > 0 {
  238. panel.TicksUntilSlowUpdate -= 1
  239. if panel.TicksUntilSlowUpdate > 0 {
  240. continue
  241. }
  242. panel.TicksUntilSlowUpdate = 10
  243. panel.SlowUpdates -= 1
  244. } else {
  245. continue
  246. }
  247. } else {
  248. panel.Stale = false
  249. }
  250. panelUpdate.Add(panel.message())
  251. if panelUpdate.Len() > 1256 {
  252. break
  253. }
  254. }
  255. b.mu.Unlock()
  256. if panelUpdate.Len() == 0 {
  257. continue
  258. }
  259. _, err := conn.Write(panelUpdate)
  260. if err != nil {
  261. strikes++
  262. if strikes >= 3 {
  263. return err
  264. }
  265. } else {
  266. strikes = 0
  267. }
  268. }
  269. return nil
  270. }
  271. func (b *bridge) updateEffect(ctx context.Context) error {
  272. overview, err := b.Overview(ctx)
  273. if err != nil {
  274. return err
  275. }
  276. if overview.Effects.Select == "*Dynamic*" && overview.State.ColorMode == "effect" {
  277. return nil
  278. }
  279. req, err := http.NewRequest("PUT", b.URL("effects"), bytes.NewReader(httpMessage))
  280. if err != nil {
  281. return err
  282. }
  283. res, err := http.DefaultClient.Do(req.WithContext(ctx))
  284. if err != nil {
  285. return err
  286. }
  287. defer res.Body.Close()
  288. if res.StatusCode != 204 {
  289. return lucifer3.ErrUnexpectedResponse
  290. }
  291. b.mu.Lock()
  292. for _, panel := range b.panels {
  293. panel.Stale = true
  294. }
  295. b.mu.Unlock()
  296. return nil
  297. }
  298. func (b *bridge) runTouchListener(ctx context.Context, bus *lucifer3.EventBus) {
  299. cooldownMap := make(map[uint16]time.Time)
  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.go 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. panelID := message.PanelID(i)
  349. b.mu.Lock()
  350. id, hasID := b.panelIDMap[panelID]
  351. swipedFromID := b.panelIDMap[message.SwipedFromPanelID(i)]
  352. b.mu.Unlock()
  353. if time.Now().Before(cooldownMap[panelID]) {
  354. continue
  355. } else if !hasID {
  356. bus.RunEvent(events.ButtonPressed{
  357. ID: fmt.Sprintf("nanoleaf:%s:meta:%x", b.host, panelID),
  358. SwipedID: swipedFromID,
  359. Name: "Touch",
  360. })
  361. } else {
  362. bus.RunEvent(events.ButtonPressed{
  363. ID: id,
  364. SwipedID: swipedFromID,
  365. Name: "Touch",
  366. })
  367. }
  368. cooldownMap[panelID] = time.Now().Add(time.Second)
  369. }
  370. }
  371. }
  372. teardownAndRetry:
  373. if touchListener != nil {
  374. _ = touchListener.Close()
  375. }
  376. if reqCloser != nil {
  377. _ = reqCloser.Close()
  378. reqCloser = nil
  379. }
  380. select {
  381. case <-time.After(time.Second * 3):
  382. case <-ctx.Done():
  383. return
  384. }
  385. }
  386. }