diff --git a/config.yaml b/config.yaml index f2e6e3e..1412659 100644 --- a/config.yaml +++ b/config.yaml @@ -11,7 +11,7 @@ plc: reconnect_delay_sec: 5 thresholds: warning_percent: 80 - critical_percent: 95 + critical_percent: 100 gauge_max_percent: 130 trend: minutes: 5 diff --git a/main.go b/main.go index 915a184..7a8c0ec 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - htmlstd "html" + "html/template" "log" "net/http" "os" @@ -49,7 +49,7 @@ type ThresholdsConfig struct { CriticalPercent float64 `yaml:"critical_percent"` GaugeMaxPercent float64 `yaml:"gauge_max_percent"` - // legacy compatibility with previous config names + // legacy compatibility LegacyWarningKn float64 `yaml:"warning_kn,omitempty"` LegacyCriticalKn float64 `yaml:"critical_kn,omitempty"` LegacyMaxKn float64 `yaml:"max_kn,omitempty"` @@ -62,7 +62,7 @@ type TrendConfig struct { type PressConfig struct { MAX_TONNAGE float64 `yaml:"MAX_TONNAGE"` - // optional legacy compatibility + // legacy compatibility LegacyMaxTonnage float64 `yaml:"max_tonnage,omitempty"` } @@ -258,8 +258,8 @@ type AppState struct { type APIState struct { Connected bool `json:"connected"` - SilaL float32 `json:"sila_l"` // % - SilaR float32 `json:"sila_r"` // % + SilaL float32 `json:"sila_l"` + SilaR float32 `json:"sila_r"` SilaLkN float32 `json:"sila_l_kn"` SilaRkN float32 `json:"sila_r_kn"` SumPercent float32 `json:"sum_percent"` @@ -268,18 +268,31 @@ type APIState struct { LastUpdate string `json:"last_update"` } +type PageData struct { + Title string + Subtitle string + LeftLabel string + RightLabel string + UnitForce string + UnitPct string + MaxTonnage float64 + WarningPercent float64 + CriticalPercent float64 + GaugeMaxPercent float64 + TrendMinutes int + MaxHistoryPoints int + PollMs int +} + var ( - state AppState - cfg Config + state AppState + cfg Config + uiTmpl = template.Must(template.New("ui").Parse(uiHTML)) ) // ============================================= // Helpers // ============================================= -func esc(s string) string { - return htmlstd.EscapeString(s) -} - func setDisconnected() { state.Lock() state.Connected = false @@ -321,7 +334,7 @@ func calculateForces(leftPercent, rightPercent float32) (leftKN, rightKN, sumPer left := (lp / 100.0) * (cfg.Press.MAX_TONNAGE / 2.0) right := (rp / 100.0) * (cfg.Press.MAX_TONNAGE / 2.0) - // total force by your rule + // total force by rule total := (sumPct / 100.0) * cfg.Press.MAX_TONNAGE return float32(left), float32(right), float32(sumPct), float32(total) @@ -359,8 +372,8 @@ func startPLCPoller() { } var helper gos7.Helper - silaL := helper.GetRealAt(buf, 0) // % - silaR := helper.GetRealAt(buf, 4) // % + silaL := helper.GetRealAt(buf, 0) + silaR := helper.GetRealAt(buf, 4) leftKN, rightKN, sumPercent, sumKN := calculateForces(silaL, silaR) @@ -405,30 +418,141 @@ func apiData(w http.ResponseWriter, r *http.Request) { } func serveUI(w http.ResponseWriter, r *http.Request) { - maxHistoryPoints := getMaxHistoryPoints(cfg) + data := PageData{ + Title: cfg.UI.Title, + Subtitle: cfg.UI.Subtitle, + LeftLabel: cfg.UI.LeftLabel, + RightLabel: cfg.UI.RightLabel, + UnitForce: cfg.UI.UnitForce, + UnitPct: cfg.UI.UnitPct, + MaxTonnage: cfg.Press.MAX_TONNAGE, + WarningPercent: cfg.Thresholds.WarningPercent, + CriticalPercent: cfg.Thresholds.CriticalPercent, + GaugeMaxPercent: cfg.Thresholds.GaugeMaxPercent, + TrendMinutes: cfg.Trend.Minutes, + MaxHistoryPoints: getMaxHistoryPoints(cfg), + PollMs: cfg.PLC.PollMs, + } - html := ` + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := uiTmpl.Execute(w, data); err != nil { + http.Error(w, "failed to render UI", http.StatusInternalServerError) + log.Printf("template execute error: %v", err) + } +} + +// ============================================= +// Main +// ============================================= +func main() { + wd, err := os.Getwd() + if err != nil { + log.Fatalf("failed to get working directory: %v", err) + } + + configPath := filepath.Join(wd, "config.yaml") + + cfg, err = loadOrCreateConfig(configPath) + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + + log.Printf("config loaded from: %s", configPath) + log.Printf("PLC: ip=%s db=%d rack=%d slot=%d poll=%dms", + cfg.PLC.IP, cfg.PLC.DBNum, cfg.PLC.Rack, cfg.PLC.Slot, cfg.PLC.PollMs) + log.Printf("Press MAX_TONNAGE: %.2f %s", cfg.Press.MAX_TONNAGE, cfg.UI.UnitForce) + + go startPLCPoller() + + http.HandleFunc("/", serveUI) + http.HandleFunc("/api/data", apiData) + + log.Println("S7-1200 Force Monitor started") + log.Printf("Open: http://localhost%s", cfg.Server.ListenAddr) + log.Fatal(http.ListenAndServe(cfg.Server.ListenAddr, nil)) +} + +// ============================================= +// Embedded UI Template +// ============================================= +const uiHTML = `
-