aboutsummaryrefslogtreecommitdiff
path: root/go-sysmon/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'go-sysmon/main.go')
-rw-r--r--go-sysmon/main.go345
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")
+}