rgb.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package main
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. intdevice "netdome.biz/paul/qmk-rgb/internal/device"
  7. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  8. "netdome.biz/paul/qmk-rgb/internal/via"
  9. )
  10. type zoneProtocol interface {
  11. SetValue(via.Channel, uint8, uint8) error
  12. SetColor(via.Channel, uint8, uint8) error
  13. }
  14. type rgbProtocol interface {
  15. zoneProtocol
  16. GetValue(via.Channel, uint8) ([]byte, error)
  17. Close() error
  18. }
  19. var openRGBProtocol = func() (rgbProtocol, error) {
  20. return OpenDevice()
  21. }
  22. // keyboardFor is a seam for tests; it reads the optional keyboards.json.
  23. var keyboardFor = intdevice.KeyboardFor
  24. // targetDeviceData is everything a command needs before it opens the keyboard.
  25. type targetDeviceData struct {
  26. Device intdevice.Device
  27. Display map[uint16]string
  28. // Requested is nil when no --zone was given, which means every channel
  29. // the keyboard has.
  30. Requested []via.Channel
  31. }
  32. // prepareTarget resolves the keyboard, its display names and the requested
  33. // channels. Enumeration does not open a HID handle, so an unusable --zone value
  34. // is still rejected before the device is opened.
  35. func prepareTarget() (targetDeviceData, error) {
  36. devices, err := discoverAll()
  37. if err != nil {
  38. return targetDeviceData{}, fmt.Errorf("discover: %w", err)
  39. }
  40. dev, err := selectDevice(devices, targetDevice)
  41. if err != nil {
  42. return targetDeviceData{}, err
  43. }
  44. keyboard, _, err := keyboardFor(dev.VendorID, dev.ProductID)
  45. if err != nil {
  46. return targetDeviceData{}, err
  47. }
  48. requested, err := resolveZoneName(targetZone, keyboard.Channels)
  49. if err != nil {
  50. return targetDeviceData{}, err
  51. }
  52. return targetDeviceData{Device: dev, Display: keyboard.Channels, Requested: requested}, nil
  53. }
  54. func forEachSelectedZone(zones []intrgb.Zone, fn func(intrgb.Zone, via.Channel) error) error {
  55. channels := zoneChannels(zones)
  56. for i, zone := range zones {
  57. if err := fn(zone, channels[i]); err != nil {
  58. return err
  59. }
  60. }
  61. return nil
  62. }
  63. func setValueOnZones(proto zoneProtocol, zones []intrgb.Zone, param, value uint8) error {
  64. return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.Channel) error {
  65. return proto.SetValue(channel, param, value)
  66. })
  67. }
  68. func setBrightnessOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error {
  69. return setValueOnZones(proto, zones, uint8(intrgb.Brightness), value)
  70. }
  71. // zoneResult records what was requested for a zone and what the keyboard
  72. // reported afterwards. The two differ on real hardware: the Impact 80 clamps
  73. // brightness at 160 on the logo and side channels and scales it up to 255 on
  74. // the backlight channel.
  75. type zoneResult struct {
  76. Zone intrgb.Zone
  77. Requested uint8
  78. Applied uint8
  79. }
  80. // Mismatch reports whether the keyboard applied something other than the
  81. // requested value.
  82. func (r zoneResult) Mismatch() bool { return r.Applied != r.Requested }
  83. // anyMismatch reports whether any zone deviated from the request.
  84. func anyMismatch(results []zoneResult) bool {
  85. for _, r := range results {
  86. if r.Mismatch() {
  87. return true
  88. }
  89. }
  90. return false
  91. }
  92. // formatResults renders one line stating what each selected zone actually
  93. // holds, so a partial application is visible instead of being summarised as
  94. // the value that was asked for.
  95. func formatResults(label string, results []zoneResult) string {
  96. var b strings.Builder
  97. b.WriteString(label)
  98. for _, r := range results {
  99. fmt.Fprintf(&b, " %s %d", r.Zone, r.Applied)
  100. }
  101. if len(results) > 0 {
  102. fmt.Fprintf(&b, " (requested %d)", results[0].Requested)
  103. }
  104. return b.String()
  105. }
  106. // setValueVerified writes one value ID to every zone and reads each back, so a
  107. // clamped or rescaled value is reported instead of silently claimed as set.
  108. // The firmware transform differs per channel and per value ID, so the read-back
  109. // is not optional for any parameter the keyboard rescales.
  110. func setValueVerified(proto rgbProtocol, zones []intrgb.Zone, param, value uint8) ([]zoneResult, error) {
  111. if err := setValueOnZones(proto, zones, param, value); err != nil {
  112. return nil, err
  113. }
  114. results := make([]zoneResult, 0, len(zones))
  115. for _, zone := range zones {
  116. raw, err := proto.GetValue(via.Channel(zone.Channel()), param)
  117. if err != nil {
  118. return nil, fmt.Errorf("read back value 0x%02x for %s: %w", param, zone, err)
  119. }
  120. if len(raw) == 0 {
  121. return nil, fmt.Errorf("read back value 0x%02x for %s: empty response", param, zone)
  122. }
  123. results = append(results, zoneResult{Zone: zone, Requested: value, Applied: raw[0]})
  124. }
  125. return results, nil
  126. }
  127. // setBrightnessVerified writes the brightness and reads every zone back.
  128. func setBrightnessVerified(proto rgbProtocol, zones []intrgb.Zone, value uint8) ([]zoneResult, error) {
  129. return setValueVerified(proto, zones, uint8(intrgb.Brightness), value)
  130. }
  131. // setSpeedVerified writes the speed and reads every zone back.
  132. func setSpeedVerified(proto rgbProtocol, zones []intrgb.Zone, value uint8) ([]zoneResult, error) {
  133. return setValueVerified(proto, zones, uint8(intrgb.Speed), value)
  134. }
  135. // colorResult records the hue and saturation one zone holds after a color was
  136. // written to it. The color value ID carries two bytes, so both components are
  137. // read back: a keyboard that stored another saturation must not be reported as
  138. // having taken the requested color.
  139. type colorResult struct {
  140. Zone intrgb.Zone
  141. RequestedHue uint8
  142. RequestedSaturation uint8
  143. Hue uint8
  144. Saturation uint8
  145. }
  146. // Mismatch reports whether the keyboard stored something other than the
  147. // requested color.
  148. func (r colorResult) Mismatch() bool {
  149. return r.Hue != r.RequestedHue || r.Saturation != r.RequestedSaturation
  150. }
  151. // setColorVerified writes one hue and saturation to every zone and reads each
  152. // back, for the same reason setValueVerified exists: the command must report
  153. // what the keyboard holds, not what it was asked for.
  154. func setColorVerified(proto rgbProtocol, zones []intrgb.Zone, hue, saturation uint8) ([]colorResult, error) {
  155. if err := setColorOnZones(proto, zones, hue, saturation); err != nil {
  156. return nil, err
  157. }
  158. results := make([]colorResult, 0, len(zones))
  159. for _, zone := range zones {
  160. raw, err := proto.GetValue(via.Channel(zone.Channel()), uint8(intrgb.ColorValue))
  161. if err != nil {
  162. return nil, fmt.Errorf("read back color for %s: %w", zone, err)
  163. }
  164. if len(raw) < 2 {
  165. return nil, fmt.Errorf("read back color for %s: got %d bytes, want hue and saturation", zone, len(raw))
  166. }
  167. results = append(results, colorResult{
  168. Zone: zone,
  169. RequestedHue: hue,
  170. RequestedSaturation: saturation,
  171. Hue: raw[0],
  172. Saturation: raw[1],
  173. })
  174. }
  175. return results, nil
  176. }
  177. func setSpeedOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error {
  178. return setValueOnZones(proto, zones, uint8(intrgb.Speed), value)
  179. }
  180. func setColorOnZones(proto zoneProtocol, zones []intrgb.Zone, hue, saturation uint8) error {
  181. return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.Channel) error {
  182. return proto.SetColor(channel, hue, saturation)
  183. })
  184. }
  185. func disableLightingOnZones(proto zoneProtocol, zones []intrgb.Zone) error {
  186. return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.Channel) error {
  187. if err := proto.SetValue(channel, uint8(intrgb.EffectID), 0); err != nil {
  188. return err
  189. }
  190. return proto.SetValue(channel, uint8(intrgb.Brightness), 0)
  191. })
  192. }
  193. func enableLightingOnZones(proto zoneProtocol, zones []intrgb.Zone) error {
  194. return forEachSelectedZone(zones, func(zone intrgb.Zone, channel via.Channel) error {
  195. if err := proto.SetValue(channel, uint8(intrgb.EffectID), intrgb.DefaultEffect(zone)); err != nil {
  196. return err
  197. }
  198. return proto.SetValue(channel, uint8(intrgb.Brightness), 160)
  199. })
  200. }
  201. func setModeOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error {
  202. return setValueOnZones(proto, zones, uint8(intrgb.EffectID), value)
  203. }
  204. // OpenDevice discovers the connected QMK keyboards and opens the one selected
  205. // by --device. Without --device it only proceeds when exactly one keyboard is
  206. // connected, so a command can never hit an unintended keyboard.
  207. func OpenDevice() (*via.Protocol, error) {
  208. if _, err := selectedZones(); err != nil {
  209. return nil, err
  210. }
  211. devices, err := intdevice.DiscoverAll()
  212. if err != nil {
  213. return nil, fmt.Errorf("discover: %w", err)
  214. }
  215. dev, err := selectDevice(devices, targetDevice)
  216. if err != nil {
  217. return nil, err
  218. }
  219. proto, err := via.New(dev)
  220. if err != nil {
  221. return nil, fmt.Errorf("open protocol: %w", err)
  222. }
  223. return proto, nil
  224. }
  225. func lightingEnabled(mode, brightness uint8) bool {
  226. return mode != 0 && brightness > 0
  227. }
  228. func ParseUint8(s string) (uint8, error) {
  229. v, err := strconv.ParseUint(s, 10, 8)
  230. if err != nil {
  231. return 0, fmt.Errorf("invalid value: %w", err)
  232. }
  233. return uint8(v), nil
  234. }