Today Commit

This commit is contained in:
proitlab committed 2026-07-23 20:42:14 +07:00
1 parent 6cddf2c9d3
commit 3b524d4a65
7 files changed
+223 -168

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ After=network.target
Type=simple
User=dsutanto
WorkingDirectory=/media/jetson/DATA/.Codes/chicken-counting-sukawarna-det
ExecStart=/media/jetson/DATA/karung-sukawarna/venv/bin/python dashboard.py --port 8080 --db chicken_counts.db
ExecStart=/media/jetson/DATA/karung-sukawarna/venv/bin/python dashboard.py --port 8080 --db db/chicken_counts.db
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
+1 -1
View File
@@ -6,7 +6,7 @@ batch:
delete_intermediate: false
checkpoint_every_n_frames: 3000
location: kandang-atas
db_path: chicken_counts.db
db_path: db/chicken_counts.db
defaults:
detection:
+191 -151
View File
@@ -1,37 +1,43 @@
#!/usr/bin/env python3
"""Live dashboard for chicken-counter pipeline using Flask.
Serve from project root:
python3 dashboard.py [--port 8080] [--date 2026-06-10] [--db chicken_counts.db]
"""
"""Live dashboard for chicken-counter pipeline."""
from __future__ import annotations
import argparse
import json
import logging
import sqlite3
import threading
import time
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from socketserver import ThreadingMixIn
from urllib.parse import unquote, urlparse
from flask import Flask, jsonify, render_template, send_file
from waitress import serve
# Suppress Flask request logs
log = logging.getLogger("werkzeug")
log.setLevel(logging.ERROR)
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
DEFAULT_SHM_DIR = "/dev/shm"
DEFAULT_PORT = 8080
app = Flask(__name__)
app.config["shm_dir"] = DEFAULT_SHM_DIR
app.config["poll_ms"] = 1000
app.config["run_date"] = ""
app.config["db_path"] = ""
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
_db_conn = None
_db_lock = threading.Lock()
_db_path = ""
def _get_db():
global _db_conn
if not _db_path:
return None
with _db_lock:
if _db_conn is None:
_db_conn = sqlite3.connect(_db_path, check_same_thread=False)
_db_conn.row_factory = sqlite3.Row
_db_conn.execute("PRAGMA journal_mode=WAL")
_db_conn.execute("PRAGMA cache_size=-8000")
return _db_conn
def _init_db(db_path: str) -> None:
@@ -51,22 +57,8 @@ def _init_db(db_path: str) -> None:
conn.close()
def _get_db():
global _db_conn
db = app.config["db_path"]
if not db:
return None
with _db_lock:
if _db_conn is None:
_db_conn = sqlite3.connect(db, check_same_thread=False)
_db_conn.row_factory = sqlite3.Row
_db_conn.execute("PRAGMA journal_mode=WAL")
_db_conn.execute("PRAGMA cache_size=-8000")
return _db_conn
def _discover_cameras():
shm = Path(app.config["shm_dir"])
def _discover_cameras(shm_dir):
shm = Path(shm_dir)
cameras = []
if shm.is_dir():
for entry in sorted(shm.iterdir()):
@@ -75,152 +67,200 @@ def _discover_cameras():
return cameras
@app.route("/")
def index():
return render_template(
"index.html",
poll_ms=app.config["poll_ms"],
shm_dir=app.config["shm_dir"],
date=app.config["run_date"] or "today",
db_path=app.config["db_path"],
)
class DashboardHandler(SimpleHTTPRequestHandler):
shm_dir = DEFAULT_SHM_DIR
poll_ms = 1000
run_date = ""
def log_message(self, format, *args):
pass
@app.route("/api/cameras")
def api_cameras():
return jsonify({"cameras": _discover_cameras()})
def do_GET(self):
try:
self._handle()
except (BrokenPipeError, ConnectionResetError):
pass
def _handle(self):
parsed = urlparse(self.path)
path = unquote(parsed.path)
@app.route("/shm/<camera_id>/stats.json")
def shm_stats(camera_id):
stats_path = Path(app.config["shm_dir"]) / f"chicken_counter_{camera_id}" / "stats.json"
if not stats_path.exists():
return jsonify({"error": "not found"}), 404
return jsonify(json.loads(stats_path.read_text()))
if path == "/":
self._serve_html()
return
if path.startswith("/stream/"):
self._handle_stream(path)
return
@app.route("/shm/<camera_id>/frame.jpg")
def shm_frame(camera_id):
frame_path = Path(app.config["shm_dir"]) / f"chicken_counter_{camera_id}" / "frame.jpg"
if not frame_path.exists():
return jsonify({"error": "not found"}), 404
try:
return send_file(frame_path, mimetype="image/jpeg", max_age=2, download_name=None)
except (FileNotFoundError, PermissionError):
return jsonify({"error": "not ready"}), 503
if path == "/api/cameras":
self._send_json({"cameras": _discover_cameras(self.shm_dir)})
return
if path.startswith("/api/db/"):
self._handle_db(path)
return
def _get_db():
db = app.config["db_path"]
if not db or not Path(db).exists():
return None
conn = sqlite3.connect(db)
conn.row_factory = sqlite3.Row
return conn
if path.startswith("/shm/"):
self._handle_shm(path)
return
self._send_error(404)
@app.route("/api/db/summary")
def db_summary():
conn = _get_db()
if not conn:
return jsonify({})
row = conn.execute("""
SELECT COUNT(DISTINCT date) AS days,
COUNT(DISTINCT location) AS locations,
COUNT(*) AS total_runs,
SUM(total_entered) AS total_chickens,
ROUND(SUM(elapsed_seconds)/3600.0, 1) AS total_hours
FROM batch_runs
""").fetchone()
return jsonify(dict(row))
def _handle_stream(self, path):
cam_id = path[len("/stream/"):]
frame_path = Path(self.shm_dir) / f"chicken_counter_{cam_id}" / "frame.jpg"
if not frame_path.exists():
self._send_error(404)
return
self.send_response(200)
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
@app.route("/api/db/history")
def db_history():
conn = _get_db()
if not conn:
return jsonify([])
rows = conn.execute("""
SELECT date, location,
COUNT(*) AS cams,
SUM(total_entered) AS total,
ROUND(SUM(elapsed_seconds)/60.0, 1) AS minutes
FROM batch_runs
GROUP BY date, location
ORDER BY date DESC, location
LIMIT 50
""").fetchall()
return jsonify([dict(r) for r in rows])
last_mtime = 0
try:
while True:
try:
mtime = frame_path.stat().st_mtime
if mtime != last_mtime:
last_mtime = mtime
data = frame_path.read_bytes()
self.wfile.write(
b"--frame\r\n"
b"Content-Type: image/jpeg\r\n"
b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n" +
data + b"\r\n"
)
self.wfile.flush()
except (FileNotFoundError, OSError):
pass
time.sleep(0.1)
except (BrokenPipeError, ConnectionResetError):
pass
def _handle_shm(self, path):
rel = path[len("/shm/"):]
parts = rel.split("/", 1)
if len(parts) < 2:
self._send_error(404)
return
@app.route("/api/db/date/<date>")
def db_date(date):
conn = _get_db()
if not conn:
return jsonify({})
cameras = conn.execute("""
SELECT camera_id, total_entered, frames_processed,
ROUND(elapsed_seconds,1) AS elapsed_seconds,
stopped_reason, source_video, location
FROM batch_runs WHERE date=? ORDER BY camera_id
""", (date,)).fetchall()
total = conn.execute(
"SELECT SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes "
"FROM batch_runs WHERE date=?", (date,)).fetchone()
return jsonify({"date": date, "total": dict(total), "cameras": [dict(r) for r in cameras]})
cam_id = parts[0]
file = parts[1]
fpath = Path(self.shm_dir) / f"chicken_counter_{cam_id}" / file
if str(fpath.resolve()).startswith(str(Path(self.shm_dir).resolve())):
if fpath.exists():
ct = "image/jpeg" if file.endswith(".jpg") else "application/json"
self.send_response(200)
self.send_header("Content-Type", ct)
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.end_headers()
self.wfile.write(fpath.read_bytes())
return
self._send_error(404)
@app.route("/api/db/camera/<camera_id>")
def db_camera(camera_id):
conn = _get_db()
if not conn:
return jsonify([])
rows = conn.execute("""
SELECT date, location, total_entered, frames_processed,
ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason
FROM batch_runs WHERE camera_id=? ORDER BY date DESC LIMIT 50
""", (camera_id,)).fetchall()
return jsonify([dict(r) for r in rows])
def _handle_db(self, path):
conn = _get_db()
if not conn:
self._send_json({})
return
if path == "/api/db/summary":
row = conn.execute("SELECT COUNT(DISTINCT date) AS days, COUNT(DISTINCT location) AS locations, COUNT(*) AS total_runs, SUM(total_entered) AS total_chickens, ROUND(SUM(elapsed_seconds)/3600.0,1) AS total_hours FROM batch_runs").fetchone()
self._send_json(dict(row))
return
@app.route("/api/db/location/<location>")
def db_location(location):
conn = _get_db()
if not conn:
return jsonify({})
history = conn.execute("""
SELECT date, GROUP_CONCAT(camera_id,', ') AS cameras,
SUM(total_entered) AS total,
ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes
FROM batch_runs WHERE location=? GROUP BY date ORDER BY date DESC LIMIT 50
""", (location,)).fetchall()
summary = conn.execute("""
SELECT COUNT(DISTINCT date) AS days, SUM(total_entered) AS total,
ROUND(SUM(elapsed_seconds)/3600.0,1) AS hours
FROM batch_runs WHERE location=?
""", (location,)).fetchone()
return jsonify({"location": location, "summary": dict(summary), "history": [dict(r) for r in history]})
if path == "/api/db/history":
rows = conn.execute("SELECT date, location, COUNT(*) AS cams, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs GROUP BY date, location ORDER BY date DESC, location LIMIT 50").fetchall()
self._send_json([dict(r) for r in rows])
return
# /api/db/date/<date>
prefix = "/api/db/date/"
if path.startswith(prefix):
date = path[len(prefix):]
cameras = conn.execute("SELECT camera_id, total_entered, frames_processed, ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason, source_video, location FROM batch_runs WHERE date=? ORDER BY camera_id", (date,)).fetchall()
total = conn.execute("SELECT SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs WHERE date=?", (date,)).fetchone()
self._send_json({"date": date, "total": dict(total), "cameras": [dict(r) for r in cameras]})
return
# /api/db/camera/<id>
prefix = "/api/db/camera/"
if path.startswith(prefix):
cam_id = path[len(prefix):]
rows = conn.execute("SELECT date, location, total_entered, frames_processed, ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason FROM batch_runs WHERE camera_id=? ORDER BY date DESC LIMIT 50", (cam_id,)).fetchall()
self._send_json([dict(r) for r in rows])
return
# /api/db/location/<name>
prefix = "/api/db/location/"
if path.startswith(prefix):
loc = path[len(prefix):]
history = conn.execute("SELECT date, GROUP_CONCAT(camera_id,', ') AS cameras, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs WHERE location=? GROUP BY date ORDER BY date DESC LIMIT 50", (loc,)).fetchall()
summary = conn.execute("SELECT COUNT(DISTINCT date) AS days, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/3600.0,1) AS hours FROM batch_runs WHERE location=?", (loc,)).fetchone()
self._send_json({"location": loc, "summary": dict(summary), "history": [dict(r) for r in history]})
return
self._send_json({})
def _serve_html(self):
html_path = TEMPLATE_DIR / "index.html"
html = html_path.read_text(encoding="utf-8")
html = html.replace("{{ poll_ms }}", str(self.poll_ms))
html = html.replace("{{ shm_dir }}", self.shm_dir)
html = html.replace("{{ date }}", self.run_date or "today")
html = html.replace("{{ db_path }}", _db_path)
data = html.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _send_json(self, obj):
data = json.dumps(obj).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _send_error(self, code):
self.send_response(code)
self.send_header("Content-Length", "0")
self.end_headers()
def main():
global _db_path
parser = argparse.ArgumentParser(description="Chicken Counter live dashboard")
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--shm-dir", default=DEFAULT_SHM_DIR)
parser.add_argument("--poll-ms", type=int, default=500)
parser.add_argument("--date", default="", help="Processing date")
parser.add_argument("--db", default="chicken_counts.db", help="SQLite database path")
parser.add_argument("--poll-ms", type=int, default=1000)
parser.add_argument("--date", default="")
parser.add_argument("--db", default="db/chicken_counts.db")
args = parser.parse_args()
app.config["shm_dir"] = args.shm_dir
app.config["poll_ms"] = args.poll_ms
app.config["run_date"] = args.date
app.config["db_path"] = str(Path(args.db).resolve()) if args.db else ""
DashboardHandler.shm_dir = args.shm_dir
DashboardHandler.poll_ms = args.poll_ms
DashboardHandler.run_date = args.date
_db_path = str(Path(args.db).resolve()) if args.db else ""
if app.config["db_path"]:
_init_db(app.config["db_path"])
if _db_path:
_init_db(_db_path)
print(f"[dashboard] http://0.0.0.0:{args.port} shm={args.shm_dir} db={args.db}" + (f" date={args.date}" if args.date else ""))
serve(app, host="0.0.0.0", port=args.port, threads=8)
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
date_info = f" date={args.date}" if args.date else ""
print(f"[dashboard] http://0.0.0.0:{args.port} shm={args.shm_dir} db={args.db}{date_info}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[dashboard] stopped")
server.server_close()
if __name__ == "__main__":
Binary file not shown.
Binary file not shown.
View File
Whitespace-only changes.
+30 -15
View File
@@ -69,6 +69,7 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
var POLL_MS = {{ poll_ms }};
var SHM = "{{ shm_dir }}";
var DB_PATH = "{{ db_path }}";
var RUN_DATE = "{{ date }}";
var cameras = [], activeCam = null, lastUpdate = 0;
var perCam = {}, lastFramePerCam = {};
@@ -114,6 +115,7 @@ function selectCam(id) {
document.getElementById("waiting-msg").style.display = "none";
document.getElementById("frame-overlay").style.display = "block";
document.getElementById("frame-img").style.display = "block";
document.getElementById("frame-img").src = "/stream/" + id;
renderCamList();
refreshNow();
}
@@ -124,7 +126,6 @@ function poll() {
fetch("/shm/" + activeCam + "/stats.json?t=" + t).then(r => r.ok ? r.json() : null).then(s => {
if (!s) { setOffline(); return; }
lastUpdate = Date.now();
var oldFrame = lastFramePerCam[activeCam] || 0;
lastFramePerCam[activeCam] = s.frame_index;
perCam[activeCam] = { total: s.total_entered_count, inside: s.inside_box_count, frame: s.frame_index, tracks: s.track_count, speed: s.smoothed_speed };
document.getElementById("s-total").textContent = s.total_entered_count;
@@ -137,18 +138,11 @@ function poll() {
tag.textContent = activeCam + " \u2022 " + status;
tag.className = "tag " + (s.backward_active ? "off" : "live");
document.getElementById("cam-count").textContent = s.total_entered_count;
// only refresh frame when index changes (every 5 frames with interval_frames=5)
if (s.frame_index != oldFrame) {
document.getElementById("frame-img").src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
}
});
}
function refreshNow() {
poll();
// also update the frame immediately
var t = Date.now();
document.getElementById("frame-img").src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
}
function showWaiting(msg) {
@@ -163,6 +157,8 @@ function setOffline() {
document.getElementById("cam-tag").className = "tag off";
}
function updateSidebar() {}
function renderCamList() {
var ul = document.getElementById("cam-list");
ul.innerHTML = cameras.map(function(c) {
@@ -180,15 +176,34 @@ function updateClock() {
function loadHistory() {
if (!DB_PATH) return;
var panel = document.getElementById("db-panel");
panel.style.display = "block";
// per-day totals
fetch("/api/db/history").then(r => r.json()).then(rows => {
var panel = document.getElementById("db-panel");
if (!rows.length) { panel.style.display = "none"; return; }
panel.style.display = "block";
panel.innerHTML = "<h3>&#x1f4ca; History</h3>" + rows.map(function(r) {
return '<div class="db-row"><span>' + r.date + ' ' + r.location + '</span>' +
'<span class="db-total">' + r.total.toLocaleString() + '</span></div>';
}).join("");
var html = "<h3>&#x1f4ca; History</h3>";
if (rows.length) {
html += rows.map(function(r) {
return '<div class="db-row"><span>' + r.date + ' ' + r.location + '</span>' +
'<span class="db-total">' + r.total.toLocaleString() + '</span></div>';
}).join("");
}
panel.innerHTML = html;
});
// per-camera breakdown for current date
if (RUN_DATE && RUN_DATE !== "today") {
fetch("/api/db/date/" + RUN_DATE).then(r => r.json()).then(data => {
if (!data.cameras || !data.cameras.length) return;
var html = panel.innerHTML;
html += '<h3 style="margin-top:12px">&#x1f4f9; ' + RUN_DATE + ' (' + (data.total.total || 0).toLocaleString() + ')</h3>';
data.cameras.forEach(function(c) {
html += '<div class="db-row"><span>' + c.camera_id + '</span>' +
'<span class="db-total">' + c.total_entered.toLocaleString() + '</span></div>';
});
panel.innerHTML = html;
});
}
}
setTimeout(function() { if (activeCam) refreshNow(); }, 500);