aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go-sysmon/.gitignore1
-rw-r--r--go-sysmon/Makefile7
-rw-r--r--go-sysmon/go.mod17
-rw-r--r--go-sysmon/go.sum26
-rw-r--r--go-sysmon/main.go345
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")
+}