catalog.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. package rgb
  2. import (
  3. "fmt"
  4. "netdome.biz/paul/qmk-rgb/internal/via"
  5. )
  6. // EffectTarget is one effect ID to write to one channel.
  7. type EffectTarget struct {
  8. Channel via.Channel
  9. ID uint8
  10. }
  11. const unknownEffectName = "unknown"
  12. // Catalog is one board's effect names, per channel. The keyboard holds numbers,
  13. // not names, so `effect <name>` needs a catalog and a board without one is
  14. // driven through raw IDs.
  15. type Catalog struct {
  16. board string
  17. names map[via.Channel][]string
  18. aliases map[via.Channel]map[string]string
  19. defaults map[via.Channel]uint8
  20. }
  21. // CatalogFor returns the effect catalog of a board, and whether one exists.
  22. //
  23. // Provenance, because these lists look invented and are not:
  24. //
  25. // - The 46 backlight names are the QMK rgb_matrix_effects.inc of the VIA era,
  26. // in order, transcribed from this board's vendor VIA definition. They are not
  27. // current QMK master, which has 45 entries under other names and other IDs,
  28. // so the list must not be repaired against upstream. Each of the 46 IDs was
  29. // taken by the keyboard when written; the board also takes ID 46, which no
  30. // name in this list covers.
  31. // - The 7 logo and 7 side names are this board's vendor VIA definition, whose
  32. // dropdowns read "fixed wave" and "breathe". The tool spells them
  33. // fixed_wave and breathing and accepts the vendor's spellings as aliases.
  34. // The keyboard takes these IDs only from 1 to 6: ID 0 means "lighting off"
  35. // and leaves the mode register untouched.
  36. //
  37. // Both lists come from the VIA JSON the vendor's Driver & Firmware page links at
  38. // https://wiki.wobkey.com/en/Products/PMOKEY-Impact-80/Driver-Firmware
  39. // (the file itself: https://drive.wobkey.com/f/d/6BtO/Impact_80.JSON). That page
  40. // is also where the firmware variants are: the proprietary one offers the
  41. // advanced lighting but disables VIA, so a board running it has no Raw HID
  42. // interface and is invisible here rather than unsupported.
  43. // - The brightness and speed transforms documented in README.md were measured
  44. // on the unit, not read from anywhere.
  45. func CatalogFor(vendorID, productID uint16) (*Catalog, bool) {
  46. if vendorID != 0x36B0 || productID != 0x309F {
  47. return nil, false
  48. }
  49. return &Catalog{
  50. board: "impact80",
  51. names: map[via.Channel][]string{
  52. via.ChannelRgblight: impact80LogoEffects[:],
  53. via.ChannelRgbMatrix: impact80BacklightEffects[:],
  54. via.ChannelAudio: impact80SideEffects[:],
  55. },
  56. aliases: map[via.Channel]map[string]string{
  57. via.ChannelRgblight: {
  58. "off": "none",
  59. "breathe": "breathing",
  60. "rainbow": "spectrum",
  61. "rainbow_wave": "wave",
  62. "solid": "light",
  63. "static": "solid",
  64. },
  65. via.ChannelRgbMatrix: {
  66. "off": "none",
  67. "breathe": "breathing",
  68. "rainbow": "rainbow_moving_chevron",
  69. "solid": "solid_color",
  70. "static": "solid",
  71. },
  72. via.ChannelAudio: {
  73. "off": "none",
  74. "breathe": "breathing",
  75. "rainbow": "spectrum",
  76. "rainbow_wave": "wave",
  77. "solid": "light",
  78. "static": "solid",
  79. },
  80. },
  81. defaults: map[via.Channel]uint8{
  82. via.ChannelRgblight: 4,
  83. via.ChannelRgbMatrix: 5,
  84. via.ChannelAudio: 4,
  85. },
  86. }, true
  87. }
  88. // Name returns the catalog's board name, which `effect --list` reports.
  89. func (c *Catalog) Name() string {
  90. if c == nil {
  91. return ""
  92. }
  93. return c.board
  94. }
  95. // Names returns the effect names of a channel, or nil when the catalog says
  96. // nothing about it.
  97. func (c *Catalog) Names(ch via.Channel) []string {
  98. if c == nil {
  99. return nil
  100. }
  101. return c.names[ch]
  102. }
  103. // EffectName returns the name of an effect ID, or "unknown" when the catalog has
  104. // no entry for it.
  105. func (c *Catalog) EffectName(ch via.Channel, id uint8) string {
  106. if c == nil {
  107. return unknownEffectName
  108. }
  109. names := c.names[ch]
  110. if int(id) >= len(names) {
  111. return unknownEffectName
  112. }
  113. return names[id]
  114. }
  115. // EffectID resolves an effect name on a channel, following one level of alias.
  116. func (c *Catalog) EffectID(ch via.Channel, name string) (uint8, bool) {
  117. if c == nil {
  118. return 0, false
  119. }
  120. for id, candidate := range c.names[ch] {
  121. if candidate == name {
  122. return uint8(id), true
  123. }
  124. }
  125. if canonical, ok := c.aliases[ch][name]; ok {
  126. for id, candidate := range c.names[ch] {
  127. if candidate == canonical {
  128. return uint8(id), true
  129. }
  130. }
  131. }
  132. return 0, false
  133. }
  134. // DefaultEffect returns the effect ID enable writes when turning a channel on.
  135. func (c *Catalog) DefaultEffect(ch via.Channel) (uint8, bool) {
  136. if c == nil {
  137. return 0, false
  138. }
  139. id, ok := c.defaults[ch]
  140. return id, ok
  141. }
  142. // ResolveEffect turns an effect name into one target per channel that supports
  143. // it, plus the subsystem names of those that do not. A skip is an error rather
  144. // than a warning when the caller asked for one channel explicitly, because a
  145. // command that silently did nothing looks like a command that worked. That is
  146. // the caller's knowledge to pass: a board with a single lighting channel is not
  147. // an explicit request for it.
  148. func ResolveEffect(catalog *Catalog, name string, channels []via.Channel, explicit bool) ([]EffectTarget, []string, error) {
  149. if catalog == nil {
  150. return nil, nil, fmt.Errorf("no effect catalog for this keyboard; set an effect by number with `mode <index>`")
  151. }
  152. // The compatibility spelling every catalog shares, so a caller cannot
  153. // resolve "static" differently from another.
  154. if name == "static" {
  155. name = "solid"
  156. }
  157. // Whether the board has the name at all is a different question from
  158. // whether the channels asked for can do it: naming a real effect on the
  159. // wrong channel is a different mistake, with a different fix, and gets its
  160. // own message.
  161. knownAnywhere := false
  162. for ch := range catalog.names {
  163. if _, ok := catalog.EffectID(ch, name); ok {
  164. knownAnywhere = true
  165. break
  166. }
  167. }
  168. if !knownAnywhere {
  169. return nil, nil, fmt.Errorf("unknown effect: %s", name)
  170. }
  171. var targets []EffectTarget
  172. var skipped []string
  173. for _, ch := range channels {
  174. id, ok := catalog.EffectID(ch, name)
  175. if !ok {
  176. if explicit {
  177. return nil, nil, fmt.Errorf("effect %s is not supported on %s", name, ch.Subsystem())
  178. }
  179. skipped = append(skipped, ch.Subsystem())
  180. continue
  181. }
  182. targets = append(targets, EffectTarget{Channel: ch, ID: id})
  183. }
  184. return targets, skipped, nil
  185. }