654 lines
22 KiB
Python
654 lines
22 KiB
Python
from flask import Flask, request, jsonify
|
|
import numpy as np
|
|
import cv2
|
|
import os
|
|
from ultralytics import YOLO
|
|
import tflite_runtime.interpreter as tflite
|
|
import mysql.connector
|
|
from mysql.connector import Error
|
|
from datetime import datetime
|
|
from collections import defaultdict
|
|
from flask_cors import CORS
|
|
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
# ===== LOAD MODEL =====
|
|
yolo = YOLO("best.pt")
|
|
interpreter = tflite.Interpreter(model_path="model_telur_cnn.tflite")
|
|
interpreter.allocate_tensors()
|
|
|
|
input_details = interpreter.get_input_details()
|
|
output_details = interpreter.get_output_details()
|
|
|
|
labels = ['A', 'B', 'C', 'TL']
|
|
|
|
# ===== KONFIGURASI DB =====
|
|
DB_CONFIG = {
|
|
"host": "10.10.1.112",
|
|
"user": "admin",
|
|
"password": "Telur@12344321",
|
|
"database": "klasifikasi_telur",
|
|
"init_command": "SET time_zone = '+07:00'"
|
|
}
|
|
|
|
def get_db():
|
|
return mysql.connector.connect(**DB_CONFIG)
|
|
|
|
# ===== HELPER =====
|
|
def map_grade(g: str) -> str:
|
|
if not g:
|
|
return "TL"
|
|
g = g.strip().upper()
|
|
if g == "A": return "A"
|
|
if g == "B": return "B"
|
|
if g == "C": return "C"
|
|
return "TL"
|
|
|
|
def period_to_sql(period: str):
|
|
if period == "today":
|
|
return "DATE(created_at) = CURDATE()", "24 HOUR"
|
|
elif period == "week":
|
|
return "created_at >= NOW() - INTERVAL 7 DAY", "7 DAY"
|
|
elif period == "month":
|
|
return "created_at >= NOW() - INTERVAL 30 DAY", "30 DAY"
|
|
else:
|
|
return "1=1", "3650 DAY"
|
|
|
|
# ===== CNN =====
|
|
def predict_cnn(img):
|
|
img_r = cv2.resize(img, (224, 224))
|
|
img_r = img_r.astype(np.float32) / 255.0
|
|
img_r = np.expand_dims(img_r, axis=0)
|
|
|
|
interpreter.set_tensor(input_details[0]['index'], img_r)
|
|
interpreter.invoke()
|
|
|
|
output = interpreter.get_tensor(output_details[0]['index'])[0]
|
|
idx = int(np.argmax(output))
|
|
conf = float(np.max(output))
|
|
return labels[idx], conf
|
|
|
|
# ===== YOLO =====
|
|
def predict_yolo(img):
|
|
results = yolo(img)
|
|
for r in results:
|
|
if r.boxes is not None and len(r.boxes) > 0:
|
|
cls = int(r.boxes.cls[0])
|
|
conf = float(r.boxes.conf[0])
|
|
return labels[cls], conf
|
|
return "TL", 0.0
|
|
|
|
def fusion(yolo_pred, yolo_conf, cnn_pred, cnn_conf):
|
|
# kalau dua-duanya sama, langsung pakai itu
|
|
if yolo_pred == cnn_pred:
|
|
return yolo_pred
|
|
|
|
# pilih berdasarkan confidence tertinggi
|
|
if cnn_conf > yolo_conf:
|
|
return cnn_pred
|
|
else:
|
|
return yolo_pred
|
|
|
|
# ===== FINAL DECISION (AI + BERAT) =====
|
|
# FIX: hanya satu definisi, logika berat yang dipakai
|
|
def final_decision(ai_grade, weight):
|
|
# tentukan grade dari berat
|
|
if weight < 40:
|
|
weight_grade = "TL"
|
|
elif weight <= 49:
|
|
weight_grade = "C"
|
|
elif weight <= 59:
|
|
weight_grade = "B"
|
|
else:
|
|
weight_grade = "A"
|
|
|
|
# ranking (semakin kecil = semakin jelek)
|
|
priority = {"TL": 0, "C": 1, "B": 2, "A": 3}
|
|
|
|
# ambil yang PALING RENDAH
|
|
return ai_grade if priority[ai_grade] < priority[weight_grade] else weight_grade
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# POST /upload — dipanggil ESP32-CAM setelah ambil foto
|
|
# Alur: simpan gambar → predict AI → insert telur (berat=0) → insert gambar,
|
|
# cnn_result, yolo_result → kembalikan telur_id ke CAM
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@app.route('/upload', methods=['POST'])
|
|
def upload():
|
|
file = request.files.get('image')
|
|
|
|
filename = datetime.now().strftime("%Y%m%d%H%M%S") + ".jpg"
|
|
os.makedirs("uploads", exist_ok=True)
|
|
path = os.path.join("uploads", filename)
|
|
|
|
if file:
|
|
file.save(path)
|
|
img = cv2.imread(path)
|
|
else:
|
|
data = request.get_data()
|
|
npimg = np.frombuffer(data, np.uint8)
|
|
img = cv2.imdecode(npimg, cv2.IMREAD_COLOR)
|
|
if img is None:
|
|
return jsonify({"error": "Image tidak valid"}), 400
|
|
cv2.imwrite(path, img)
|
|
|
|
yolo_pred, yolo_conf = predict_yolo(img)
|
|
cnn_pred, cnn_conf = predict_cnn(img)
|
|
ai_grade = fusion(yolo_pred, yolo_conf, cnn_pred, cnn_conf)
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
# Insert telur dulu dengan berat=0, grade sementara dari AI
|
|
# Berat & grade final akan diupdate oleh DevKit via /updateFinal
|
|
cursor.execute(
|
|
"INSERT INTO telur (grade, berat) VALUES (%s, %s)",
|
|
(ai_grade, 0)
|
|
)
|
|
telur_id = cursor.lastrowid
|
|
|
|
cursor.execute(
|
|
"INSERT INTO gambar (telur_id, path) VALUES (%s, %s)",
|
|
(telur_id, path)
|
|
)
|
|
gambar_id = cursor.lastrowid
|
|
|
|
cursor.execute(
|
|
"INSERT INTO yolo_result (gambar_id, grade, confidence) VALUES (%s, %s, %s)",
|
|
(gambar_id, yolo_pred, yolo_conf)
|
|
)
|
|
cursor.execute(
|
|
"INSERT INTO cnn_result (gambar_id, grade, confidence) VALUES (%s, %s, %s)",
|
|
(gambar_id, cnn_pred, cnn_conf)
|
|
)
|
|
|
|
conn.commit()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# FIX: kembalikan "telur_id" agar konsisten dengan semua endpoint lain
|
|
return jsonify({"telur_id":telur_id,"grade":ai_grade,"yolo":yolo_pred,"yolo_conf": round(yolo_conf, 3),"cnn":cnn_pred,"cnn_conf":round(cnn_conf, 3),
|
|
})
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# GET /latest — dipanggil DevKit untuk polling hasil foto terbaru
|
|
# FIX: kembalikan key "telur_id" (bukan "id") agar DevKit bisa parse
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@app.route('/latest', methods=['GET'])
|
|
def latest():
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
cursor.execute("""
|
|
SELECT id, grade, berat, created_at
|
|
FROM telur
|
|
WHERE berat = 0
|
|
ORDER BY id DESC
|
|
LIMIT 1
|
|
""")
|
|
row = cursor.fetchone()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
if not row:
|
|
return jsonify({"telur_id": 0, "berat": 0, "grade": "TL", "timestamp": "-"})
|
|
|
|
return jsonify({
|
|
"telur_id": row["id"], # FIX: pakai key "telur_id"
|
|
"berat": float(row["berat"]) if row["berat"] else 0,
|
|
"grade": map_grade(row["grade"] or "TL"),
|
|
"timestamp": row["created_at"].strftime("%H:%M") if row["created_at"] else "-",
|
|
})
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# POST /updateFinal — dipanggil DevKit dengan berat aktual
|
|
# Server hitung grade final (AI + berat) lalu update tabel telur
|
|
# FIX: indentasi try/except diperbaiki, masuk ke dalam fungsi
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@app.route('/updateFinal', methods=['POST'])
|
|
def update_final():
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "No data"}), 400
|
|
|
|
telur_id = data.get("id")
|
|
berat = float(data.get("berat", 0))
|
|
|
|
if not telur_id:
|
|
return jsonify({"error": "id tidak ada"}), 400
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
# Ambil AI grade yang sudah tersimpan saat /upload
|
|
cursor.execute("SELECT grade FROM telur WHERE id=%s", (telur_id,))
|
|
row = cursor.fetchone()
|
|
|
|
ai_grade = row[0] if row else "TL"
|
|
|
|
# Hitung grade final: kombinasi AI + berat
|
|
final = final_decision(ai_grade, berat)
|
|
|
|
cursor.execute(
|
|
"UPDATE telur SET berat=%s, grade=%s WHERE id=%s",
|
|
(berat, final, telur_id)
|
|
)
|
|
conn.commit()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
print(f"[updateFinal] id={telur_id}, berat={berat}, ai={ai_grade}, final={final}")
|
|
|
|
return jsonify({"final": final, "berat": round(berat, 2)})
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# GET /telur — list semua telur untuk dashboard
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@app.route('/telur', methods=['GET'])
|
|
def get_telur():
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
cursor.execute("""
|
|
SELECT id, grade, berat, created_at
|
|
FROM telur
|
|
ORDER BY id DESC
|
|
LIMIT 500
|
|
""")
|
|
rows = cursor.fetchall()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
result = []
|
|
for row in rows:
|
|
result.append({
|
|
"id": row["id"],
|
|
"grade": map_grade(row["grade"] or "TL"),
|
|
"weight": float(row["berat"]) if row["berat"] else 0,
|
|
"timestamp": row["created_at"].strftime("%H:%M") if row["created_at"] else "-",
|
|
"date": row["created_at"].strftime("%d/%m/%Y") if row["created_at"] else "-",
|
|
})
|
|
|
|
return jsonify(result)
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# GET /summary?period=today|week|month|all
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@app.route('/summary', methods=['GET'])
|
|
def summary():
|
|
period = request.args.get("period", "today")
|
|
if period not in ("today", "week", "month", "all"):
|
|
period = "today"
|
|
|
|
where_clause, history_interval = period_to_sql(period)
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
cursor.execute(f"""
|
|
SELECT
|
|
grade,
|
|
COUNT(*) AS jumlah,
|
|
AVG(berat) AS rata_berat
|
|
FROM telur
|
|
WHERE {where_clause}
|
|
GROUP BY grade
|
|
""")
|
|
grade_rows = cursor.fetchall()
|
|
|
|
result = {
|
|
"gradeA": {"count": 0, "avgWeight": 0},
|
|
"gradeB": {"count": 0, "avgWeight": 0},
|
|
"gradeC": {"count": 0, "avgWeight": 0},
|
|
"gradeTL": {"count": 0, "avgWeight": 0},
|
|
}
|
|
for row in grade_rows:
|
|
g = map_grade(row["grade"] or "TL")
|
|
key = f"grade{g}"
|
|
if key in result:
|
|
result[key]["count"] = int(row["jumlah"])
|
|
result[key]["avgWeight"] = round(float(row["rata_berat"] or 0), 1)
|
|
|
|
if period in ("today", "week"):
|
|
slot_expr = """DATE_FORMAT(
|
|
DATE_SUB(created_at, INTERVAL MOD(MINUTE(created_at), 30) MINUTE),
|
|
'%%H:%%i'
|
|
)"""
|
|
else:
|
|
slot_expr = "DATE_FORMAT(created_at, '%%d %%b')"
|
|
|
|
cursor.execute(f"""
|
|
SELECT
|
|
{slot_expr} AS slot,
|
|
grade,
|
|
COUNT(*) AS jumlah
|
|
FROM telur
|
|
WHERE created_at >= NOW() - INTERVAL {history_interval}
|
|
GROUP BY slot, grade
|
|
ORDER BY slot ASC
|
|
""")
|
|
hist_rows = cursor.fetchall()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
hist_map = defaultdict(lambda: {"A": 0, "B": 0, "C": 0, "TL": 0})
|
|
for row in hist_rows:
|
|
g = map_grade(row["grade"] or "TL")
|
|
hist_map[row["slot"]][g] += int(row["jumlah"])
|
|
|
|
history = [
|
|
{"time": slot, "A": v["A"], "B": v["B"], "C": v["C"], "TL": v["TL"]}
|
|
for slot, v in sorted(hist_map.items())
|
|
]
|
|
|
|
return jsonify({**result, "history": history})
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# GET /getSpeed & POST /setSpeed
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@app.route('/getSpeed', methods=['GET'])
|
|
def get_speed():
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
cursor.execute("SELECT conveyor_speed FROM settings LIMIT 1")
|
|
row = cursor.fetchone()
|
|
cursor.close()
|
|
conn.close()
|
|
return jsonify({"speed": row["conveyor_speed"] if row else 200})
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/setSpeed', methods=['POST'])
|
|
def set_speed():
|
|
data = request.get_json()
|
|
speed = int(data["speed"])
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute("UPDATE settings SET conveyor_speed=%s WHERE id=1", (speed,))
|
|
conn.commit()
|
|
cursor.close()
|
|
conn.close()
|
|
return jsonify({"status": "ok", "speed": speed})
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# POST /login & POST /register
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@app.route('/login', methods=['POST'])
|
|
def login():
|
|
data = request.get_json()
|
|
username = data.get('username')
|
|
password = data.get('password')
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
cursor.execute(
|
|
"SELECT * FROM users WHERE username = %s AND password = %s",
|
|
(username, password)
|
|
)
|
|
user = cursor.fetchone()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
if user:
|
|
return jsonify({"success": True, "username": user["username"], "nama": user["nama"], "role": user["role"]})
|
|
return jsonify({"success": False, "message": "Username atau password salah"}), 401
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.route('/register', methods=['POST'])
|
|
def register():
|
|
data = request.get_json()
|
|
nama = data.get('nama')
|
|
username = data.get('username')
|
|
password = data.get('password')
|
|
role = data.get('role', 'user')
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
|
|
existing = cursor.fetchone()
|
|
if existing:
|
|
cursor.close()
|
|
conn.close()
|
|
return jsonify({"success": False, "message": "Username sudah dipakai"}), 400
|
|
|
|
cursor.execute(
|
|
"INSERT INTO users (nama, username, password, role) VALUES (%s, %s, %s, %s)",
|
|
(nama, username, password, role)
|
|
)
|
|
conn.commit()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
return jsonify({"success": True, "message": "Registrasi berhasil"})
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# POST /order — simpan transaksi (orders + order_items)
|
|
# ─────────────────────────────────────────────────────────────
|
|
@app.route('/order', methods=['POST'])
|
|
def create_order():
|
|
data = request.get_json()
|
|
cart = data.get("cart", [])
|
|
nama = data.get("nama", "guest")
|
|
|
|
harga_map = {
|
|
"A": 1500,
|
|
"B": 1200,
|
|
"C": 900
|
|
}
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
total = 0
|
|
|
|
# 🔥 CEK STOK DULU (BIAR GA MINUS)
|
|
for item in cart:
|
|
cursor.execute(
|
|
"SELECT COUNT(*) FROM telur WHERE grade = %s",
|
|
(item["grade"],)
|
|
)
|
|
stok = cursor.fetchone()[0]
|
|
|
|
if stok < item["qty"]:
|
|
return jsonify({
|
|
"success": False,
|
|
"message": f"Stok grade {item['grade']} tidak cukup"
|
|
})
|
|
|
|
# 🔥 HITUNG TOTAL
|
|
for item in cart:
|
|
total += harga_map[item["grade"]] * item["qty"]
|
|
|
|
# 🔥 INSERT KE ORDERS
|
|
cursor.execute(
|
|
"INSERT INTO orders (nama, total) VALUES (%s, %s)",
|
|
(nama, total)
|
|
)
|
|
|
|
order_id = cursor.lastrowid
|
|
|
|
# 🔥 INSERT ITEMS + HAPUS STOK
|
|
for item in cart:
|
|
grade = item["grade"]
|
|
qty = item["qty"]
|
|
harga = harga_map[grade]
|
|
|
|
# insert item
|
|
cursor.execute(
|
|
"INSERT INTO order_items (order_id, grade, qty, harga) VALUES (%s,%s,%s,%s)",
|
|
(order_id, grade, qty, harga)
|
|
)
|
|
|
|
# 🔥 HAPUS STOK (INI INTI NYA)
|
|
cursor.execute("""
|
|
DELETE FROM telur
|
|
WHERE grade = %s
|
|
ORDER BY id ASC
|
|
LIMIT %s
|
|
""", (grade, qty))
|
|
|
|
conn.commit()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
print(f"[ORDER] sukses id={order_id}, total={total}")
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"order_id": order_id
|
|
})
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 📦 GET ALL ORDERS (ADMIN / HISTORY)
|
|
# ─────────────────────────────────────────────
|
|
@app.route('/orders', methods=['GET'])
|
|
def get_orders():
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor(dictionary=True)
|
|
|
|
cursor.execute("""
|
|
SELECT o.id, o.nama, o.total, o.status, o.bukti_pembayaran, o.created_at
|
|
FROM orders o
|
|
ORDER BY o.id DESC
|
|
""")
|
|
|
|
orders = cursor.fetchall()
|
|
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
return jsonify(orders)
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 🔄 UPDATE STATUS ORDER (ADMIN APPROVE)
|
|
# ─────────────────────────────────────────────
|
|
@app.route('/order/status', methods=['POST'])
|
|
def update_status():
|
|
data = request.json
|
|
order_id = data.get("order_id")
|
|
status = data.get("status")
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"UPDATE orders SET status=%s WHERE id=%s",
|
|
(status, order_id)
|
|
)
|
|
|
|
conn.commit()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
return jsonify({"success": True})
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 📤 UPLOAD BUKTI PEMBAYARAN
|
|
# ─────────────────────────────────────────────
|
|
import os
|
|
from werkzeug.utils import secure_filename
|
|
from datetime import datetime
|
|
from flask import send_from_directory
|
|
|
|
UPLOAD_FOLDER_BUKTI = 'uploads_bukti'
|
|
os.makedirs(UPLOAD_FOLDER_BUKTI, exist_ok=True)
|
|
|
|
@app.route('/upload-bukti', methods=['POST'])
|
|
def upload_bukti():
|
|
file = request.files.get('file')
|
|
order_id = request.form.get('order_id')
|
|
|
|
if not file or not order_id:
|
|
return jsonify({"success": False, "message": "File / order_id kosong"})
|
|
|
|
filename = datetime.now().strftime("%Y%m%d%H%M%S_") + secure_filename(file.filename)
|
|
filepath = os.path.join(UPLOAD_FOLDER_BUKTI, filename)
|
|
|
|
file.save(filepath)
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"UPDATE orders SET bukti_pembayaran=%s, status='menunggu_verifikasi' WHERE id=%s",
|
|
(filename, order_id)
|
|
)
|
|
|
|
conn.commit()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
print(f"[UPLOAD BUKTI] order_id={order_id}, file={filename}")
|
|
|
|
return jsonify({"success": True})
|
|
|
|
except Error as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 🖼️ AKSES FILE BUKTI PEMBAYARAN
|
|
# ─────────────────────────────────────────────
|
|
@app.route('/uploads-bukti/<filename>')
|
|
def get_bukti(filename):
|
|
return send_from_directory(UPLOAD_FOLDER_BUKTI, filename)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=5000, debug=True) |