Add new file

parents
# Captura video de 2 camaras + JSON con parametros de Moonraker
# - Guarda frames temporalmente, genera video y los borra al finalizar
# Uso: ~/klipper_capture/venv/bin/python3 capture_session.py
import urllib.request
import requests
import json
import os
import time
import threading
import subprocess
import shutil
from datetime import datetime, timezone
# ---------------------------------------------------------------
# CONFIGURACION
# ---------------------------------------------------------------
PI_IP = "127.0.0.1"
CAM1_PORT = 8080
CAM2_PORT = 8081
MOONRAKER_PORT = 7125
INTERVAL_S = 0.5
OUTPUT_DIR = "/home/pi/printer_data/captures"
# Resolucion: configurar en ~/printer_data/config/crowsnest.conf
# linea "resolution: 640x480" de cada camara y reiniciar crowsnest
# ---------------------------------------------------------------
MOONRAKER_BASE = "http://" + PI_IP + ":" + str(MOONRAKER_PORT)
QUERY = (
"extruder&heater_bed&fan&toolhead&motion_report&print_stats"
"&gcode_move&system_stats&idle_timeout&webhooks"
"&temperature_sensor%20einsy_board"
"&temperature_host%20raspberry_pi"
"&temperature_sensor%20raspberry_pi"
"&filament_switch_sensor%20filament_sensor"
"&tmc2130%20stepper_x&tmc2130%20stepper_y"
"&tmc2130%20stepper_z&tmc2130%20extruder"
"&firmware_retraction&virtual_sdcard"
"&pause_resume&display_status&exclude_object"
"&save_variables&stepper_enable&mcu"
"&heater_fan%20nozzle_cooling_fan&probe"
)
cam1 = {"jpeg": None, "timestamp_us": None, "lock": threading.Lock()}
cam2 = {"jpeg": None, "timestamp_us": None, "lock": threading.Lock()}
stop_event = threading.Event()
# ---------------------------------------------------------------
# HELPERS
# ---------------------------------------------------------------
def r(val, decimals=2):
if val is None:
return None
try:
return round(float(val), decimals)
except Exception:
return val
def pct(val):
if val is None:
return None
try:
return round(float(val) * 100, 1)
except Exception:
return val
def safe(d, *keys, default=None):
for k in keys:
if not isinstance(d, dict):
return default
d = d.get(k, default)
return d
def seconds_to_timecode(s):
total_ms = int(round(s * 1000))
ms = total_ms % 1000
sec = (total_ms // 1000) % 60
mn = (total_ms // 1000) // 60
return str(mn) + ":" + str(sec).zfill(2) + "." + str(ms).zfill(3)
# ---------------------------------------------------------------
# HILO DE CAMARA
# ---------------------------------------------------------------
def camera_thread(port, buf):
url = "http://" + PI_IP + ":" + str(port) + "/?action=stream"
while not stop_event.is_set():
try:
with urllib.request.urlopen(url, timeout=10) as stream:
buffer = b""
timestamp_us = None
while not stop_event.is_set():
chunk = stream.read(4096)
if not chunk:
break
buffer += chunk
if b"X-Timestamp:" in buffer and timestamp_us is None:
try:
s = buffer.index(b"X-Timestamp:") + len(b"X-Timestamp:")
e = buffer.index(b"\r\n", s)
timestamp_us = int(buffer[s:e].strip())
except Exception:
pass
start = buffer.find(b"\xff\xd8")
end = buffer.find(b"\xff\xd9")
if start != -1 and end != -1 and end > start:
jpeg = buffer[start:end + 2]
with buf["lock"]:
buf["jpeg"] = jpeg
buf["timestamp_us"] = timestamp_us
buffer = buffer[end + 2:]
timestamp_us = None
if len(buffer) > 500000:
buffer = buffer[-100000:]
except Exception as e:
if not stop_event.is_set():
print("[cam:" + str(port) + "] Error: " + str(e) + " - reconectando en 2s")
time.sleep(2)
# ---------------------------------------------------------------
# MOONRAKER
# ---------------------------------------------------------------
def get_printer_data():
try:
r_http = requests.get(
MOONRAKER_BASE + "/printer/objects/query?" + QUERY,
timeout=1.0
)
if r_http.status_code != 200:
return {}
raw = r_http.json()
status = safe(raw, "result", "status", default={})
ext = status.get("extruder", {})
bed = status.get("heater_bed", {})
fan = status.get("fan", {})
th = status.get("toolhead", {})
mr_s = status.get("motion_report", {})
ps = status.get("print_stats", {})
gcm = status.get("gcode_move", {})
idle = status.get("idle_timeout", {})
wh = status.get("webhooks", {})
einsy = status.get("temperature_sensor einsy_board", {})
pi_th = status.get("temperature_host raspberry_pi", {})
pi_ts = status.get("temperature_sensor raspberry_pi", {})
fil = status.get("filament_switch_sensor filament_sensor", {})
tmcx = status.get("tmc2130 stepper_x", {})
tmcy = status.get("tmc2130 stepper_y", {})
tmcz = status.get("tmc2130 stepper_z", {})
tmce = status.get("tmc2130 extruder", {})
fret = status.get("firmware_retraction", {})
vsd = status.get("virtual_sdcard", {})
pr = status.get("pause_resume", {})
ds = status.get("display_status", {})
eo = status.get("exclude_object", {})
sv = status.get("save_variables", {})
steppers = status.get("stepper_enable", {})
mcu = status.get("mcu", {})
nozzle_fan = status.get("heater_fan nozzle_cooling_fan", {})
probe = status.get("probe", {})
sys_stats = status.get("system_stats", {})
try:
import psutil
cpu_pct = psutil.cpu_percent()
mem_mb = round(psutil.virtual_memory().used / 1024 / 1024, 1)
disk_free = round(psutil.disk_usage("/").free / 1024 / 1024 / 1024, 2)
cpu_temp = None
temps = psutil.sensors_temperatures()
if "cpu_thermal" in temps:
cpu_temp = round(temps["cpu_thermal"][0].current, 1)
except Exception:
cpu_pct = cpu_temp = mem_mb = disk_free = None
lp = mr_s.get("live_position", [])
pos_x = r(lp[0], 3) if len(lp) > 0 else None
pos_y = r(lp[1], 3) if len(lp) > 1 else None
pos_z = r(lp[2], 3) if len(lp) > 2 else None
pos_e = r(lp[3], 3) if len(lp) > 3 else None
gp = gcm.get("gcode_position", [])
gx = r(gp[0], 3) if len(gp) > 0 else None
gy = r(gp[1], 3) if len(gp) > 1 else None
gz = r(gp[2], 3) if len(gp) > 2 else None
return {
"estado": {
"state": ps.get("state"),
"filename": ps.get("filename"),
"print_duration_s": r(ps.get("print_duration"), 1),
"total_duration_s": r(ps.get("total_duration"), 1),
"filament_used_mm": r(ps.get("filament_used")),
"current_layer": safe(ps, "info", "current_layer"),
"total_layers": safe(ps, "info", "total_layer"),
"message": ps.get("message"),
"is_paused": pr.get("is_paused"),
"display_message": ds.get("message"),
"was_interrupted": safe(sv, "variables", "was_interrupted"),
"current_object": eo.get("current_object"),
"excluded_objects": eo.get("excluded_objects"),
},
"hotend": {
"temp_c": r(ext.get("temperature")),
"target_c": r(ext.get("target"), 1),
"power_pct": pct(ext.get("power")),
"can_extrude": ext.get("can_extrude"),
"pressure_advance": r(ext.get("pressure_advance"), 4),
"smooth_time": r(ext.get("smooth_time"), 4),
},
"cama": {
"temp_c": r(bed.get("temperature")),
"target_c": r(bed.get("target"), 1),
"power_pct": pct(bed.get("power")),
},
"temperaturas": {
"einsy_board_c": r(einsy.get("temperature")),
"einsy_min_c": r(einsy.get("measured_min_temp")),
"einsy_max_c": r(einsy.get("measured_max_temp")),
"pi_c": r(pi_th.get("temperature")),
"pi_min_c": r(pi_ts.get("measured_min_temp")),
"pi_max_c": r(pi_ts.get("measured_max_temp")),
"cpu_c": cpu_temp,
},
"posicion": {
"x_mm": pos_x,
"y_mm": pos_y,
"z_mm": pos_z,
"e_mm": pos_e,
"live_velocity_mms": r(mr_s.get("live_velocity")),
"live_extruder_vel_mms": r(mr_s.get("live_extruder_velocity")),
},
"movimiento": {
"speed_factor_pct": pct(gcm.get("speed_factor")),
"extrude_factor_pct": pct(gcm.get("extrude_factor")),
"gcode_x": gx,
"gcode_y": gy,
"gcode_z": gz,
"speed_mms": r(gcm.get("speed"), 1),
"absolute_coordinates": gcm.get("absolute_coordinates"),
"absolute_extrude": gcm.get("absolute_extrude"),
"max_velocity_mms": r(th.get("max_velocity"), 1),
"max_accel_mms2": r(th.get("max_accel"), 1),
"min_cruise_ratio": r(th.get("minimum_cruise_ratio"), 3),
"square_corner_vel_mms": r(th.get("square_corner_velocity"), 1),
"print_time_s": r(th.get("print_time"), 3),
"stalls": th.get("stalls"),
},
"ventiladores": {
"capa_speed_pct": pct(fan.get("speed")),
"nozzle_speed_pct": pct(nozzle_fan.get("speed")),
},
"drivers_tmc2130": {
"x_run_current_a": r(tmcx.get("run_current"), 4),
"x_hold_current_a": r(tmcx.get("hold_current"), 4),
"y_run_current_a": r(tmcy.get("run_current"), 4),
"y_hold_current_a": r(tmcy.get("hold_current"), 4),
"z_run_current_a": r(tmcz.get("run_current"), 4),
"z_hold_current_a": r(tmcz.get("hold_current"), 4),
"e_run_current_a": r(tmce.get("run_current"), 4),
"e_hold_current_a": r(tmce.get("hold_current"), 4),
},
"motores_habilitados": {
"stepper_x": steppers.get("stepper_x"),
"stepper_y": steppers.get("stepper_y"),
"stepper_z": steppers.get("stepper_z"),
"extruder": steppers.get("extruder"),
},
"filamento": {
"detectado": fil.get("filament_detected"),
"sensor_enabled": fil.get("enabled"),
"retract_length_mm": r(fret.get("retract_length"), 3),
"retract_speed_mms": r(fret.get("retract_speed"), 1),
"unretract_extra_mm": r(fret.get("unretract_extra_length"), 3),
"unretract_speed_mms": r(fret.get("unretract_speed"), 1),
},
"sdcard": {
"progress_pct": pct(vsd.get("progress")),
"file_position": vsd.get("file_position"),
"file_size": vsd.get("file_size"),
"is_active": vsd.get("is_active"),
},
"probe": {
"last_z_result": r(probe.get("last_z_result"), 4),
"last_query": probe.get("last_query"),
},
"mcu": {
"version": mcu.get("mcu_version"),
"freq_hz": safe(mcu, "mcu_stats", "freq"),
"mcu_awake_s": r(safe(mcu, "mcu_stats", "mcu_awake"), 4),
"mcu_task_avg_s": r(safe(mcu, "mcu_stats", "mcu_task_avg"), 6),
"mcu_task_stddev": r(safe(mcu, "mcu_stats", "mcu_task_stddev"), 6),
"bytes_write": safe(mcu, "mcu_stats", "bytes_write"),
"bytes_read": safe(mcu, "mcu_stats", "bytes_read"),
"bytes_retransmit": safe(mcu, "mcu_stats", "bytes_retransmit"),
"send_seq": safe(mcu, "mcu_stats", "send_seq"),
"receive_seq": safe(mcu, "mcu_stats", "receive_seq"),
"srtt_s": r(safe(mcu, "mcu_stats", "srtt"), 6),
"rto_s": r(safe(mcu, "mcu_stats", "rto"), 6),
},
"sistema": {
"cpu_pct": cpu_pct,
"mem_used_mb": mem_mb,
"disk_free_gb": disk_free,
"sysload": r(sys_stats.get("sysload"), 3),
},
"klipper": {
"state": wh.get("state"),
"idle_state": idle.get("state"),
"idle_timeout_s": r(idle.get("idle_timeout"), 1),
"printing_time_s": r(idle.get("printing_time"), 1),
},
}
except Exception as e:
print("[Moonraker] Error: " + str(e))
return {}
# ---------------------------------------------------------------
# GENERACION DE VIDEO con ffmpeg concat demuxer
# ---------------------------------------------------------------
def generate_video(frames_dir, output_path, records, cam_key):
if len(records) < 2:
print("[Video] No hay suficientes frames para " + cam_key)
return
concat_path = os.path.join(frames_dir, "concat.txt")
total_duration = 0.0
with open(concat_path, "w") as f:
for i in range(len(records) - 1):
rec1 = records[i]
rec2 = records[i + 1]
ts1 = rec1[cam_key]["timestamp_us"]
ts2 = rec2[cam_key]["timestamp_us"]
if ts1 is None or ts2 is None:
duration = INTERVAL_S
else:
duration = (ts2 - ts1) / 1000000.0
if duration <= 0:
duration = INTERVAL_S
total_duration += duration
filename = os.path.basename(rec1[cam_key]["file"])
f.write("file " + filename + "\n")
f.write("duration " + "{:.6f}".format(duration) + "\n")
# ffmpeg requiere repetir el ultimo frame sin duration
last_file = os.path.basename(records[-1][cam_key]["file"])
f.write("file " + last_file + "\n")
print("[Video] Frames: " + str(len(records)) + " | Duracion real: " + str(round(total_duration, 1)) + "s")
print("[Video] Generando " + output_path + " ...")
cmd = [
"ffmpeg", "-y",
"-f", "concat",
"-safe", "0",
"-i", concat_path,
"-vsync", "vfr",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
output_path
]
try:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("[Video] OK: " + output_path)
else:
print("[Video] Error ffmpeg: " + result.stderr[-300:])
except FileNotFoundError:
print("[Video] ffmpeg no encontrado. Instala con: sudo apt install ffmpeg")
# ---------------------------------------------------------------
# BUCLE PRINCIPAL
# ---------------------------------------------------------------
def capture_loop():
session_dir = None
cam1_dir = None
cam2_dir = None
records = []
frame_idx = 0
started = False
last_ts1 = None
last_ts2 = None
session_start_time = None
session_start_ts = None
stop_reason = "manual"
print("[Sistema] Esperando a que la impresora empiece a imprimir...")
try:
while True:
loop_start = time.time()
printer_data = get_printer_data()
state = printer_data.get("estado", {}).get("state", "desconocido")
if not started:
if state == "printing":
started = True
session_start_time = time.time()
session_name = "session_" + datetime.now().strftime("%Y%m%d_%H%M%S")
session_dir = os.path.join(OUTPUT_DIR, session_name)
cam1_dir = os.path.join(session_dir, "cam1_tmp")
cam2_dir = os.path.join(session_dir, "cam2_tmp")
os.makedirs(cam1_dir, exist_ok=True)
os.makedirs(cam2_dir, exist_ok=True)
print("[Sistema] Impresion detectada -> Sesion: " + session_dir)
else:
print("[Sistema] Estado: " + state + " - esperando...")
time.sleep(2)
continue
if state in ("complete", "finished", "cancelled", "error"):
stop_reason = state
print("[Sistema] Estado final: " + state + " - guardando ultima muestra...")
with cam1["lock"]:
jpeg1_final = cam1["jpeg"]
ts1_final = cam1["timestamp_us"]
with cam2["lock"]:
jpeg2_final = cam2["jpeg"]
ts2_final = cam2["timestamp_us"]
if jpeg1_final and jpeg2_final and session_dir:
name = "frame_" + str(frame_idx).zfill(6) + ".jpg"
sample_time_s = (ts1_final - session_start_ts) / 1000000.0 if ts1_final and session_start_ts else frame_idx * INTERVAL_S
with open(os.path.join(cam1_dir, name), "wb") as f:
f.write(jpeg1_final)
with open(os.path.join(cam2_dir, name), "wb") as f:
f.write(jpeg2_final)
records.append({
"sample_index": frame_idx,
"video_timestamp": seconds_to_timecode(sample_time_s),
"wall_time_utc": datetime.now(timezone.utc).isoformat(),
"cam1": {"file": "cam1/" + name, "timestamp_us": ts1_final, "timestamp_s": ts1_final / 1000000.0 if ts1_final else None},
"cam2": {"file": "cam2/" + name, "timestamp_us": ts2_final, "timestamp_s": ts2_final / 1000000.0 if ts2_final else None},
"printer": printer_data,
})
break
with cam1["lock"]:
jpeg1 = cam1["jpeg"]
ts1 = cam1["timestamp_us"]
with cam2["lock"]:
jpeg2 = cam2["jpeg"]
ts2 = cam2["timestamp_us"]
if jpeg1 is None or jpeg2 is None:
print("[Sistema] Esperando frames de las camaras...")
time.sleep(INTERVAL_S)
continue
if ts1 == last_ts1 and ts2 == last_ts2:
elapsed = time.time() - loop_start
time.sleep(max(0, INTERVAL_S - elapsed))
continue
last_ts1 = ts1
last_ts2 = ts2
name = "frame_" + str(frame_idx).zfill(6) + ".jpg"
if session_start_ts is None:
session_start_ts = ts1
sample_time_s = (ts1 - session_start_ts) / 1000000.0 if ts1 and session_start_ts else frame_idx * INTERVAL_S
timecode = seconds_to_timecode(sample_time_s)
with open(os.path.join(cam1_dir, name), "wb") as f:
f.write(jpeg1)
with open(os.path.join(cam2_dir, name), "wb") as f:
f.write(jpeg2)
record = {
"sample_index": frame_idx,
"video_timestamp": timecode,
"wall_time_utc": datetime.now(timezone.utc).isoformat(),
"cam1": {
"file": "cam1/" + name,
"timestamp_us": ts1,
"timestamp_s": ts1 / 1000000.0 if ts1 else None,
},
"cam2": {
"file": "cam2/" + name,
"timestamp_us": ts2,
"timestamp_s": ts2 / 1000000.0 if ts2 else None,
},
"printer": printer_data,
}
records.append(record)
temp = printer_data.get("hotend", {}).get("temp_c", 0) or 0
cama = printer_data.get("cama", {}).get("temp_c", 0) or 0
print(
"[" + str(frame_idx).zfill(6) + "]"
+ " " + timecode
+ " T=" + str(temp) + "C"
+ " cama=" + str(cama) + "C"
+ " cam1_ts=" + str(ts1)
+ " cam2_ts=" + str(ts2)
)
frame_idx += 1
elapsed = time.time() - loop_start
time.sleep(max(0, INTERVAL_S - elapsed))
except KeyboardInterrupt:
stop_reason = "manual"
print("\n[Sistema] Detenido manualmente.")
finally:
stop_event.set()
if session_dir is None:
print("[Sistema] No se inicio ninguna sesion (impresora nunca entro en estado printing).")
return
json_path = os.path.join(session_dir, "session_data.json")
with open(json_path, "w") as f:
json.dump(records, f, indent=2)
print("[Sistema] JSON guardado: " + json_path + " (" + str(len(records)) + " muestras)")
print("[Sistema] Motivo de parada: " + stop_reason)
generate_video(cam1_dir, os.path.join(session_dir, "video_cam1.mp4"), records, "cam1")
generate_video(cam2_dir, os.path.join(session_dir, "video_cam2.mp4"), records, "cam2")
print("[Sistema] Borrando frames temporales...")
shutil.rmtree(cam1_dir, ignore_errors=True)
shutil.rmtree(cam2_dir, ignore_errors=True)
print("[Sistema] Frames borrados.")
print("[Sistema] Sesion completa: " + session_dir)
print("[Sistema] Iniciando transferencia...")
transfer_result = subprocess.run(
["/home/pi/File_Transfer/venv/bin/python3", "/home/pi/File_Transfer/script/Transfer.py",
session_dir],
capture_output=True, text=True
)
if transfer_result.returncode == 0:
print("[Transferencia] OK")
else:
print("[Transferencia] Error: " + transfer_result.stderr[-300:])
# ---------------------------------------------------------------
# ARRANQUE
# ---------------------------------------------------------------
def main():
print("[Sistema] Esperando impresion para crear carpeta de sesion...")
t1 = threading.Thread(target=camera_thread, args=(CAM1_PORT, cam1), daemon=True)
t2 = threading.Thread(target=camera_thread, args=(CAM2_PORT, cam2), daemon=True)
t1.start()
t2.start()
time.sleep(2)
capture_loop()
if __name__ == "__main__":
main()
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment