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") }