diff options
| author | Blaster4385 <[email protected]> | 2025-10-29 10:49:58 +0530 |
|---|---|---|
| committer | Blaster4385 <[email protected]> | 2025-10-29 10:49:58 +0530 |
| commit | 6a253bf6fcd845b1abbcf5cf3064196793f1a0bd (patch) | |
| tree | 71238c3fc04df6f0f38fee6ab1e8f0f254c5e3ed /go-sysmon/main.go | |
| parent | 9a7fd378324a514b1baff96f0ee86b21e6406fe7 (diff) | |
Diffstat (limited to 'go-sysmon/main.go')
| -rw-r--r-- | go-sysmon/main.go | 345 |
1 files changed, 345 insertions, 0 deletions
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") +} |