diff options
| -rw-r--r-- | go-sysmon/.gitignore | 1 | ||||
| -rw-r--r-- | go-sysmon/Makefile | 7 | ||||
| -rw-r--r-- | go-sysmon/go.mod | 17 | ||||
| -rw-r--r-- | go-sysmon/go.sum | 26 | ||||
| -rw-r--r-- | go-sysmon/main.go | 345 |
5 files changed, 396 insertions, 0 deletions
diff --git a/go-sysmon/.gitignore b/go-sysmon/.gitignore new file mode 100644 index 0000000..14854ff --- /dev/null +++ b/go-sysmon/.gitignore @@ -0,0 +1 @@ +esp8266-sysmon diff --git a/go-sysmon/Makefile b/go-sysmon/Makefile new file mode 100644 index 0000000..a4aa7da --- /dev/null +++ b/go-sysmon/Makefile @@ -0,0 +1,7 @@ +.PHONY: sysmon clean + +sysmon: + go build -ldflags "-s -w" + +clean: + rm esp8266-sysmon diff --git a/go-sysmon/go.mod b/go-sysmon/go.mod new file mode 100644 index 0000000..6613f73 --- /dev/null +++ b/go-sysmon/go.mod @@ -0,0 +1,17 @@ +module esp8266-sysmon + +go 1.23.4 + +require ( + github.com/creack/goselect v0.1.2 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.7 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.bug.st/serial v1.6.4 // indirect + golang.org/x/sys v0.34.0 // indirect +) diff --git a/go-sysmon/go.sum b/go-sysmon/go.sum new file mode 100644 index 0000000..93d25db --- /dev/null +++ b/go-sysmon/go.sum @@ -0,0 +1,26 @@ +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM= +github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go-sysmon/main.go b/go-sysmon/main.go new file mode 100644 index 0000000..580f27d --- /dev/null +++ b/go-sysmon/main.go @@ -0,0 +1,345 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "os/signal" + "runtime" + "strconv" + "strings" + "sync/atomic" + "syscall" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/mem" + "go.bug.st/serial" +) + +type Config struct { + SerialPort string + BaudRate int + ConnectionWait time.Duration + SendInterval time.Duration + ReconnectDelay time.Duration + CPUTempPath string + Debug bool +} + +type SystemMetrics struct { + CPUTemp int + CPUUsage int + RAMTotal int + RAMAvailable int + OSName string + KernelVersion string + Uptime string + Time string +} + +type MetricsCollector struct { + cpuUsage atomic.Int32 + ramTotal int + osName string + kernelVersion string + cpuTempPaths []string + cpuTempCache atomic.Int32 + lastTempCheck atomic.Int64 + ctx context.Context + cancel context.CancelFunc +} + +type SerialManager struct { + cfg *Config + collector *MetricsCollector +} + +func loadConfig() *Config { + cfg := &Config{ + BaudRate: 115200, + ConnectionWait: 6 * time.Second, + SendInterval: 3 * time.Second, + ReconnectDelay: 5 * time.Second, + CPUTempPath: os.Getenv("CPU_TEMP_PATH"), + Debug: os.Getenv("DEBUG") == "1" || os.Getenv("DEBUG") == "true", + } + + if env := os.Getenv("SERIAL_PORT"); env != "" { + cfg.SerialPort = env + } else if runtime.GOOS == "windows" { + cfg.SerialPort = "COM3" + } else { + cfg.SerialPort = "/dev/arduino" + } + + return cfg +} + +var debugEnabled bool + +func log(level, format string, args ...interface{}) { + if level == "DEBUG" && !debugEnabled { + return + } + timestamp := time.Now().Format("15:04:05") + fmt.Printf("[%s] [%s] %s\n", timestamp, level, fmt.Sprintf(format, args...)) +} + +func (m *SystemMetrics) Format() string { + return fmt.Sprintf( + "\nCpuTemp=%d,CpuUsage=%d,RamMax=%d,RamFree=%d,Time=%s,OS=%s,Kernel=%s,Uptime=%s", + m.CPUTemp, m.CPUUsage, m.RAMTotal, m.RAMAvailable, + m.Time, m.OSName, m.KernelVersion, m.Uptime, + ) +} + +func NewMetricsCollector(cfg *Config) (*MetricsCollector, error) { + ctx, cancel := context.WithCancel(context.Background()) + mc := &MetricsCollector{ + ctx: ctx, + cancel: cancel, + } + + mc.osName = getOSName() + mc.kernelVersion = getKernelVersion() + mc.ramTotal, _ = getRAMInfo() + mc.cpuTempPaths = mc.getTempPaths(cfg.CPUTempPath) + + log("INFO", "System: OS=%s, Kernel=%s, RAM=%dMB", mc.osName, mc.kernelVersion, mc.ramTotal) + + go mc.monitorCPU() + return mc, nil +} + +func (mc *MetricsCollector) Close() { + mc.cancel() +} + +func (mc *MetricsCollector) monitorCPU() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-mc.ctx.Done(): + return + case <-ticker.C: + if percent, err := cpu.Percent(0, false); err == nil && len(percent) > 0 { + mc.cpuUsage.Store(int32(percent[0])) + } + } + } +} + +func getOSName() string { + if runtime.GOOS == "windows" { + return "Windows" + } + + file, err := os.Open("/etc/os-release") + if err != nil { + return runtime.GOOS + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "NAME=") { + return strings.Trim(line[5:], `"`) + } + } + return "Unknown" +} + +func getKernelVersion() string { + info, err := host.Info() + if err != nil { + return "Unknown" + } + + version := info.KernelVersion + if idx := strings.Index(version, "-"); idx > 0 { + version = version[:idx] + } + if len(version) > 10 { + version = version[:10] + } + return version +} + +func getUptime() string { + uptime, err := host.Uptime() + if err != nil { + return "00:00" + } + h, m := uptime/3600, (uptime%3600)/60 + return fmt.Sprintf("%02d:%02d", h, m) +} + +func getRAMInfo() (int, int) { + vm, err := mem.VirtualMemory() + if err != nil { + return 0, 0 + } + return int(vm.Total >> 20), int(vm.Available >> 20) +} + +func (mc *MetricsCollector) getTempPaths(customPath string) []string { + if customPath != "" { + return []string{customPath} + } + return []string{ + "/sys/class/thermal/thermal_zone0/temp", + "/sys/class/hwmon/hwmon0/temp1_input", + "/sys/devices/platform/coretemp.0/hwmon/hwmon0/temp1_input", + "/sys/devices/pci0000:00/0000:00:18.3/hwmon/hwmon0/temp1_input", + } +} + +func (mc *MetricsCollector) getCPUTemp() int { + if runtime.GOOS == "windows" { + return 0 + } + + now := time.Now().UnixMilli() + if now-mc.lastTempCheck.Load() < 2000 { + return int(mc.cpuTempCache.Load()) + } + + for _, path := range mc.cpuTempPaths { + data, err := os.ReadFile(path) + if err != nil { + continue + } + + val, err := strconv.Atoi(strings.TrimSpace(string(data))) + if err != nil { + continue + } + + temp := val / 1000 + if temp > 0 && temp < 120 { + mc.cpuTempCache.Store(int32(temp)) + mc.lastTempCheck.Store(now) + return temp + } + } + + return 0 +} + +func (mc *MetricsCollector) Collect() *SystemMetrics { + _, ramAvail := getRAMInfo() + return &SystemMetrics{ + CPUTemp: mc.getCPUTemp(), + CPUUsage: int(mc.cpuUsage.Load()), + RAMTotal: mc.ramTotal, + RAMAvailable: ramAvail, + OSName: mc.osName, + KernelVersion: mc.kernelVersion, + Uptime: getUptime(), + Time: time.Now().Format("15:04:05"), + } +} + +func NewSerialManager(cfg *Config, collector *MetricsCollector) *SerialManager { + return &SerialManager{cfg: cfg, collector: collector} +} + +func (sm *SerialManager) connect() (serial.Port, error) { + return serial.Open(sm.cfg.SerialPort, &serial.Mode{BaudRate: sm.cfg.BaudRate}) +} + +func (sm *SerialManager) Run(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + port, err := sm.connect() + if err != nil { + log("ERROR", "Failed to open serial port: %v", err) + time.Sleep(sm.cfg.ReconnectDelay) + continue + } + + log("INFO", "Connected to %s", sm.cfg.SerialPort) + time.Sleep(sm.cfg.ConnectionWait) + + err = sm.sendData(ctx, port) + port.Close() + + if err == context.Canceled { + return err + } + + if err != nil { + log("ERROR", "Send error: %v", err) + } + + log("INFO", "Reconnecting...") + time.Sleep(sm.cfg.ReconnectDelay) + } +} + +func (sm *SerialManager) sendData(ctx context.Context, port serial.Port) error { + ticker := time.NewTicker(sm.cfg.SendInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + metrics := sm.collector.Collect() + data := metrics.Format() + + if _, err := port.Write([]byte(data)); err != nil { + return err + } + + log("DEBUG", "Sent: %s", strings.TrimSpace(data)) + } + } +} + +func main() { + log("INFO", "Starting system monitor on %s", runtime.GOOS) + + cfg := loadConfig() + debugEnabled = cfg.Debug + + collector, err := NewMetricsCollector(cfg) + if err != nil { + log("ERROR", "Failed to start: %v", err) + os.Exit(1) + } + defer collector.Close() + + manager := NewSerialManager(cfg, collector) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + go func() { + <-sigChan + log("INFO", "Shutting down...") + cancel() + }() + + if err := manager.Run(ctx); err != nil && err != context.Canceled { + log("ERROR", "Fatal: %v", err) + os.Exit(1) + } + + log("INFO", "Stopped") +} |