rgb.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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. // targetDeviceData is everything a command needs before it opens the keyboard.
  21. type targetDeviceData struct {
  22. Device intdevice.Device
  23. Display map[uint16]string
  24. // Alternatives are the names a channel also answers to besides the one its
  25. // definition gives it, so a name the user already knows keeps working.
  26. Alternatives map[uint16][]string
  27. // Requested is nil when the zone was "all" or the command named none, which
  28. // both mean every channel the keyboard has. It is never nil because a
  29. // command forgot to ask: the ones that write refuse an unnamed zone at their
  30. // own arity, before anything is opened.
  31. Requested []via.Channel
  32. }
  33. // prepareTarget resolves the keyboard, its display names and the requested
  34. // channels. Enumeration does not open a HID handle, so an unusable zone is still
  35. // rejected before the device is opened.
  36. var prepareTarget = func(zone string) (targetDeviceData, error) {
  37. devices, err := discoverAll()
  38. if err != nil {
  39. return targetDeviceData{}, fmt.Errorf("discover: %w", err)
  40. }
  41. dev, err := selectDevice(devices, targetDevice)
  42. if err != nil {
  43. return targetDeviceData{}, err
  44. }
  45. // A channel is named by the definition file that describes the board, because
  46. // those are the names VIA shows. Without a definition the QMK subsystem name
  47. // follows from the channel number and is all there is.
  48. display, alternatives := applyDefinitionLabels(dev.VendorID, dev.ProductID)
  49. requested, err := resolveZoneName(zone, display, alternatives)
  50. if err != nil {
  51. return targetDeviceData{}, err
  52. }
  53. dev.Name = boardName(dev, display)
  54. return targetDeviceData{
  55. Device: dev,
  56. Display: display,
  57. Alternatives: alternatives,
  58. Requested: requested,
  59. }, nil
  60. }
  61. func forEachChannel(channels []via.Channel, fn func(via.Channel) error) error {
  62. for _, ch := range channels {
  63. if err := fn(ch); err != nil {
  64. return err
  65. }
  66. }
  67. return nil
  68. }
  69. func setValueOnChannels(proto zoneProtocol, channels []via.Channel, param, value uint8) error {
  70. return forEachChannel(channels, func(ch via.Channel) error {
  71. return proto.SetValue(ch, param, value)
  72. })
  73. }
  74. // zoneResult records what was requested for a zone and what the keyboard
  75. // reported afterwards. The two differ on real hardware: the Impact 80 clamps
  76. // brightness at 160 on the logo and side channels and scales it up to 255 on
  77. // the backlight channel.
  78. type zoneResult struct {
  79. Name string
  80. Requested uint8
  81. Applied uint8
  82. }
  83. // Mismatch reports whether the keyboard applied something other than the
  84. // requested value.
  85. func (r zoneResult) Mismatch() bool { return r.Applied != r.Requested }
  86. // anyMismatch reports whether any zone deviated from the request.
  87. func anyMismatch(results []zoneResult) bool {
  88. for _, r := range results {
  89. if r.Mismatch() {
  90. return true
  91. }
  92. }
  93. return false
  94. }
  95. // formatResults renders one line stating what each selected zone actually
  96. // holds, so a partial application is visible instead of being summarised as
  97. // the value that was asked for.
  98. func formatResults(label string, results []zoneResult) string {
  99. var b strings.Builder
  100. b.WriteString(label)
  101. for _, r := range results {
  102. fmt.Fprintf(&b, " %s %d", r.Name, r.Applied)
  103. }
  104. if len(results) > 0 {
  105. fmt.Fprintf(&b, " (requested %d)", results[0].Requested)
  106. }
  107. return b.String()
  108. }
  109. // readBackValues reads one value ID from every channel, in the order given.
  110. // Every verified set reads back through this, so a parameter that needs no new
  111. // read-back path reuses it.
  112. func readBackValues(proto rgbProtocol, channels []via.Channel, display map[uint16]string, param uint8) ([]uint8, error) {
  113. values := make([]uint8, 0, len(channels))
  114. for _, ch := range channels {
  115. raw, err := proto.GetValue(ch, param)
  116. if err != nil {
  117. return nil, fmt.Errorf("read back value 0x%02x for %s: %w", param, channelName(ch, display), err)
  118. }
  119. if len(raw) == 0 {
  120. return nil, fmt.Errorf("read back value 0x%02x for %s: empty response", param, channelName(ch, display))
  121. }
  122. values = append(values, raw[0])
  123. }
  124. return values, nil
  125. }
  126. // setValueVerified writes one value ID to every zone and reads each back, so a
  127. // clamped or rescaled value is reported instead of silently claimed as set.
  128. // The firmware transform differs per channel and per value ID, so the read-back
  129. // is not optional for any parameter the keyboard rescales.
  130. func setValueVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, param, value uint8) ([]zoneResult, error) {
  131. if err := setValueOnChannels(proto, channels, param, value); err != nil {
  132. return nil, err
  133. }
  134. applied, err := readBackValues(proto, channels, display, param)
  135. if err != nil {
  136. return nil, err
  137. }
  138. results := make([]zoneResult, 0, len(channels))
  139. for i, ch := range channels {
  140. results = append(results, zoneResult{Name: channelName(ch, display), Requested: value, Applied: applied[i]})
  141. }
  142. return results, nil
  143. }
  144. // effectResult records the effect a zone was asked for and the one the keyboard
  145. // holds afterwards. The Impact 80's logo and side channels do not take effect ID
  146. // 0: the firmware reads it as "lighting off" and leaves the mode register where
  147. // it was, so the effect that is still running has to be named rather than
  148. // reported as the requested one.
  149. type effectResult struct {
  150. Name string
  151. RequestedName string
  152. RequestedID uint8
  153. AppliedName string
  154. AppliedID uint8
  155. }
  156. // Mismatch reports whether the keyboard holds an effect other than the one
  157. // requested.
  158. func (r effectResult) Mismatch() bool { return r.AppliedID != r.RequestedID }
  159. // anyEffectMismatch reports whether any zone kept an effect of its own.
  160. func anyEffectMismatch(results []effectResult) bool {
  161. for _, r := range results {
  162. if r.Mismatch() {
  163. return true
  164. }
  165. }
  166. return false
  167. }
  168. // formatEffectResults names the effect each selected zone actually holds, so an
  169. // effect the keyboard refused is visible instead of being summarised as the one
  170. // that was asked for.
  171. func formatEffectResults(requestedName string, results []effectResult) string {
  172. var b strings.Builder
  173. b.WriteString("Effect")
  174. for _, r := range results {
  175. fmt.Fprintf(&b, " %s %q (%d)", r.Name, r.AppliedName, r.AppliedID)
  176. }
  177. if len(results) > 0 {
  178. fmt.Fprintf(&b, " (requested %q (%d))", requestedName, results[0].RequestedID)
  179. }
  180. return b.String()
  181. }
  182. // setEffectVerified writes one effect ID per channel and reads every channel
  183. // back. It cannot reuse setValueVerified, because one effect name is a
  184. // different index on each subsystem, so the requested value is carried per
  185. // target while the read-back stays shared.
  186. func setEffectVerified(proto rgbProtocol, targets []intrgb.EffectTarget, display map[uint16]string, catalog *intrgb.Catalog) ([]effectResult, error) {
  187. channels := make([]via.Channel, 0, len(targets))
  188. requested := make(map[via.Channel]uint8, len(targets))
  189. for _, t := range targets {
  190. if err := proto.SetValue(t.Channel, uint8(intrgb.EffectID), t.ID); err != nil {
  191. return nil, fmt.Errorf("set effect on %s: %w", channelName(t.Channel, display), err)
  192. }
  193. channels = append(channels, t.Channel)
  194. requested[t.Channel] = t.ID
  195. }
  196. applied, err := readBackValues(proto, channels, display, uint8(intrgb.EffectID))
  197. if err != nil {
  198. return nil, err
  199. }
  200. results := make([]effectResult, 0, len(channels))
  201. for i, ch := range channels {
  202. results = append(results, effectResult{
  203. Name: channelName(ch, display),
  204. RequestedName: catalog.EffectName(ch, requested[ch]),
  205. RequestedID: requested[ch],
  206. AppliedName: catalog.EffectName(ch, applied[i]),
  207. AppliedID: applied[i],
  208. })
  209. }
  210. return results, nil
  211. }
  212. // setBrightnessVerified writes the brightness and reads every channel back.
  213. func setBrightnessVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, value uint8) ([]zoneResult, error) {
  214. return setValueVerified(proto, channels, display, uint8(intrgb.Brightness), value)
  215. }
  216. // setSpeedVerified writes the speed and reads every channel back.
  217. func setSpeedVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, value uint8) ([]zoneResult, error) {
  218. return setValueVerified(proto, channels, display, uint8(intrgb.Speed), value)
  219. }
  220. // formatEffectIDResults renders the same line as formatEffectResults, with the
  221. // numbers: the request was a number, and a board without a definition has no name
  222. // to put in its place.
  223. func formatEffectIDResults(results []effectResult) string {
  224. var b strings.Builder
  225. b.WriteString("Effect")
  226. for _, r := range results {
  227. fmt.Fprintf(&b, " %s %d", r.Name, r.AppliedID)
  228. }
  229. if len(results) > 0 {
  230. fmt.Fprintf(&b, " (requested %d)", results[0].RequestedID)
  231. }
  232. return b.String()
  233. }
  234. // colorResult records the hue and saturation one channel holds after a color was
  235. // written to it. The color value ID carries two bytes, so both components are
  236. // read back: a keyboard that stored another saturation must not be reported as
  237. // having taken the requested color.
  238. type colorResult struct {
  239. Name string
  240. RequestedHue uint8
  241. RequestedSaturation uint8
  242. Hue uint8
  243. Saturation uint8
  244. }
  245. // Mismatch reports whether the keyboard stored something other than the
  246. // requested color.
  247. func (r colorResult) Mismatch() bool {
  248. return r.Hue != r.RequestedHue || r.Saturation != r.RequestedSaturation
  249. }
  250. // setColorVerified writes one hue and saturation to every channel and reads each
  251. // back, for the same reason setValueVerified exists: the command must report
  252. // what the keyboard holds, not what it was asked for.
  253. func setColorVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, hue, saturation uint8) ([]colorResult, error) {
  254. if err := setColorOnChannels(proto, channels, hue, saturation); err != nil {
  255. return nil, err
  256. }
  257. results := make([]colorResult, 0, len(channels))
  258. for _, ch := range channels {
  259. raw, err := proto.GetValue(ch, uint8(intrgb.ColorValue))
  260. if err != nil {
  261. return nil, fmt.Errorf("read back color for %s: %w", channelName(ch, display), err)
  262. }
  263. if len(raw) < 2 {
  264. return nil, fmt.Errorf("read back color for %s: got %d bytes, want hue and saturation", channelName(ch, display), len(raw))
  265. }
  266. results = append(results, colorResult{
  267. Name: channelName(ch, display),
  268. RequestedHue: hue,
  269. RequestedSaturation: saturation,
  270. Hue: raw[0],
  271. Saturation: raw[1],
  272. })
  273. }
  274. return results, nil
  275. }
  276. func setColorOnChannels(proto zoneProtocol, channels []via.Channel, hue, saturation uint8) error {
  277. return forEachChannel(channels, func(ch via.Channel) error {
  278. return proto.SetColor(ch, hue, saturation)
  279. })
  280. }
  281. func setBrightnessOnChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
  282. return setValueOnChannels(proto, channels, uint8(intrgb.Brightness), value)
  283. }
  284. func setSpeedChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
  285. return setValueOnChannels(proto, channels, uint8(intrgb.Speed), value)
  286. }
  287. func disableLightingOnChannels(proto zoneProtocol, channels []via.Channel) error {
  288. return forEachChannel(channels, func(ch via.Channel) error {
  289. if err := proto.SetValue(ch, uint8(intrgb.EffectID), 0); err != nil {
  290. return err
  291. }
  292. return proto.SetValue(ch, uint8(intrgb.Brightness), 0)
  293. })
  294. }
  295. func enableLightingOnChannels(proto zoneProtocol, channels []via.Channel, catalog *intrgb.Catalog) error {
  296. return forEachChannel(channels, func(ch via.Channel) error {
  297. effect, ok := catalog.DefaultEffect(ch)
  298. if !ok {
  299. return fmt.Errorf("no default effect for %s", channelName(ch, nil))
  300. }
  301. if err := proto.SetValue(ch, uint8(intrgb.EffectID), effect); err != nil {
  302. return err
  303. }
  304. return proto.SetValue(ch, uint8(intrgb.Brightness), 160)
  305. })
  306. }
  307. // openTarget opens the keyboard and resolves the requested channels against the
  308. // ones it actually has. It is a seam because a command needs all three: the
  309. // handle it writes to, the names it reports with, and the channels it may touch.
  310. var openTarget = func(zone string) (rgbProtocol, targetDeviceData, []via.Channel, error) {
  311. target, err := prepareTarget(zone)
  312. if err != nil {
  313. return nil, targetDeviceData{}, nil, err
  314. }
  315. proto, err := via.New(target.Device)
  316. if err != nil {
  317. return nil, targetDeviceData{}, nil, fmt.Errorf("open protocol: %w", err)
  318. }
  319. channels, err := resolveChannels(proto, target)
  320. if err != nil {
  321. proto.Close()
  322. return nil, targetDeviceData{}, nil, err
  323. }
  324. return proto, target, channels, nil
  325. }
  326. // resolveChannels intersects the requested channels with the detected ones, and
  327. // refuses a name that resolves to a channel this keyboard does not have. A
  328. // selection of several channels is refused as a whole when one of them is
  329. // missing: a command that quietly wrote two of the three channels it was asked
  330. // for would report a success it did not deliver.
  331. func resolveChannels(proto rgbProtocol, target targetDeviceData) ([]via.Channel, error) {
  332. present, err := proto.DetectChannels()
  333. if err != nil {
  334. return nil, err
  335. }
  336. if err := displayNameConflicts(target.Display, present); err != nil {
  337. return nil, err
  338. }
  339. if target.Requested == nil {
  340. if len(present) == 0 {
  341. return nil, fmt.Errorf("this keyboard exposes no VIA lighting channels")
  342. }
  343. return present, nil
  344. }
  345. presentSet := make(map[via.Channel]bool, len(present))
  346. for _, ch := range present {
  347. presentSet[ch] = true
  348. }
  349. var found []via.Channel
  350. var missing []string
  351. for _, ch := range target.Requested {
  352. if presentSet[ch] {
  353. found = append(found, ch)
  354. } else {
  355. missing = append(missing, channelName(ch, target.Display))
  356. }
  357. }
  358. if len(missing) > 0 {
  359. return nil, fmt.Errorf("this keyboard has no %s channel", strings.Join(missing, " or "))
  360. }
  361. return found, nil
  362. }
  363. // lightingEnabled reports whether a channel is lit: a mode other than none and a
  364. // brightness above zero.
  365. func lightingEnabled(mode, brightness uint8) bool {
  366. return mode != 0 && brightness > 0
  367. }
  368. func ParseUint8(s string) (uint8, error) {
  369. v, err := strconv.ParseUint(s, 10, 8)
  370. if err != nil {
  371. return 0, fmt.Errorf("invalid value: %w", err)
  372. }
  373. return uint8(v), nil
  374. }