|
|
@@ -5,6 +5,7 @@ import (
|
|
|
"fmt"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
+ "strconv"
|
|
|
"strings"
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
@@ -13,16 +14,41 @@ import (
|
|
|
"netdome.biz/paul/qmk-rgb/internal/via"
|
|
|
)
|
|
|
|
|
|
-// effectRangeProbe is what `keyboard definitions generate` needs from a
|
|
|
-// keyboard: the highest effect ID each channel takes, and a way to close it.
|
|
|
+// effectRangeProbe is what `keyboard definitions generate` needs from a keyboard:
|
|
|
+// the effect IDs each channel takes, a way to close it, and the two Vial calls
|
|
|
+// that may answer the question without writing to it.
|
|
|
+//
|
|
|
// It is deliberately not a method on rgbProtocol, because that interface is what
|
|
|
// every other command's test stub implements and a probe is not something
|
|
|
// brightness and colour have to know about.
|
|
|
type effectRangeProbe interface {
|
|
|
EffectTop(via.Channel) (int, error)
|
|
|
+ VialVersion() (uint32, bool, error)
|
|
|
+ VialEffectIDs() ([]uint16, error)
|
|
|
Close() error
|
|
|
}
|
|
|
|
|
|
+// slotsFrom is where a channel's effect slots came from. It is reported per
|
|
|
+// channel because the two answers are not the same kind of thing: one is the
|
|
|
+// firmware listing what it compiled in, the other is the firmware's answer to a
|
|
|
+// value written above the top.
|
|
|
+type slotsFrom string
|
|
|
+
|
|
|
+const (
|
|
|
+ // slotsMeasured means the range was found by writing above the top and
|
|
|
+ // reading back what the firmware clamped to.
|
|
|
+ slotsMeasured slotsFrom = "measured by clamping"
|
|
|
+ // slotsReported means the firmware listed them itself, which is Vial's
|
|
|
+ // vialrgb_get_supported and nothing else.
|
|
|
+ slotsReported slotsFrom = "reported by the firmware"
|
|
|
+)
|
|
|
+
|
|
|
+// experimentalVial marks the Vial path. It is written from Vial's source and
|
|
|
+// tested against a scripted keyboard, and nobody has run it against a Vial
|
|
|
+// keyboard, so every command that uses it says so rather than presenting an
|
|
|
+// unverified answer as a measurement. Remove the word when a real one has.
|
|
|
+const experimentalVial = "experimental"
|
|
|
+
|
|
|
// generateForce replaces a definition that is already stored. It is the same
|
|
|
// rule `keyboard fetch` follows, for the same reason: the file may be one the
|
|
|
// user has written themselves, and this command is the one they would have run
|
|
|
@@ -53,7 +79,8 @@ func NewKeyboardDefinitionsGenerateCmd() *cobra.Command {
|
|
|
cmd := &cobra.Command{
|
|
|
Use: "generate",
|
|
|
Short: "Write a definition for the connected keyboard, with the effect names left to you",
|
|
|
- Long: "Ask the keyboard how many effect IDs each of its lighting channels takes, then\n" +
|
|
|
+ Long: experimentalVial + ": the Vial path below has not been run against a Vial keyboard.\n\n" +
|
|
|
+ "Ask the keyboard how many effect IDs each of its lighting channels takes, then\n" +
|
|
|
"write a definition file with one unnamed option per ID, and a note beside it\n" +
|
|
|
"saying which spellings other keyboards' definitions use for the same numbers.\n\n" +
|
|
|
"The names are yours to write: open the file and put one in each options entry,\n" +
|
|
|
@@ -62,7 +89,10 @@ func NewKeyboardDefinitionsGenerateCmd() *cobra.Command {
|
|
|
"manufacturer's, and a wrong name is worse than none: the tool reports an effect\n" +
|
|
|
"as unknown and set by number until you fill it in.\n\n" +
|
|
|
"A definition already stored for this keyboard is not replaced, because it is\n" +
|
|
|
- "probably the one you wrote. Edit it, or pass --force.",
|
|
|
+ "probably the one you wrote. Edit it, or pass --force.\n\n" +
|
|
|
+ "A keyboard running Vial firmware is asked for its rgb_matrix effect IDs instead of\n" +
|
|
|
+ "being written to, which is the better answer, and it numbers them in its own\n" +
|
|
|
+ "namespace rather than QMK's. Every channel says where its slots came from.",
|
|
|
Args: cobra.NoArgs,
|
|
|
RunE: runKeyboardDefinitionsGenerate,
|
|
|
}
|
|
|
@@ -122,18 +152,34 @@ func runKeyboardDefinitionsGenerate(cmd *cobra.Command, args []string) error {
|
|
|
ProductID: fmt.Sprintf("0x%04X", target.Device.ProductID),
|
|
|
}
|
|
|
notes := &strings.Builder{}
|
|
|
- tops := map[via.Channel]int{}
|
|
|
+
|
|
|
+ // Vial can list the rgb_matrix effect IDs without the keyboard being written
|
|
|
+ // to. It is asked first because its answer is the better one, and only where
|
|
|
+ // the firmware serves it: VIALRGB_ENABLE is a build option, and a Vial
|
|
|
+ // keyboard without it answers nothing.
|
|
|
+ version, isVial, err := probe.VialVersion()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if isVial {
|
|
|
+ fmt.Fprintf(cmd.OutOrStdout(), "This keyboard answers as Vial firmware, protocol version %d (%s: not run against real hardware)\n",
|
|
|
+ version, experimentalVial)
|
|
|
+ }
|
|
|
+
|
|
|
+ sources := map[via.Channel]slotsFrom{}
|
|
|
+ slots := map[via.Channel][]uint16{}
|
|
|
|
|
|
for _, ch := range channels {
|
|
|
- top, err := probe.EffectTop(ch)
|
|
|
+ ids, from, err := slotsFor(probe, ch, isVial)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- if menu := scaffoldMenu(ch, top); len(menu.Content) > 0 {
|
|
|
+ if menu := scaffoldMenu(ch, ids); len(menu.Content) > 0 {
|
|
|
doc.Menus = append(doc.Menus, menu)
|
|
|
}
|
|
|
- tops[ch] = top
|
|
|
- writeCandidates(notes, ch, top)
|
|
|
+ sources[ch] = from
|
|
|
+ slots[ch] = ids
|
|
|
+ writeCandidates(notes, ch, ids, from)
|
|
|
}
|
|
|
|
|
|
dir, err := ensureDefinitionsDir()
|
|
|
@@ -170,10 +216,12 @@ func runKeyboardDefinitionsGenerate(cmd *cobra.Command, args []string) error {
|
|
|
verb, doc.Name, doc.VendorID, doc.ProductID, describeDataDir(path))
|
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Every effect is an unnamed option. Open the file and write a name into each one:\n")
|
|
|
for _, ch := range channels {
|
|
|
- if _, ok := intrgb.EffectValueKey(ch); !ok {
|
|
|
+ ids, ok := slots[ch]
|
|
|
+ if !ok {
|
|
|
continue
|
|
|
}
|
|
|
- fmt.Fprintf(cmd.OutOrStdout(), " %-10s %d slots, IDs 0 to %d\n", ch.Subsystem(), tops[ch]+1, tops[ch])
|
|
|
+ fmt.Fprintf(cmd.OutOrStdout(), " %-10s %d slots, IDs %s (%s)\n",
|
|
|
+ ch.Subsystem(), len(ids), describeSlotRange(ids), sources[ch])
|
|
|
}
|
|
|
fmt.Fprintf(cmd.OutOrStdout(), "\nWhat other keyboards call the same numbers is in %s\n", describeDataDir(notePath))
|
|
|
return nil
|
|
|
@@ -219,11 +267,70 @@ type scaffoldEntry struct {
|
|
|
Options any `json:"options,omitempty"`
|
|
|
}
|
|
|
|
|
|
+// slotsFor returns the effect IDs a channel has and where that came from.
|
|
|
+//
|
|
|
+// Vial is asked only for rgb_matrix, because VialRGB is an rgb_matrix extension:
|
|
|
+// there is no Vial list for backlight, rgblight, audio or led_matrix, and those
|
|
|
+// keep the clamping answer. A Vial keyboard without VIALRGB_ENABLE answers
|
|
|
+// nothing, which is an error rather than an empty list, and it falls back to the
|
|
|
+// measurement because a board that cannot list its effects can still be asked.
|
|
|
+func slotsFor(probe effectRangeProbe, ch via.Channel, isVial bool) ([]uint16, slotsFrom, error) {
|
|
|
+ if isVial && ch == via.ChannelRgbMatrix {
|
|
|
+ ids, err := probe.VialEffectIDs()
|
|
|
+ if err == nil && len(ids) > 0 {
|
|
|
+ return ids, slotsReported, nil
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ fmt.Fprintf(os.Stderr, "vialrgb effect list on %s unavailable (%v), falling back to the clamp\n", ch.Subsystem(), err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ top, err := probe.EffectTop(ch)
|
|
|
+ if err != nil {
|
|
|
+ return nil, "", err
|
|
|
+ }
|
|
|
+ ids := make([]uint16, 0, top+1)
|
|
|
+ for id := 0; id <= top; id++ {
|
|
|
+ ids = append(ids, uint16(id))
|
|
|
+ }
|
|
|
+ return ids, slotsMeasured, nil
|
|
|
+}
|
|
|
+
|
|
|
+// describeSlotRange writes a list of IDs as a range when it is one, because
|
|
|
+// "0 to 45" says in six characters what a list of forty-six does not.
|
|
|
+func describeSlotRange(ids []uint16) string {
|
|
|
+ if len(ids) == 0 {
|
|
|
+ return "none"
|
|
|
+ }
|
|
|
+ runs := true
|
|
|
+ for i := 1; i < len(ids); i++ {
|
|
|
+ if ids[i] != ids[i-1]+1 {
|
|
|
+ runs = false
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if runs {
|
|
|
+ return fmt.Sprintf("%d to %d", ids[0], ids[len(ids)-1])
|
|
|
+ }
|
|
|
+ shown := ids
|
|
|
+ if len(shown) > 8 {
|
|
|
+ shown = append(append([]uint16{}, ids[:8]...), 0xFFFF)
|
|
|
+ }
|
|
|
+ parts := make([]string, 0, len(shown))
|
|
|
+ for _, id := range shown {
|
|
|
+ if id == 0xFFFF {
|
|
|
+ parts = append(parts, "...")
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ parts = append(parts, strconv.Itoa(int(id)))
|
|
|
+ }
|
|
|
+ return strings.Join(parts, ", ")
|
|
|
+}
|
|
|
+
|
|
|
// scaffoldMenu is one lighting channel as a VIA menu: the three controls the
|
|
|
// value IDs name, with the effect list holding one unnamed option per slot. The
|
|
|
// name is the channel's own, so the file names it the way the tool does rather
|
|
|
// than inventing a label — a board the user fills in can rename it.
|
|
|
-func scaffoldMenu(ch via.Channel, top int) scaffoldMenuT {
|
|
|
+func scaffoldMenu(ch via.Channel, ids []uint16) scaffoldMenuT {
|
|
|
valueKey, ok := intrgb.EffectValueKey(ch)
|
|
|
if !ok {
|
|
|
// A channel with no registered value key has no list this tool reads, so
|
|
|
@@ -237,8 +344,8 @@ func scaffoldMenu(ch via.Channel, top int) scaffoldMenuT {
|
|
|
// `id_qmk_rgb_matrix_brightness` rather than one the parser will not find.
|
|
|
prefix := strings.TrimSuffix(valueKey, "_effect")
|
|
|
|
|
|
- options := make([][]any, 0, top+1)
|
|
|
- for id := 0; id <= top; id++ {
|
|
|
+ options := make([][]any, 0, len(ids))
|
|
|
+ for _, id := range ids {
|
|
|
// The empty name is the point: it states a slot without naming it, and the
|
|
|
// parser skips it, so the channel reports no effects until a name is
|
|
|
// written here.
|
|
|
@@ -273,15 +380,27 @@ func scaffoldMenu(ch via.Channel, top int) scaffoldMenuT {
|
|
|
// writeCandidates records what other keyboards' definitions were seen to call
|
|
|
// each ID, with the counts and the manufacturer, because a name without those is
|
|
|
// a guess wearing a number.
|
|
|
-func writeCandidates(out *strings.Builder, ch via.Channel, top int) {
|
|
|
+func writeCandidates(out *strings.Builder, ch via.Channel, ids []uint16, from slotsFrom) {
|
|
|
valueKey, ok := intrgb.EffectValueKey(ch)
|
|
|
if !ok {
|
|
|
return
|
|
|
}
|
|
|
- fmt.Fprintf(out, "\n%s (channel %d), IDs 0 to %d\n", ch.Subsystem(), ch, top)
|
|
|
+ fmt.Fprintf(out, "\n%s (channel %d), %d slots, IDs %s (%s)\n",
|
|
|
+ ch.Subsystem(), ch, len(ids), describeSlotRange(ids), from)
|
|
|
+
|
|
|
+ if from == slotsReported {
|
|
|
+ // The firmware that listed these numbers numbers them in its own
|
|
|
+ // namespace, and the measurement below is QMK's. Saying "here is what
|
|
|
+ // other boards call ID 7" next to a slot that is not QMK's ID 7 is the one
|
|
|
+ // thing this note must not do.
|
|
|
+ fmt.Fprintf(out, " These IDs are the firmware's own numbering, not QMK's: the list the firmware sends\n")
|
|
|
+ fmt.Fprintf(out, " is VIALRGB_EFFECT_* and the translation to QMK's stays inside the firmware. The\n")
|
|
|
+ fmt.Fprintf(out, " spellings other keyboards use are indexed by QMK's numbers, so they are not listed here.\n")
|
|
|
+ return
|
|
|
+ }
|
|
|
seen := 0
|
|
|
- for id := 0; id <= top; id++ {
|
|
|
- obs, ok := spotted.Candidates(valueKey, id)
|
|
|
+ for _, id := range ids {
|
|
|
+ obs, ok := spotted.Candidates(valueKey, int(id))
|
|
|
if !ok {
|
|
|
continue
|
|
|
}
|