catalog.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. package rgb
  2. import (
  3. "fmt"
  4. "strings"
  5. "netdome.biz/paul/qmk-rgb/internal/via"
  6. )
  7. // EffectTarget is one effect ID to write to one channel.
  8. type EffectTarget struct {
  9. Channel via.Channel
  10. ID uint8
  11. }
  12. const unknownEffectName = "unknown"
  13. // Effect is one named effect on a channel: the ID the keyboard uses and the name
  14. // a definition gives it. The pair is explicit because a VIA definition may
  15. // attach a number to an option that is not its position in the list, so a
  16. // board's effect ID is not an index into its names.
  17. type Effect struct {
  18. ID uint8
  19. Name string
  20. }
  21. // Catalog is one board's effect names, per channel. The keyboard holds numbers,
  22. // not names, so `effect <name>` needs a catalog and a board without one is
  23. // driven through raw IDs.
  24. type Catalog struct {
  25. board string
  26. names map[via.Channel][]Effect
  27. aliases map[via.Channel]map[string]string
  28. defaults map[via.Channel]uint8
  29. }
  30. // dense turns a name list whose index is the effect ID into effect entries. It
  31. // is how a hand-written list becomes a catalog; a definition carries its IDs
  32. // itself and does not go through here.
  33. func dense(names []string) []Effect {
  34. effects := make([]Effect, 0, len(names))
  35. for id, name := range names {
  36. effects = append(effects, Effect{ID: uint8(id), Name: name})
  37. }
  38. return effects
  39. }
  40. // NewCatalog returns a catalog for a board described by effect entries, as a VIA
  41. // definition file provides them. The board name is what `effect --list` reports;
  42. // the entries may leave gaps, because a definition names only the effects a
  43. // board's firmware implements.
  44. //
  45. // The tool's aliases come with it, because they are a spelling convenience and
  46. // not board knowledge: a definition writes the manufacturer's spelling, and the
  47. // user should not have to know which one it chose.
  48. func NewCatalog(board string, effects map[via.Channel][]Effect) *Catalog {
  49. return &Catalog{board: board, names: effects, aliases: defaultAliases}
  50. }
  51. // defaultAliases are the spellings every catalog accepts, whatever the source of
  52. // its names. They resolve per channel, so `rainbow` is the vendor's `spectrum`
  53. // where that is the name and the catalog's own on another channel.
  54. var defaultAliases = map[via.Channel]map[string]string{
  55. via.ChannelRgblight: {
  56. "off": "none",
  57. "breathe": "breathing",
  58. "rainbow": "spectrum",
  59. "rainbow_wave": "wave",
  60. "solid": "light",
  61. "static": "solid",
  62. },
  63. via.ChannelRgbMatrix: {
  64. "off": "none",
  65. "breathe": "breathing",
  66. "rainbow": "rainbow_moving_chevron",
  67. "solid": "solid_color",
  68. "static": "solid",
  69. },
  70. via.ChannelAudio: {
  71. "off": "none",
  72. "breathe": "breathing",
  73. "rainbow": "spectrum",
  74. "rainbow_wave": "wave",
  75. "solid": "light",
  76. "static": "solid",
  77. },
  78. }
  79. // CatalogFor returns the effect catalog of a board, and whether one exists.
  80. //
  81. // Provenance, because these lists look invented and are not:
  82. //
  83. // - The 46 backlight names are the QMK rgb_matrix_effects.inc of the VIA era,
  84. // in order, transcribed from this board's vendor VIA definition. They are not
  85. // current QMK master, which has 45 entries under other names and other IDs,
  86. // so the list must not be repaired against upstream. Each of the 46 IDs was
  87. // taken by the keyboard when written; the board also takes ID 46, which no
  88. // name in this list covers.
  89. // - The 7 logo and 7 side names are this board's vendor VIA definition, whose
  90. // dropdowns read "fixed wave" and "breathe". The tool spells them
  91. // fixed_wave and breathing and accepts the vendor's spellings as aliases.
  92. // The keyboard takes these IDs only from 1 to 6: ID 0 means "lighting off"
  93. // and leaves the mode register untouched.
  94. // - The 47th backlight name, "freeze" at ID 46, is the tool's own. The
  95. // vendor's JSON stops at 45 and names no such behaviour; the name and the
  96. // behaviour behind it were measured on an Impact 80 and are recorded in
  97. // impact80.go. It is a tool name, not a name the board uses.
  98. //
  99. // Both lists come from the VIA JSON the vendor's Driver & Firmware page links at
  100. // https://wiki.wobkey.com/en/Products/PMOKEY-Impact-80/Driver-Firmware
  101. // (the file itself: https://drive.wobkey.com/f/d/6BtO/Impact_80.JSON). That page
  102. // is also where the firmware variants are: the proprietary one offers the
  103. // advanced lighting but disables VIA, so a board running it has no Raw HID
  104. // interface and is invisible here rather than unsupported.
  105. // - The brightness and speed transforms documented in README.md were measured
  106. // on the unit, not read from anywhere.
  107. func CatalogFor(vendorID, productID uint16) (*Catalog, bool) {
  108. if vendorID != 0x36B0 || productID != 0x309F {
  109. return nil, false
  110. }
  111. return &Catalog{
  112. board: "impact80",
  113. names: map[via.Channel][]Effect{
  114. via.ChannelRgblight: dense(impact80LogoEffects[:]),
  115. via.ChannelRgbMatrix: dense(impact80BacklightEffects[:]),
  116. via.ChannelAudio: dense(impact80SideEffects[:]),
  117. },
  118. aliases: defaultAliases,
  119. defaults: map[via.Channel]uint8{
  120. via.ChannelRgblight: 4,
  121. via.ChannelRgbMatrix: 5,
  122. via.ChannelAudio: 4,
  123. },
  124. }, true
  125. }
  126. // Name returns the catalog's board name, which `effect --list` reports.
  127. func (c *Catalog) Name() string {
  128. if c == nil {
  129. return ""
  130. }
  131. return c.board
  132. }
  133. // Effects returns a channel's named effects, or nil when the catalog says
  134. // nothing about it.
  135. func (c *Catalog) Effects(ch via.Channel) []Effect {
  136. if c == nil {
  137. return nil
  138. }
  139. return c.names[ch]
  140. }
  141. // Names returns the effect names of a channel in effect-ID order, or nil when
  142. // the catalog says nothing about it.
  143. func (c *Catalog) Names(ch via.Channel) []string {
  144. effects := c.Effects(ch)
  145. if effects == nil {
  146. return nil
  147. }
  148. names := make([]string, 0, len(effects))
  149. for _, e := range effects {
  150. names = append(names, e.Name)
  151. }
  152. return names
  153. }
  154. // EffectName returns the name of an effect ID, or "unknown" when the catalog has
  155. // no entry for it.
  156. func (c *Catalog) EffectName(ch via.Channel, id uint8) string {
  157. if c == nil {
  158. return unknownEffectName
  159. }
  160. for _, e := range c.names[ch] {
  161. if e.ID == id {
  162. return e.Name
  163. }
  164. }
  165. return unknownEffectName
  166. }
  167. // EffectID resolves an effect name on a channel, following one level of alias.
  168. func (c *Catalog) EffectID(ch via.Channel, name string) (uint8, bool) {
  169. if c == nil {
  170. return 0, false
  171. }
  172. if id, found := c.byName(ch, name); found {
  173. return id, true
  174. }
  175. if canonical, ok := c.aliases[ch][name]; ok {
  176. if id, found := c.EffectID(ch, canonical); found {
  177. return id, true
  178. }
  179. }
  180. // A definition may carry the other spelling of the same pair: the tool's
  181. // alias table says "breathe" for "breathing", and a manufacturer's file
  182. // may use either. So a name that is the target of an alias resolves to
  183. // that alias where the channel has it.
  184. for alias, canonical := range c.aliases[ch] {
  185. if canonical != name {
  186. continue
  187. }
  188. if id, found := c.byName(ch, alias); found {
  189. return id, true
  190. }
  191. }
  192. return 0, false
  193. }
  194. // byName finds an effect by its exact name, without following an alias, then by
  195. // the same name with spaces written as underscores. A definition carries display
  196. // spellings where the tool carries identifiers, and the difference is
  197. // whitespace, not a different effect: "fixed wave" and "fixed_wave" name the
  198. // same thing, and the tool's documented spelling has to keep working while a
  199. // definition file is present.
  200. func (c *Catalog) byName(ch via.Channel, name string) (uint8, bool) {
  201. for _, e := range c.names[ch] {
  202. if e.Name == name {
  203. return e.ID, true
  204. }
  205. }
  206. spaced := strings.ReplaceAll(name, "_", " ")
  207. if spaced != name {
  208. for _, e := range c.names[ch] {
  209. if e.Name == spaced {
  210. return e.ID, true
  211. }
  212. }
  213. }
  214. return 0, false
  215. }
  216. // DefaultEffect returns the effect ID enable writes when turning a channel on.
  217. func (c *Catalog) DefaultEffect(ch via.Channel) (uint8, bool) {
  218. if c == nil {
  219. return 0, false
  220. }
  221. id, ok := c.defaults[ch]
  222. return id, ok
  223. }
  224. // ResolveEffect turns an effect name into one target per channel that supports
  225. // it, plus the subsystem names of those that do not. A skip is an error rather
  226. // than a warning when the caller asked for one channel explicitly, because a
  227. // command that silently did nothing looks like a command that worked. That is
  228. // the caller's knowledge to pass: a board with a single lighting channel is not
  229. // an explicit request for it.
  230. func ResolveEffect(catalog *Catalog, name string, channels []via.Channel, explicit bool) ([]EffectTarget, []string, error) {
  231. if catalog == nil {
  232. return nil, nil, fmt.Errorf("no effect catalog for this keyboard; set an effect by number with `mode <index>`")
  233. }
  234. // The compatibility spelling every catalog shares, so a caller cannot
  235. // resolve "static" differently from another.
  236. if name == "static" {
  237. name = "solid"
  238. }
  239. // Whether the board has the name at all is a different question from
  240. // whether the channels asked for can do it: naming a real effect on the
  241. // wrong channel is a different mistake, with a different fix, and gets its
  242. // own message.
  243. knownAnywhere := false
  244. for ch := range catalog.names {
  245. if _, ok := catalog.EffectID(ch, name); ok {
  246. knownAnywhere = true
  247. break
  248. }
  249. }
  250. if !knownAnywhere {
  251. return nil, nil, fmt.Errorf("unknown effect: %s", name)
  252. }
  253. var targets []EffectTarget
  254. var skipped []string
  255. for _, ch := range channels {
  256. id, ok := catalog.EffectID(ch, name)
  257. if !ok {
  258. if explicit {
  259. return nil, nil, fmt.Errorf("effect %s is not supported on %s", name, ch.Subsystem())
  260. }
  261. skipped = append(skipped, ch.Subsystem())
  262. continue
  263. }
  264. targets = append(targets, EffectTarget{Channel: ch, ID: id})
  265. }
  266. return targets, skipped, nil
  267. }