initial commit
This commit is contained in:
21
Makefile
Normal file
21
Makefile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
PREFIX ?= /usr/local
|
||||||
|
BINDIR ?= $(PREFIX)/bin
|
||||||
|
CONFDIR ?= $(HOME)/.config/led-controller
|
||||||
|
|
||||||
|
.PHONY: build install install-service clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o led-controller .
|
||||||
|
|
||||||
|
install: build
|
||||||
|
install -Dm755 led-controller $(DESTDIR)$(BINDIR)/led-controller
|
||||||
|
install -Dm644 config.example.toml $(DESTDIR)$(CONFDIR)/config.toml
|
||||||
|
|
||||||
|
install-service:
|
||||||
|
install -Dm644 led-controller.service $(HOME)/.config/systemd/user/led-controller.service
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable led-controller.service
|
||||||
|
@echo "Start with: systemctl --user start led-controller"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f led-controller
|
||||||
90
README.md
Normal file
90
README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# led-controller
|
||||||
|
|
||||||
|
A daemon that automatically turns RGB LEDs on and off based on display state. When your monitor sleeps or you lock your screen, LEDs turn off. When you wake or unlock, they turn back on.
|
||||||
|
|
||||||
|
Controls two types of lights:
|
||||||
|
- **Computer RGB** via [OpenRGB](https://openrgb.org/) CLI (motherboard, RAM, GPU, peripherals)
|
||||||
|
- **External LEDs** via CH341 USB serial relay (LED strips, desk lights, etc.)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Linux with D-Bus (GNOME, KDE, XFCE, etc.)
|
||||||
|
- Go 1.21+
|
||||||
|
- [OpenRGB](https://openrgb.org/) installed (for PC RGB control)
|
||||||
|
- CH341 USB relay module on `/dev/ttyUSB0` (for external LED control)
|
||||||
|
|
||||||
|
## Build & Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
make install # binary + config to ~/.config/led-controller/
|
||||||
|
make install-service # enable systemd user service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Daemon (automatic)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start via systemd
|
||||||
|
systemctl --user start led-controller
|
||||||
|
|
||||||
|
# Or run directly
|
||||||
|
./led-controller daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
The daemon turns LEDs on at startup, then listens for D-Bus signals to toggle them:
|
||||||
|
- **Screen lock/blank** → LEDs off
|
||||||
|
- **Screen unlock/unblank** → LEDs on
|
||||||
|
- **System suspend** → LEDs off
|
||||||
|
- **System wake** → LEDs on
|
||||||
|
|
||||||
|
### Manual toggle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./led-controller on
|
||||||
|
./led-controller off
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Config file: `~/.config/led-controller/config.toml`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[openrgb]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[relay]
|
||||||
|
enabled = true
|
||||||
|
device = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
channel = 1
|
||||||
|
|
||||||
|
[monitor]
|
||||||
|
# "screensaver" - screen lock/blank events only
|
||||||
|
# "logind" - system suspend/resume only
|
||||||
|
# "all" - both (default)
|
||||||
|
method = "all"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
The USB relay device requires read/write access. Add your user to the `dialout` group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG dialout $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Log out and back in for the group change to take effect.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
main.go Daemon entry point, signal handling, event loop
|
||||||
|
config/ TOML config loader
|
||||||
|
monitor/ D-Bus listener (screensaver + logind signals)
|
||||||
|
controller/
|
||||||
|
controller.go Orchestrates OpenRGB + relay
|
||||||
|
openrgb/ OpenRGB CLI wrapper
|
||||||
|
relay/ CH341 serial relay protocol (0xA0 command format)
|
||||||
|
```
|
||||||
14
config.example.toml
Normal file
14
config.example.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[openrgb]
|
||||||
|
enabled = true
|
||||||
|
host = "localhost"
|
||||||
|
port = 6742
|
||||||
|
|
||||||
|
[relay]
|
||||||
|
enabled = true
|
||||||
|
device = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
channel = 1
|
||||||
|
|
||||||
|
[monitor]
|
||||||
|
# Options: "screensaver", "logind", "all"
|
||||||
|
method = "all"
|
||||||
65
config/config.go
Normal file
65
config/config.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
OpenRGB OpenRGBConfig `toml:"openrgb"`
|
||||||
|
Relay RelayConfig `toml:"relay"`
|
||||||
|
Monitor MonitorConfig `toml:"monitor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenRGBConfig struct {
|
||||||
|
Host string `toml:"host"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
Enabled bool `toml:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelayConfig struct {
|
||||||
|
Device string `toml:"device"`
|
||||||
|
Channel int `toml:"channel"`
|
||||||
|
Baud int `toml:"baud"`
|
||||||
|
Enabled bool `toml:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorConfig struct {
|
||||||
|
// Which D-Bus signals to listen on. Options: "screensaver", "logind", "all"
|
||||||
|
Method string `toml:"method"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(path string) (*Config, error) {
|
||||||
|
cfg := &Config{
|
||||||
|
OpenRGB: OpenRGBConfig{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 6742,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
Relay: RelayConfig{
|
||||||
|
Device: "/dev/ttyUSB0",
|
||||||
|
Channel: 1,
|
||||||
|
Baud: 9600,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
Monitor: MonitorConfig{
|
||||||
|
Method: "all",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("read config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := toml.Unmarshal(data, cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
82
controller/controller.go
Normal file
82
controller/controller.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"led-controller/config"
|
||||||
|
"led-controller/controller/openrgb"
|
||||||
|
"led-controller/controller/relay"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
cfg *config.Config
|
||||||
|
orgb *openrgb.Client
|
||||||
|
relay *relay.Relay
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *config.Config) (*Controller, error) {
|
||||||
|
c := &Controller{cfg: cfg}
|
||||||
|
|
||||||
|
if cfg.OpenRGB.Enabled {
|
||||||
|
client, err := openrgb.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warning: openrgb unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
c.orgb = client
|
||||||
|
log.Println("openrgb: connected via CLI")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Relay.Enabled {
|
||||||
|
c.relay = relay.New(cfg.Relay.Device, cfg.Relay.Channel, cfg.Relay.Baud)
|
||||||
|
log.Printf("relay: configured on %s channel %d", cfg.Relay.Device, cfg.Relay.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) SetLEDs(on bool) error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if c.cfg.OpenRGB.Enabled {
|
||||||
|
if err := c.setOpenRGB(on); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("openrgb: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.cfg.Relay.Enabled && c.relay != nil {
|
||||||
|
if err := c.relay.Set(on); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("relay: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%v", errs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) setOpenRGB(on bool) error {
|
||||||
|
if c.orgb == nil {
|
||||||
|
client, err := openrgb.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.orgb = client
|
||||||
|
}
|
||||||
|
|
||||||
|
if on {
|
||||||
|
return c.orgb.SetAllOn(0xFF, 0xFF, 0xFF)
|
||||||
|
}
|
||||||
|
return c.orgb.SetAllOff()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) Close() {
|
||||||
|
if c.orgb != nil {
|
||||||
|
c.orgb.Close()
|
||||||
|
}
|
||||||
|
if c.relay != nil {
|
||||||
|
c.relay.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
49
controller/openrgb/client.go
Normal file
49
controller/openrgb/client.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package openrgb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client controls RGB devices via the openrgb CLI.
|
||||||
|
type Client struct {
|
||||||
|
bin string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Connect() (*Client, error) {
|
||||||
|
bin, err := exec.LookPath("openrgb")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("openrgb not found in PATH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify openrgb can list devices (confirms it's running/accessible)
|
||||||
|
cmd := exec.Command(bin, "--list-devices")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return nil, fmt.Errorf("openrgb list-devices failed: %w\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{bin: bin}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAllOff sets all devices to black (off) using direct mode.
|
||||||
|
func (c *Client) SetAllOff() error {
|
||||||
|
cmd := exec.Command(c.bin, "--mode", "direct", "--color", "000000")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("openrgb set off: %w\n%s", err, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAllOn sets all devices to the given color.
|
||||||
|
func (c *Client) SetAllOn(r, g, b byte) error {
|
||||||
|
color := fmt.Sprintf("%02X%02X%02X", r, g, b)
|
||||||
|
cmd := exec.Command(c.bin, "--mode", "direct", "--color", color)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("openrgb set on: %w\n%s", err, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
82
controller/relay/relay.go
Normal file
82
controller/relay/relay.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CH341 serial relay protocol (LCUS-type modules on /dev/ttyUSBx).
|
||||||
|
//
|
||||||
|
// Command format:
|
||||||
|
// ON: 0xA0 <channel> 0x01 <checksum>
|
||||||
|
// OFF: 0xA0 <channel> 0x00 <checksum>
|
||||||
|
// checksum = (0xA0 + channel + state) & 0xFF
|
||||||
|
|
||||||
|
type Relay struct {
|
||||||
|
devicePath string
|
||||||
|
channel byte
|
||||||
|
baud int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(devicePath string, channel, baud int) *Relay {
|
||||||
|
return &Relay{
|
||||||
|
devicePath: devicePath,
|
||||||
|
channel: byte(channel),
|
||||||
|
baud: baud,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Relay) Set(on bool) error {
|
||||||
|
f, err := os.OpenFile(r.devicePath, os.O_RDWR|unix.O_NOCTTY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("relay open %s: %w", r.devicePath, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fd := int(f.Fd())
|
||||||
|
if err := configureSerial(fd, r.baud); err != nil {
|
||||||
|
return fmt.Errorf("relay serial config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var state byte
|
||||||
|
if on {
|
||||||
|
state = 0x01
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum := (0xA0 + r.channel + state) & 0xFF
|
||||||
|
cmd := []byte{0xA0, r.channel, state, checksum}
|
||||||
|
|
||||||
|
if _, err := f.Write(cmd); err != nil {
|
||||||
|
return fmt.Errorf("relay write: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Relay) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureSerial(fd, baud int) error {
|
||||||
|
baudRate, ok := baudRates[baud]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported baud rate: %d", baud)
|
||||||
|
}
|
||||||
|
|
||||||
|
var t unix.Termios
|
||||||
|
t.Cflag = unix.CS8 | unix.CLOCAL | unix.CREAD | baudRate
|
||||||
|
t.Cc[unix.VMIN] = 1
|
||||||
|
t.Cc[unix.VTIME] = 0
|
||||||
|
|
||||||
|
return unix.IoctlSetTermios(fd, unix.TCSETS, &t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var baudRates = map[int]uint32{
|
||||||
|
9600: unix.B9600,
|
||||||
|
19200: unix.B19200,
|
||||||
|
38400: unix.B38400,
|
||||||
|
57600: unix.B57600,
|
||||||
|
115200: unix.B115200,
|
||||||
|
}
|
||||||
9
go.mod
Normal file
9
go.mod
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module led-controller
|
||||||
|
|
||||||
|
go 1.25.8
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
|
golang.org/x/sys v0.27.0 // indirect
|
||||||
|
)
|
||||||
6
go.sum
Normal file
6
go.sum
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
|
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||||
|
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
BIN
led-controller
Executable file
BIN
led-controller
Executable file
Binary file not shown.
13
led-controller.service
Normal file
13
led-controller.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=LED Controller Daemon
|
||||||
|
After=graphical-session.target
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/led-controller --config %h/.config/led-controller/config.toml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
104
main.go
Normal file
104
main.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"led-controller/config"
|
||||||
|
"led-controller/controller"
|
||||||
|
"led-controller/monitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfgPath := flag.String("config", "/etc/led-controller/config.toml", "path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
|
||||||
|
cfg, err := config.Load(*cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual toggle: led-controller [--config ...] on|off
|
||||||
|
if len(args) > 0 {
|
||||||
|
switch args[0] {
|
||||||
|
case "on", "off":
|
||||||
|
ctrl, err := controller.New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
defer ctrl.Close()
|
||||||
|
|
||||||
|
on := args[0] == "on"
|
||||||
|
if err := ctrl.SetLEDs(on); err != nil {
|
||||||
|
log.Fatalf("failed to set LEDs %s: %v", args[0], err)
|
||||||
|
}
|
||||||
|
fmt.Printf("LEDs turned %s\n", args[0])
|
||||||
|
return
|
||||||
|
case "daemon":
|
||||||
|
// fall through to daemon mode below
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: led-controller [--config path] [on|off|daemon]\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemon mode
|
||||||
|
log.Printf("led-controller starting (relay=%s)", cfg.Relay.Device)
|
||||||
|
|
||||||
|
ctrl, err := controller.New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
defer ctrl.Close()
|
||||||
|
|
||||||
|
// Turn LEDs on at startup — the display is on if we're booting
|
||||||
|
log.Println("startup — enabling LEDs")
|
||||||
|
if err := ctrl.SetLEDs(true); err != nil {
|
||||||
|
log.Printf("error enabling LEDs at startup: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
events := make(chan monitor.Event, 16)
|
||||||
|
mon, err := monitor.New(cfg, events)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create monitor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go mon.Run(ctx)
|
||||||
|
|
||||||
|
log.Println("daemon running, waiting for display events")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ev := <-events:
|
||||||
|
switch ev {
|
||||||
|
case monitor.DisplayOn:
|
||||||
|
log.Println("display ON — enabling LEDs")
|
||||||
|
if err := ctrl.SetLEDs(true); err != nil {
|
||||||
|
log.Printf("error enabling LEDs: %v", err)
|
||||||
|
}
|
||||||
|
case monitor.DisplayOff:
|
||||||
|
log.Println("display OFF — disabling LEDs")
|
||||||
|
if err := ctrl.SetLEDs(false); err != nil {
|
||||||
|
log.Printf("error disabling LEDs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case sig := <-sigCh:
|
||||||
|
log.Printf("received %v, shutting down", sig)
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
monitor/monitor.go
Normal file
146
monitor/monitor.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
|
||||||
|
"led-controller/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DisplayOn Event = iota
|
||||||
|
DisplayOff
|
||||||
|
)
|
||||||
|
|
||||||
|
type Monitor struct {
|
||||||
|
cfg *config.Config
|
||||||
|
events chan<- Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *config.Config, events chan<- Event) (*Monitor, error) {
|
||||||
|
return &Monitor{cfg: cfg, events: events}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) Run(ctx context.Context) {
|
||||||
|
method := m.cfg.Monitor.Method
|
||||||
|
|
||||||
|
if method == "screensaver" || method == "all" {
|
||||||
|
go m.watchScreenSaver(ctx)
|
||||||
|
}
|
||||||
|
if method == "logind" || method == "all" {
|
||||||
|
go m.watchLogind(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchScreenSaver listens on the session bus for org.freedesktop.ScreenSaver.ActiveChanged
|
||||||
|
// This covers GNOME, KDE, XFCE, and other freedesktop-compliant DEs.
|
||||||
|
func (m *Monitor) watchScreenSaver(ctx context.Context) {
|
||||||
|
conn, err := dbus.ConnectSessionBus()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("screensaver monitor: cannot connect to session bus: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Match both the freedesktop and GNOME screensaver interfaces
|
||||||
|
rules := []string{
|
||||||
|
"type='signal',interface='org.freedesktop.ScreenSaver',member='ActiveChanged'",
|
||||||
|
"type='signal',interface='org.gnome.ScreenSaver',member='ActiveChanged'",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
call := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, rule)
|
||||||
|
if call.Err != nil {
|
||||||
|
log.Printf("screensaver monitor: AddMatch failed for %q: %v", rule, call.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sigCh := make(chan *dbus.Signal, 16)
|
||||||
|
conn.Signal(sigCh)
|
||||||
|
|
||||||
|
log.Println("screensaver monitor: listening for ActiveChanged signals")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case sig := <-sigCh:
|
||||||
|
if sig == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sig.Name == "org.freedesktop.ScreenSaver.ActiveChanged" ||
|
||||||
|
sig.Name == "org.gnome.ScreenSaver.ActiveChanged" {
|
||||||
|
if len(sig.Body) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
active, ok := sig.Body[0].(bool)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if active {
|
||||||
|
log.Println("screensaver monitor: screen locked/blanked")
|
||||||
|
m.events <- DisplayOff
|
||||||
|
} else {
|
||||||
|
log.Println("screensaver monitor: screen unlocked/unblanked")
|
||||||
|
m.events <- DisplayOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchLogind listens on the system bus for org.freedesktop.login1.Manager.PrepareForSleep.
|
||||||
|
// This catches system suspend/resume events.
|
||||||
|
func (m *Monitor) watchLogind(ctx context.Context) {
|
||||||
|
conn, err := dbus.ConnectSystemBus()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("logind monitor: cannot connect to system bus: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
rule := "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'"
|
||||||
|
call := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, rule)
|
||||||
|
if call.Err != nil {
|
||||||
|
log.Printf("logind monitor: AddMatch failed: %v", call.Err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sigCh := make(chan *dbus.Signal, 16)
|
||||||
|
conn.Signal(sigCh)
|
||||||
|
|
||||||
|
log.Println("logind monitor: listening for PrepareForSleep signals")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case sig := <-sigCh:
|
||||||
|
if sig == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sig.Name == "org.freedesktop.login1.Manager.PrepareForSleep" {
|
||||||
|
if len(sig.Body) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
preparing, ok := sig.Body[0].(bool)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if preparing {
|
||||||
|
log.Println("logind monitor: system preparing to sleep")
|
||||||
|
m.events <- DisplayOff
|
||||||
|
} else {
|
||||||
|
log.Println("logind monitor: system woke up")
|
||||||
|
m.events <- DisplayOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user