package main import ( "context" "fmt" "regexp" "strconv" "strings" ) // The vendor daemons are reached through their own command-line clients, which speak to them on the // system bus. Their bus policy admits the `users` and `wheel` groups, so none of this needs root. // Profiles are the platform profiles asusd offers on this model, in its spelling. var Profiles = []string{"Quiet", "Balanced", "Performance"} // canonicalProfile accepts any case and answers asusd's spelling, or an error naming the choices. func canonicalProfile(s string) (string, error) { for _, p := range Profiles { if strings.EqualFold(strings.TrimSpace(s), p) { return p, nil } } return "", fmt.Errorf("profile %q is not one of %s", s, strings.Join(Profiles, ", ")) } // ProfileState is what asusd says about the platform profile. type ProfileState struct { Active string `json:"active"` OnAC string `json:"on_ac,omitempty"` Battery string `json:"on_battery,omitempty"` Platform string `json:"platform_profile,omitempty"` Choices string `json:"platform_profile_choices,omitempty"` } var ( activeProfile = regexp.MustCompile(`(?m)^Active profile:\s*(\S+)`) acProfile = regexp.MustCompile(`(?m)^AC profile\s+(\S+)`) batteryProfile = regexp.MustCompile(`(?m)^Battery profile\s+(\S+)`) ) // ParseProfileGet reads `asusctl profile get`. func ParseProfileGet(out string) (ProfileState, error) { var p ProfileState if m := activeProfile.FindStringSubmatch(out); m != nil { p.Active = m[1] } else { return p, fmt.Errorf("asusctl profile get said no active profile: %q", strings.TrimSpace(out)) } if m := acProfile.FindStringSubmatch(out); m != nil { p.OnAC = m[1] } if m := batteryProfile.FindStringSubmatch(out); m != nil { p.Battery = m[1] } return p, nil } // Profile reads the platform profile from asusd and the kernel. func (m *Machine) Profile(ctx context.Context) (ProfileState, error) { out, err := m.Run(ctx, "asusctl", "profile", "get") if err != nil { return ProfileState{}, vendor("asusctl", err) } p, err := ParseProfileGet(out) if err != nil { return p, err } p.Platform = m.read("/sys/firmware/acpi/platform_profile") p.Choices = m.read("/sys/firmware/acpi/platform_profile_choices") return p, nil } // SetProfile has asusd switch the active profile. func (m *Machine) SetProfile(ctx context.Context, profile string) error { _, err := m.Run(ctx, "asusctl", "profile", "set", profile) return vendor("asusctl", err) } var chargeLimit = regexp.MustCompile(`charge limit:\s*(\d+)\s*%`) // ChargeLimit is the battery's charge limit as asusd reports it. func (m *Machine) ChargeLimit(ctx context.Context) (int, error) { out, err := m.Run(ctx, "asusctl", "battery", "info") if err != nil { return 0, vendor("asusctl", err) } g := chargeLimit.FindStringSubmatch(out) if g == nil { return 0, fmt.Errorf("asusctl battery info said no limit: %q", strings.TrimSpace(out)) } n, _ := strconv.Atoi(g[1]) return n, nil } // Keyboard backlight levels in asusd's spelling, index = the kernel's brightness value. var KeyboardLevels = []string{"off", "low", "med", "high"} var ledLevel = regexp.MustCompile(`(?i)brightness:\s*(off|low|med|high)`) // ParseLeds reads `asusctl leds get`. func ParseLeds(out string) (string, error) { g := ledLevel.FindStringSubmatch(out) if g == nil { return "", fmt.Errorf("asusctl leds get said no level: %q", strings.TrimSpace(out)) } return strings.ToLower(g[1]), nil } // FanCurve is one fan's curve in one profile: eight points of temperature (°C) and duty (0-255). type FanCurve struct { Fan string `json:"fan"` Enabled bool `json:"enabled"` Temp []int `json:"temp_c"` PWM []int `json:"pwm"` } var ( fanBlock = regexp.MustCompile(`(?s)fan:\s*(\w+),\s*pwm:\s*\(([^)]*)\),\s*temp:\s*\(([^)]*)\),\s*enabled:\s*(true|false)`) ) // ParseFanCurves reads `asusctl fan-curve --mod-profile

`. func ParseFanCurves(out string) []FanCurve { var curves []FanCurve for _, g := range fanBlock.FindAllStringSubmatch(out, -1) { curves = append(curves, FanCurve{Fan: g[1], PWM: ints(g[2]), Temp: ints(g[3]), Enabled: g[4] == "true"}) } return curves } func ints(list string) []int { var out []int for _, f := range strings.Split(list, ",") { if n, err := strconv.Atoi(strings.TrimSpace(f)); err == nil { out = append(out, n) } } return out } // GPUState is what supergfxd says about the hybrid GPU. type GPUState struct { Mode string `json:"mode"` Supported []string `json:"supported"` Power string `json:"dgpu_power,omitempty"` PendingAction string `json:"pending_action,omitempty"` PendingMode string `json:"pending_mode,omitempty"` Vendor string `json:"dgpu_vendor,omitempty"` } // ParseSupported reads `supergfxctl -s`: `[Integrated, Hybrid, AsusMuxDgpu]`. func ParseSupported(out string) []string { out = strings.Trim(strings.TrimSpace(out), "[]") var modes []string for _, f := range strings.Split(out, ",") { if f = strings.TrimSpace(f); f != "" { modes = append(modes, f) } } return modes } // GPU reads supergfxd. func (m *Machine) GPU(ctx context.Context) (GPUState, error) { var g GPUState mode, err := m.Run(ctx, "supergfxctl", "-g") if err != nil { return g, vendor("supergfxctl", err) } g.Mode = strings.TrimSpace(mode) if s, err := m.Run(ctx, "supergfxctl", "-s"); err == nil { g.Supported = ParseSupported(s) } if s, err := m.Run(ctx, "supergfxctl", "-S"); err == nil { g.Power = strings.TrimSpace(s) } if s, err := m.Run(ctx, "supergfxctl", "-p"); err == nil { g.PendingAction = strings.TrimSpace(s) } if s, err := m.Run(ctx, "supergfxctl", "-P"); err == nil { g.PendingMode = strings.TrimSpace(s) } if s, err := m.Run(ctx, "supergfxctl", "-V"); err == nil { g.Vendor = strings.TrimSpace(s) } return g, nil } // vendor names a vendor client that is not installed, rather than passing on "executable file not // found". asusctl is in the distribution's repositories; supergfxctl is not, and the module keeps it as // it was found until the mesh can build packages from the user repository (research 027, question 1). func vendor(name string, err error) error { if err == nil { return nil } if notInstalled(err) { switch name { case "supergfxctl": return fmt.Errorf("supergfxctl is not installed: it is not in the distribution's repositories, " + "and this module keeps the copy it finds rather than install one (novox/hq research 027, question 1)") default: return fmt.Errorf("%s is not installed; the module's package resource installs it", name) } } return err } // AsusdConfig is the few settings of asusd's own file that decide what this module's code does. The // file is asusd's: it rewrites it whenever a setting changes, so the module reads it and never writes // it. type AsusdConfig struct { ChargeLimit *int `json:"charge_control_end_threshold,omitempty"` ProfileOnAC string `json:"platform_profile_on_ac,omitempty"` ProfileOnBattery string `json:"platform_profile_on_battery,omitempty"` ChangesProfileOnAC *bool `json:"change_platform_profile_on_ac,omitempty"` ChangesProfileOnBatt *bool `json:"change_platform_profile_on_battery,omitempty"` ACCommand string `json:"ac_command,omitempty"` BatteryCommand string `json:"bat_command,omitempty"` DisablesPowerdOnBatt *bool `json:"disable_nvidia_powerd_on_battery,omitempty"` } var ronField = regexp.MustCompile(`(?m)^\s{4}([a-z_]+):\s*(.*?),?\s*$`) // ParseAsusdRon reads the top-level scalar fields of asusd.ron. RON is not a format the mesh // speaks; these are one line each, and nothing nested is read. func ParseAsusdRon(text string) AsusdConfig { var c AsusdConfig for _, g := range ronField.FindAllStringSubmatch(text, -1) { key, value := g[1], strings.TrimSuffix(strings.TrimSpace(g[2]), ",") unquoted := strings.Trim(value, `"`) boolean := func() *bool { b := value == "true"; return &b } switch key { case "charge_control_end_threshold": if n, err := strconv.Atoi(value); err == nil { c.ChargeLimit = &n } case "platform_profile_on_ac": c.ProfileOnAC = unquoted case "platform_profile_on_battery": c.ProfileOnBattery = unquoted case "change_platform_profile_on_ac": c.ChangesProfileOnAC = boolean() case "change_platform_profile_on_battery": c.ChangesProfileOnBatt = boolean() case "ac_command": c.ACCommand = unquoted case "bat_command": c.BatteryCommand = unquoted case "disable_nvidia_powerd_on_battery": c.DisablesPowerdOnBatt = boolean() } } return c } // Asusd reads asusd's file; nil when it is not there. func (m *Machine) Asusd() *AsusdConfig { text := m.read("/etc/asusd/asusd.ron") if text == "" { return nil } c := ParseAsusdRon(text) return &c }