package main import ( "context" "fmt" "math" "strconv" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) // Tools is the module's tools, over one machine and its switcher. func Tools(m *Machine, sw *Switcher) []stdio.Tool { ctx := context.Background return []stdio.Tool{ { Name: "zephyrus_brightness", Description: "Read or set the internal panel's and the keyboard's backlight. With no argument, reads both. " + "panel is a percentage (40) or a step (+5, -10), never below 1 %; keyboard is off, low, med, high, 0-3, + or -.", Input: map[string]any{ "panel": map[string]any{"type": "string", "description": "percentage or step, e.g. 40, +5, -10"}, "keyboard": map[string]any{"type": "string", "description": "off, low, med, high, 0-3, + or -"}, }, Run: func(args map[string]any) (any, error) { out := map[string]any{} if p := str(args, "panel"); p != "" { got, err := m.SetPanel(ctx(), p) if err != nil { return nil, err } out["panel"] = got } else if got, err := m.PanelBrightness(); err == nil { out["panel"] = got } else { out["panel_error"] = err.Error() } if k := str(args, "keyboard"); k != "" { got, err := m.SetKeyboard(ctx(), k) if err != nil { return nil, err } out["keyboard"] = got } else if got, err := m.KeyboardBrightness(); err == nil { out["keyboard"] = got } else { out["keyboard_error"] = err.Error() } return out, nil }, }, { Name: "zephyrus_battery", Description: "The battery: charge, energy, health (full against design), cycles, the charge limit, the power it gives or takes, and time left when discharging.", Run: func(map[string]any) (any, error) { return map[string]any{"source": PowerSource(m.Supplies()), "batteries": orEmpty(m.Batteries())}, nil }, }, { Name: "zephyrus_charge_limit", Description: fmt.Sprintf("Read or set the battery charge limit through asusd. limit is 20-100; oneshot charges to full once "+ "and goes back to the limit. The module asserts %d %% again when its process next starts.", ChargeLimitPercent), Input: map[string]any{ "limit": map[string]any{"type": "integer", "description": "20-100"}, "oneshot": map[string]any{"type": "boolean", "description": "charge to full once, keeping the limit"}, }, Run: func(args map[string]any) (any, error) { return ChargeLimitTool(ctx(), m, args) }, }, { Name: "zephyrus_gpu_mode", Description: "Read or set the hybrid GPU's mode through supergfxd: Integrated, Hybrid or AsusMuxDgpu as the machine supports. " + "Answers the mode, the discrete GPU's power state, any pending mode and the action it waits for (a logout, a reboot), " + "and whether asusd will switch it again on the next change of power source.", Input: map[string]any{ "mode": map[string]any{"type": "string", "description": "a supported mode, e.g. Integrated or Hybrid"}, }, Run: func(args map[string]any) (any, error) { return GPUModeTool(ctx(), m, args) }, }, { Name: "zephyrus_profile", Description: "Read or set the platform profile (Quiet, Balanced, Performance) through asusd. A profile set here is held " + fmt.Sprintf("for hold_minutes (default %d, 0 for none) before the module's switcher may move it; a change of power source ends the hold.", int(DefaultHold.Minutes())), Input: map[string]any{ "profile": map[string]any{"type": "string", "enum": Profiles}, "hold_minutes": map[string]any{"type": "integer", "description": "how long the switcher leaves it (default 60, at most 1440)"}, }, Run: func(args map[string]any) (any, error) { return ProfileTool(ctx(), m, sw, args) }, }, { Name: "zephyrus_thermals", Description: "Every temperature and fan the hardware reports (°C, RPM), the hottest, the platform profile, and the discrete GPU's temperature when it is awake (it is not woken to be read).", Run: func(map[string]any) (any, error) { return m.Thermals(ctx()), nil }, }, { Name: "zephyrus_power_draw", Description: "What the machine draws: the battery's flow in watts, the APU's package power, the discrete GPU's draw when awake, and the power source with the reason it was decided.", Run: func(map[string]any) (any, error) { return m.PowerDraw(ctx()), nil }, }, { Name: "zephyrus_profile_policy", Description: "What the module's profile switcher would choose now and why: the power source, recent CPU load against the thresholds, " + "the decision, the profile in force, any hold, what woke it, and what it asserted in asusd at start.", Run: func(map[string]any) (any, error) { return PolicyTool(ctx(), m, sw), nil }, }, { Name: "zephyrus_fan_curves", Description: "The fan curves asusd holds for each profile (or one profile): per fan, eight points of temperature and duty.", Input: map[string]any{ "profile": map[string]any{"type": "string", "enum": Profiles}, }, Run: func(args map[string]any) (any, error) { return FanCurvesTool(ctx(), m, args) }, }, { Name: "zephyrus_keys", Description: "Every custom key on the laptop: each triggerhappy trigger (the vendor keys that reach no X client), the module's own i3 lines " + "(keys the firmware sends as ordinary presses, and what starts with the session), and the keys the firmware handles itself — " + "what each runs and the file it is defined in. Warns when one key is bound in two trigger files, which fires it twice.", Run: func(map[string]any) (any, error) { return m.Keys(), nil }, }, { Name: "zephyrus_check", Description: "Check what this module expects of the machine: the model, the vendor packages and daemons, the NVIDIA options in force, " + "suspend and resume, the charge limit, one authority each over the profile and the GPU mode, and the predecessor's leftovers. Says what it did not check.", Run: func(map[string]any) (any, error) { return m.Check(ctx(), sw), nil }, }, } } // ChargeLimitTool reads or sets the limit. func ChargeLimitTool(ctx context.Context, m *Machine, args map[string]any) (any, error) { out := map[string]any{"module_limit_percent": ChargeLimitPercent} if v, given := args["limit"]; given && v != nil { n, err := whole(v, "limit") if err != nil { return nil, err } if n < 20 || n > 100 { return nil, fmt.Errorf("limit %d is outside 20-100", n) } if _, err := m.Run(ctx, "asusctl", "battery", "limit", strconv.Itoa(n)); err != nil { return nil, vendor("asusctl", err) } out["set"] = n } if b, _ := args["oneshot"].(bool); b { if _, err := m.Run(ctx, "asusctl", "battery", "oneshot"); err != nil { return nil, vendor("asusctl", err) } out["oneshot"] = "charging to full once; the limit returns after" } if n, err := m.ChargeLimit(ctx); err == nil { out["asusd_limit_percent"] = n } else { out["asusd_error"] = err.Error() } for _, b := range m.Batteries() { if b.LimitPercent != nil { out["kernel_limit_percent"] = *b.LimitPercent } } return out, nil } // GPUModeTool reads or sets the GPU mode. func GPUModeTool(ctx context.Context, m *Machine, args map[string]any) (any, error) { g, err := m.GPU(ctx) if err != nil { return nil, err } out := map[string]any{} if want := str(args, "mode"); want != "" { mode := "" for _, s := range g.Supported { if strings.EqualFold(s, want) { mode = s } } if mode == "" { return nil, fmt.Errorf("mode %q is not one this machine supports (%s)", want, strings.Join(g.Supported, ", ")) } said, err := m.Run(ctx, "supergfxctl", "-m", mode) if err != nil { return nil, vendor("supergfxctl", err) } out["requested"] = mode if s := strings.TrimSpace(said); s != "" { out["supergfxctl_said"] = s } if g, err = m.GPU(ctx); err != nil { return nil, err } } out["gpu"] = g if c := m.Asusd(); c != nil && (c.ACCommand != "" || c.BatteryCommand != "") { out["asusd_switches_it"] = map[string]string{"on_ac": c.ACCommand, "on_battery": c.BatteryCommand, "note": "asusd runs these on every change of power source, so a mode set here lasts until the next one"} } return out, nil } // ProfileTool reads or sets the profile. func ProfileTool(ctx context.Context, m *Machine, sw *Switcher, args map[string]any) (any, error) { out := map[string]any{} if want := str(args, "profile"); want != "" { p, err := canonicalProfile(want) if err != nil { return nil, err } hold := DefaultHold if v, given := args["hold_minutes"]; given && v != nil { n, err := whole(v, "hold_minutes") if err != nil { return nil, err } if n < 0 { return nil, fmt.Errorf("hold_minutes must not be negative") } hold = time.Duration(min(n, 1440)) * time.Minute } if err := m.SetProfile(ctx, p); err != nil { return nil, err } out["set"] = p if sw != nil { if until := sw.Hold(p, hold); !until.IsZero() { out["held_until"] = until } } } state, err := m.Profile(ctx) if err != nil { return nil, err } out["profile"] = state return out, nil } // PolicyTool reports the switcher and, independently of it, what the policy says now. func PolicyTool(ctx context.Context, m *Machine, sw *Switcher) any { out := map[string]any{ "thresholds": map[string]any{ "on_battery": ProfileOnBattery, "on_ac": ProfileOnAC, "under_load": ProfileUnderLoad, "cpu_high_percent": CPUHighPercent, "cpu_low_percent": CPULowPercent, "sample_every": SampleEvery.String(), "sustain_samples": SustainSamples, "relax_samples": RelaxSamples, "set_by": "constants until settings exist (novox/hq issue 168)", }, } if sw != nil { out["switcher"] = sw.Report() } else { var p Policy out["decision_now"] = p.Decide(PowerSource(m.Supplies())) } if state, err := m.Profile(ctx); err == nil { out["in_force"] = state } else { out["in_force_error"] = err.Error() } if pid, ok := m.predecessorProcess("auto-profile"); ok { out["second_switcher"] = fmt.Sprintf("the predecessor's auto-profile still runs (pid %d) and overrides this every five seconds", pid) } return out } // FanCurvesTool reads asusd's fan curves. func FanCurvesTool(ctx context.Context, m *Machine, args map[string]any) (any, error) { profiles := Profiles if want := str(args, "profile"); want != "" { p, err := canonicalProfile(want) if err != nil { return nil, err } profiles = []string{p} } out := map[string]any{} for _, p := range profiles { said, err := m.Run(ctx, "asusctl", "fan-curve", "--mod-profile", strings.ToLower(p)) if err != nil { return nil, vendor("asusctl", err) } out[p] = orEmpty(ParseFanCurves(said)) } return out, nil } func str(args map[string]any, key string) string { s, _ := args[key].(string) return strings.TrimSpace(s) } // whole is an integer argument given as a JSON number or a numeric string. func whole(v any, key string) (int, error) { switch n := v.(type) { case float64: if n != math.Trunc(n) { return 0, fmt.Errorf("%s must be a whole number, not %v", key, n) } return int(n), nil case string: i, err := strconv.Atoi(strings.TrimSpace(n)) if err != nil { return 0, fmt.Errorf("%s must be a whole number, not %q", key, n) } return i, nil } return 0, fmt.Errorf("%s must be a whole number", key) } func orEmpty[T any](s []T) []T { if s == nil { return []T{} } return s }