diff --git a/main.go b/main.go index 135336d..396f732 100644 --- a/main.go +++ b/main.go @@ -7,10 +7,10 @@ import ( "fmt" "html/template" "log" + "math" "net/http" "os" "path/filepath" - "sort" "strconv" "strings" "sync" @@ -108,7 +108,7 @@ func defaultConfig() Config { Press: PressConfig{MAX_TONNAGE: 64}, UI: UIConfig{ Title: "Force Monitor", - Subtitle: "Siemens S7-1215C • Live monitoring • PLC values in % • kN calculated from MAX_TONNAGE", + Subtitle: "Siemens S7-1215C • Piezo peak/stroke input • PLC values in % • kN calculated from MAX_TONNAGE", LeftLabel: "LEVI STEBER", RightLabel: "DESNI STEBER", UnitForce: "kN", @@ -263,45 +263,45 @@ func loadOrCreateConfig(configPath string) (Config, error) { return cfg, nil } -type Measurement struct { - Time string `json:"time"` - SilaL float32 `json:"sila_l"` - SilaR float32 `json:"sila_r"` -} - type Sample struct { - TS time.Time - SilaLPct float32 - SilaRPct float32 - SilaLKN float32 - SilaRKN float32 - SumPercent float32 - SumKN float32 + TS time.Time + SilaLPct float32 + SilaRPct float32 + SilaLKN float32 + SilaRKN float32 + SumPercent float32 + SumKN float32 + ImbalancePercent float32 + BiasPercent float32 } type AppState struct { sync.RWMutex - Connected bool - SilaL float32 - SilaR float32 - SilaLkN float32 - SilaRkN float32 - SumPercent float32 - SumkN float32 - LastUpdate time.Time - DroppedSamples uint64 + Connected bool + SilaL float32 + SilaR float32 + SilaLkN float32 + SilaRkN float32 + SumPercent float32 + SumkN float32 + ImbalancePercent float32 + BiasPercent float32 + LastUpdate time.Time + DroppedSamples uint64 } type APIState struct { - Connected bool `json:"connected"` - 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"` - SumkN float32 `json:"sum_kn"` - LastUpdate string `json:"last_update"` - DroppedSamples uint64 `json:"dropped_samples"` + Connected bool `json:"connected"` + 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"` + SumkN float32 `json:"sum_kn"` + ImbalancePercent float32 `json:"imbalance_percent"` + BiasPercent float32 `json:"bias_percent"` + LastUpdate string `json:"last_update"` + DroppedSamples uint64 `json:"dropped_samples"` } type HistoryPoint struct { @@ -360,15 +360,17 @@ func snapshotState() APIState { } return APIState{ - Connected: state.Connected, - SilaL: state.SilaL, - SilaR: state.SilaR, - SilaLkN: state.SilaLkN, - SilaRkN: state.SilaRkN, - SumPercent: state.SumPercent, - SumkN: state.SumkN, - LastUpdate: lastUpdate, - DroppedSamples: state.DroppedSamples, + Connected: state.Connected, + SilaL: state.SilaL, + SilaR: state.SilaR, + SilaLkN: state.SilaLkN, + SilaRkN: state.SilaRkN, + SumPercent: state.SumPercent, + SumkN: state.SumkN, + ImbalancePercent: state.ImbalancePercent, + BiasPercent: state.BiasPercent, + LastUpdate: lastUpdate, + DroppedSamples: state.DroppedSamples, } } @@ -423,11 +425,12 @@ CREATE TABLE IF NOT EXISTS samples ( sila_l_kn REAL NOT NULL, sila_r_kn REAL NOT NULL, sum_pct REAL NOT NULL, - sum_kn REAL NOT NULL + sum_kn REAL NOT NULL, + imbalance_pct REAL NOT NULL, + bias_pct REAL NOT NULL ); CREATE INDEX IF NOT EXISTS idx_samples_ts ON samples(ts); ` - if _, err := database.Exec(schema); err != nil { _ = database.Close() return nil, fmt.Errorf("create schema: %w", err) @@ -453,7 +456,12 @@ func startDBWriter(database *sql.DB) { return } - stmt, err := tx.Prepare(`INSERT INTO samples (ts, sila_l_pct, sila_r_pct, sila_l_kn, sila_r_kn, sum_pct, sum_kn) VALUES (?, ?, ?, ?, ?, ?, ?)`) + stmt, err := tx.Prepare(` + INSERT INTO samples ( + ts, sila_l_pct, sila_r_pct, sila_l_kn, sila_r_kn, + sum_pct, sum_kn, imbalance_pct, bias_pct + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `) if err != nil { _ = tx.Rollback() log.Printf("db prepare failed: %v", err) @@ -470,6 +478,8 @@ func startDBWriter(database *sql.DB) { s.SilaRKN, s.SumPercent, s.SumKN, + s.ImbalancePercent, + s.BiasPercent, ) if err != nil { ok = false @@ -557,6 +567,8 @@ func startPLCPoller() { silaR := helper.GetRealAt(buf, 4) leftKN, rightKN, sumPercent, sumKN := calculateForces(silaL, silaR) + imbalance := float32(math.Abs(float64(silaL - silaR))) + bias := silaL - silaR now := time.Now() state.Lock() @@ -567,17 +579,21 @@ func startPLCPoller() { state.SilaRkN = rightKN state.SumPercent = sumPercent state.SumkN = sumKN + state.ImbalancePercent = imbalance + state.BiasPercent = bias state.LastUpdate = now state.Unlock() enqueueSample(Sample{ - TS: now, - SilaLPct: silaL, - SilaRPct: silaR, - SilaLKN: leftKN, - SilaRKN: rightKN, - SumPercent: sumPercent, - SumKN: sumKN, + TS: now, + SilaLPct: silaL, + SilaRPct: silaR, + SilaLKN: leftKN, + SilaRKN: rightKN, + SumPercent: sumPercent, + SumKN: sumKN, + ImbalancePercent: imbalance, + BiasPercent: bias, }) time.Sleep(time.Duration(cfg.PLC.PollMs) * time.Millisecond) @@ -607,6 +623,14 @@ func parseWindow(raw string) (time.Duration, string, error) { return d, s, nil } +func formatHistoryLabel(t time.Time, window time.Duration) string { + local := t.Local() + if window >= 12*time.Hour { + return local.Format("02.01 15:04") + } + return local.Format("15:04:05.000") +} + func queryHistory(window time.Duration) ([]HistoryPoint, error) { cutoff := time.Now().Add(-window).UTC().Format(time.RFC3339Nano) @@ -628,7 +652,7 @@ func queryHistory(window time.Duration) ([]HistoryPoint, error) { continue } points = append(points, HistoryPoint{ - Time: t.Local().Format("15:04:05.000"), + Time: formatHistoryLabel(t, window), SilaL: float32(l), SilaR: float32(r), }) @@ -641,15 +665,14 @@ func queryHistory(window time.Duration) ([]HistoryPoint, error) { return points, nil } - sampled := downsamplePoints(points, cfg.DB.MaxChartPoints) - sort.Slice(sampled, func(i, j int) bool { return sampled[i].Time < sampled[j].Time }) - return sampled, nil + return downsamplePoints(points, cfg.DB.MaxChartPoints), nil } func downsamplePoints(points []HistoryPoint, max int) []HistoryPoint { if len(points) <= max || max < 3 { return points } + out := make([]HistoryPoint, 0, max) step := float64(len(points)-1) / float64(max-1) used := make(map[int]struct{}, max) @@ -666,9 +689,10 @@ func downsamplePoints(points []HistoryPoint, max int) []HistoryPoint { out = append(out, points[idx]) } - if out[len(out)-1].Time != points[len(points)-1].Time { - out[len(out)-1] = points[len(points)-1] + if len(out) == 0 { + return points } + out[len(out)-1] = points[len(points)-1] return out } @@ -782,11 +806,10 @@ const uiHTML = ` --bg1: #050816; --bg2: #0b1224; --panel: rgba(255,255,255,0.055); - --line: rgba(255,255,255,0.09); - --muted: #a1a1aa; } * { box-sizing: border-box; } + body { font-family: 'Inter', system-ui, sans-serif; background: @@ -795,33 +818,40 @@ const uiHTML = ` linear-gradient(180deg, var(--bg1) 0%, var(--bg2) 100%); color: #f4f4f5; } + .title { font-family: 'Space Grotesk', sans-serif; } + .glass { background: var(--panel); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); } + .soft-glow-green { box-shadow: 0 0 0 1px rgba(34,197,94,0.28), 0 0 38px rgba(34,197,94,0.08); } .soft-glow-yellow { box-shadow: 0 0 0 1px rgba(234,179,8,0.28), 0 0 38px rgba(234,179,8,0.08); } .soft-glow-red { box-shadow: 0 0 0 1px rgba(239,68,68,0.28), 0 0 38px rgba(239,68,68,0.08); } + .gauge-container { position: relative; width: 100%; - max-width: 420px; - height: 340px; + max-width: 500px; + height: 390px; margin: 0 auto; } + .gauge-canvas { width: 100%; height: 100%; display: block; } + .window-btn.active { border-color: rgba(34,211,238,0.9); color: white; background: rgba(34,211,238,0.14); box-shadow: 0 0 0 1px rgba(34,211,238,0.18) inset; } + .chart-wrap { width: min(92vw, 1800px); margin: 0 auto; @@ -843,6 +873,7 @@ const uiHTML = `
{{.Subtitle}}
MAX_TONNAGE = {{printf "%.1f" .MaxTonnage}} {{.UnitForce}}
+