rgb.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. DetectChannels() ([]via.Channel, error)
  18. Close() error
  19. }
  20. // keyboardFor is a seam for tests; it reads the optional keyboards.json.
  21. var keyboardFor = intdevice.KeyboardFor
  22. // targetDeviceData is everything a command needs before it opens the keyboard.
  23. type targetDeviceData struct {
  24. Device intdevice.Device
  25. Display map[uint16]string
  26. // Requested is nil when no --zone was given, which means every channel
  27. // the keyboard has.
  28. Requested []via.Channel
  29. }
  30. // prepareTarget resolves the keyboard, its display names and the requested
  31. // channels. Enumeration does not open a HID handle, so an unusable --zone value
  32. // is still rejected before the device is opened.
  33. func prepareTarget() (targetDeviceData, error) {
  34. devices, err := discoverAll()
  35. if err != nil {
  36. return targetDeviceData{}, fmt.Errorf("discover: %w", err)
  37. }
  38. dev, err := selectDevice(devices, targetDevice)
  39. if err != nil {
  40. return targetDeviceData{}, err
  41. }
  42. keyboard, _, err := keyboardFor(dev.VendorID, dev.ProductID)
  43. if err != nil {
  44. return targetDeviceData{}, err
  45. }
  46. requested, err := resolveZoneName(targetZone, keyboard.Channels)
  47. if err != nil {
  48. return targetDeviceData{}, err
  49. }
  50. return targetDeviceData{Device: dev, Display: keyboard.Channels, Requested: requested}, nil
  51. }
  52. func forEachChannel(channels []via.Channel, fn func(via.Channel) error) error {
  53. for _, ch := range channels {
  54. if err := fn(ch); err != nil {
  55. return err
  56. }
  57. }
  58. return nil
  59. }
  60. func setValueOnChannels(proto zoneProtocol, channels []via.Channel, param, value uint8) error {
  61. return forEachChannel(channels, func(ch via.Channel) error {
  62. return proto.SetValue(ch, param, value)
  63. })
  64. }
  65. // zoneResult records what was requested for a zone and what the keyboard
  66. // reported afterwards. The two differ on real hardware: the Impact 80 clamps
  67. // brightness at 160 on the logo and side channels and scales it up to 255 on
  68. // the backlight channel.
  69. type zoneResult struct {
  70. Name string
  71. Requested uint8
  72. Applied uint8
  73. }
  74. // Mismatch reports whether the keyboard applied something other than the
  75. // requested value.
  76. func (r zoneResult) Mismatch() bool { return r.Applied != r.Requested }
  77. // anyMismatch reports whether any zone deviated from the request.
  78. func anyMismatch(results []zoneResult) bool {
  79. for _, r := range results {
  80. if r.Mismatch() {
  81. return true
  82. }
  83. }
  84. return false
  85. }
  86. // formatResults renders one line stating what each selected zone actually
  87. // holds, so a partial application is visible instead of being summarised as
  88. // the value that was asked for.
  89. func formatResults(label string, results []zoneResult) string {
  90. var b strings.Builder
  91. b.WriteString(label)
  92. for _, r := range results {
  93. fmt.Fprintf(&b, " %s %d", r.Name, r.Applied)
  94. }
  95. if len(results) > 0 {
  96. fmt.Fprintf(&b, " (requested %d)", results[0].Requested)
  97. }
  98. return b.String()
  99. }
  100. // setValueVerified writes one value ID to every zone and reads each back, so a
  101. // clamped or rescaled value is reported instead of silently claimed as set.
  102. // The firmware transform differs per channel and per value ID, so the read-back
  103. // is not optional for any parameter the keyboard rescales.
  104. func setValueVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, param, value uint8) ([]zoneResult, error) {
  105. if err := setValueOnChannels(proto, channels, param, value); err != nil {
  106. return nil, err
  107. }
  108. results := make([]zoneResult, 0, len(channels))
  109. for _, ch := range channels {
  110. raw, err := proto.GetValue(ch, param)
  111. if err != nil {
  112. return nil, fmt.Errorf("read back value 0x%02x for %s: %w", param, channelName(ch, display), err)
  113. }
  114. if len(raw) == 0 {
  115. return nil, fmt.Errorf("read back value 0x%02x for %s: empty response", param, channelName(ch, display))
  116. }
  117. results = append(results, zoneResult{Name: channelName(ch, display), Requested: value, Applied: raw[0]})
  118. }
  119. return results, nil
  120. }
  121. // setBrightnessVerified writes the brightness and reads every channel back.
  122. func setBrightnessVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, value uint8) ([]zoneResult, error) {
  123. return setValueVerified(proto, channels, display, uint8(intrgb.Brightness), value)
  124. }
  125. // setSpeedVerified writes the speed and reads every channel back.
  126. func setSpeedVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, value uint8) ([]zoneResult, error) {
  127. return setValueVerified(proto, channels, display, uint8(intrgb.Speed), value)
  128. }
  129. // colorResult records the hue and saturation one channel holds after a color was
  130. // written to it. The color value ID carries two bytes, so both components are
  131. // read back: a keyboard that stored another saturation must not be reported as
  132. // having taken the requested color.
  133. type colorResult struct {
  134. Name string
  135. RequestedHue uint8
  136. RequestedSaturation uint8
  137. Hue uint8
  138. Saturation uint8
  139. }
  140. // Mismatch reports whether the keyboard stored something other than the
  141. // requested color.
  142. func (r colorResult) Mismatch() bool {
  143. return r.Hue != r.RequestedHue || r.Saturation != r.RequestedSaturation
  144. }
  145. // setColorVerified writes one hue and saturation to every channel and reads each
  146. // back, for the same reason setValueVerified exists: the command must report
  147. // what the keyboard holds, not what it was asked for.
  148. func setColorVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, hue, saturation uint8) ([]colorResult, error) {
  149. if err := setColorOnChannels(proto, channels, hue, saturation); err != nil {
  150. return nil, err
  151. }
  152. results := make([]colorResult, 0, len(channels))
  153. for _, ch := range channels {
  154. raw, err := proto.GetValue(ch, uint8(intrgb.ColorValue))
  155. if err != nil {
  156. return nil, fmt.Errorf("read back color for %s: %w", channelName(ch, display), err)
  157. }
  158. if len(raw) < 2 {
  159. return nil, fmt.Errorf("read back color for %s: got %d bytes, want hue and saturation", channelName(ch, display), len(raw))
  160. }
  161. results = append(results, colorResult{
  162. Name: channelName(ch, display),
  163. RequestedHue: hue,
  164. RequestedSaturation: saturation,
  165. Hue: raw[0],
  166. Saturation: raw[1],
  167. })
  168. }
  169. return results, nil
  170. }
  171. func setColorOnChannels(proto zoneProtocol, channels []via.Channel, hue, saturation uint8) error {
  172. return forEachChannel(channels, func(ch via.Channel) error {
  173. return proto.SetColor(ch, hue, saturation)
  174. })
  175. }
  176. func setBrightnessOnChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
  177. return setValueOnChannels(proto, channels, uint8(intrgb.Brightness), value)
  178. }
  179. func setSpeedChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
  180. return setValueOnChannels(proto, channels, uint8(intrgb.Speed), value)
  181. }
  182. func disableLightingOnChannels(proto zoneProtocol, channels []via.Channel) error {
  183. return forEachChannel(channels, func(ch via.Channel) error {
  184. if err := proto.SetValue(ch, uint8(intrgb.EffectID), 0); err != nil {
  185. return err
  186. }
  187. return proto.SetValue(ch, uint8(intrgb.Brightness), 0)
  188. })
  189. }
  190. func enableLightingOnChannels(proto zoneProtocol, channels []via.Channel, catalog *intrgb.Catalog) error {
  191. return forEachChannel(channels, func(ch via.Channel) error {
  192. effect, ok := catalog.DefaultEffect(ch)
  193. if !ok {
  194. return fmt.Errorf("no default effect for %s", channelName(ch, nil))
  195. }
  196. if err := proto.SetValue(ch, uint8(intrgb.EffectID), effect); err != nil {
  197. return err
  198. }
  199. return proto.SetValue(ch, uint8(intrgb.Brightness), 160)
  200. })
  201. }
  202. func setModeOnChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
  203. return setValueOnChannels(proto, channels, uint8(intrgb.EffectID), value)
  204. }
  205. // openTarget opens the keyboard and resolves the requested channels against the
  206. // ones it actually has. It is a seam because a command needs all three: the
  207. // handle it writes to, the names it reports with, and the channels it may touch.
  208. var openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) {
  209. target, err := prepareTarget()
  210. if err != nil {
  211. return nil, targetDeviceData{}, nil, err
  212. }
  213. proto, err := via.New(target.Device)
  214. if err != nil {
  215. return nil, targetDeviceData{}, nil, fmt.Errorf("open protocol: %w", err)
  216. }
  217. channels, err := resolveChannels(proto, target)
  218. if err != nil {
  219. proto.Close()
  220. return nil, targetDeviceData{}, nil, err
  221. }
  222. return proto, target, channels, nil
  223. }
  224. // resolveChannels intersects the requested channels with the detected ones, and
  225. // refuses a name that resolves to a channel this keyboard does not have.
  226. func resolveChannels(proto rgbProtocol, target targetDeviceData) ([]via.Channel, error) {
  227. present, err := proto.DetectChannels()
  228. if err != nil {
  229. return nil, err
  230. }
  231. if err := displayNameConflicts(target.Display, present); err != nil {
  232. return nil, err
  233. }
  234. if target.Requested == nil {
  235. if len(present) == 0 {
  236. return nil, fmt.Errorf("this keyboard exposes no VIA lighting channels")
  237. }
  238. return present, nil
  239. }
  240. presentSet := make(map[via.Channel]bool, len(present))
  241. for _, ch := range present {
  242. presentSet[ch] = true
  243. }
  244. var found []via.Channel
  245. for _, ch := range target.Requested {
  246. if presentSet[ch] {
  247. found = append(found, ch)
  248. }
  249. }
  250. if len(found) == 0 {
  251. return nil, fmt.Errorf("channel %s is not present on this keyboard", target.Requested[0].Subsystem())
  252. }
  253. return found, nil
  254. }
  255. // lightingEnabled reports whether a channel is lit: a mode other than none and a
  256. // brightness above zero.
  257. func lightingEnabled(mode, brightness uint8) bool {
  258. return mode != 0 && brightness > 0
  259. }
  260. func ParseUint8(s string) (uint8, error) {
  261. v, err := strconv.ParseUint(s, 10, 8)
  262. if err != nil {
  263. return 0, fmt.Errorf("invalid value: %w", err)
  264. }
  265. return uint8(v), nil
  266. }