#!/home/sam/dev/perso/flsun/klippy-env/bin/python
"""Pilotage au clavier de la calibration manuelle Klipper (test du papier).

    ./calib.py

Une touche = une action, pas de commande a taper. Le retour de Klipper
s'affiche sous chaque action (position Z apres chaque TESTZ, etc.).
"""
import importlib.util
import os
import sys
import termios
import tty

HERE = os.path.dirname(os.path.abspath(__file__))
spec = importlib.util.spec_from_file_location("kc", os.path.join(HERE, "klippy-client.py"))
kc = importlib.util.module_from_spec(spec)
spec.loader.exec_module(kc)

MENU = """
┌─────────────────────────── calibration manuelle ───────────────────────────┐
│  h  homing (G28)                    s  demarrer DELTA_CALIBRATE manuel      │
│  m  demarrer BED_MESH manuel        c  SAVE_CONFIG (ecrit + redemarre)      │
├─────────────────────────────────────────────────────────────────────────────┤
│  PgDn    descendre 100 mm           PgUp    monter 100 mm    (G1, tout le   │
│  J       descendre  10 mm           K       monter  10 mm     temps dispo)  │
│  ↓ / j   descendre   1 mm           ↑ / k   monter   1 mm    (TESTZ, en     │
│  d       descendre  0.1 mm          u       monter  0.1 mm    calibration)  │
│  -       bissection vers le bas     +       bissection vers le haut         │
│          (moitie du dernier pas)            (moitie du dernier pas)         │
├─────────────────────────────────────────────────────────────────────────────┤
│  Entree / a   ACCEPT (point valide, passe au suivant)                       │
│  v            MESURER : buse a Z=0.1 sur 7 points ; d/u j/k pour ajuster    │
│               jusqu'au frottement, Entree = suivant ; carte des ecarts       │
│  R            REPETABILITE : 2 points, 5 approches par des chemins differents│
│  [  /  ]      PENDANT UN PRINT : buse -0.05 / +0.05 (1re couche)  p  offset │
│  x            ABORT                 ?  reafficher ce menu       q  quitter  │
└─────────────────────────────────────────────────────────────────────────────┘
Methode : descendre par 1 mm en regardant, puis 0.1, puis alterner - et +
jusqu'a ce que la feuille frotte juste. Entree. Sept points.
"""

ACTIONS = {
    "h": "G28",
    "s": "DELTA_CALIBRATE METHOD=manual HORIZONTAL_MOVE_Z=2",
    "m": "BED_MESH_CALIBRATE METHOD=manual HORIZONTAL_MOVE_Z=2",
    "c": "SAVE_CONFIG",
    # Grands pas : G1 relatif (G91 ... G90 dans un seul script, atomique).
    # Marchent hors calibration aussi ; proteges par les limites de Klipper.
    "PGDN": "G91\nG1 Z-100 F1800\nG90",
    "PGUP": "G91\nG1 Z100 F1800\nG90",
    "J": "G91\nG1 Z-10 F1800\nG90",
    "K": "G91\nG1 Z10 F1800\nG90",
    # Petits pas : TESTZ (seulement pendant une calibration manuelle).
    "j": "TESTZ Z=-1",
    "k": "TESTZ Z=+1",
    "d": "TESTZ Z=-.1",
    "u": "TESTZ Z=+.1",
    "-": "TESTZ Z=-",
    "+": "TESTZ Z=+",
    "=": "TESTZ Z=+",
    "a": "ACCEPT",
    "\r": "ACCEPT",
    "\n": "ACCEPT",
    "x": "ABORT",
    # Premiere couche a la volee, PENDANT une impression (baby-steps).
    # Negatif = buse plus proche du plateau. L'offset se voit avec p.
    "[": "SET_GCODE_OFFSET Z_ADJUST=-0.05 MOVE=1",
    "]": "SET_GCODE_OFFSET Z_ADJUST=+0.05 MOVE=1",
    "p": "GET_POSITION",
}
# Sequences d'echappement du terminal -> touche logique
ESCAPES = {
    "[A": "k",       # fleche haut   -> +1 mm
    "[B": "j",       # fleche bas    -> -1 mm
    "[5~": "PGUP",   # Page Up       -> +100 mm
    "[6~": "PGDN",   # Page Down     -> -100 mm
}


def read_key():
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    try:
        tty.setcbreak(fd)
        ch = sys.stdin.read(1)
        if ch != "\x1b":
            return ch
        # Lire la sequence jusqu'a son caractere final (lettre ou '~'), 4 max.
        seq = ""
        for _ in range(4):
            c = sys.stdin.read(1)
            seq += c
            if c.isalpha() or c == "~":
                break
        return ESCAPES.get(seq, "")
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old)


def en_impression(k):
    """Vrai si klippy est en train d'imprimer (print_stats.state == printing)."""
    import json
    import socket
    try:
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        s.settimeout(3)
        s.connect(kc.UDS)
        s.sendall(json.dumps({"id": 99, "method": "objects/query",
                              "params": {"objects": {"print_stats": None}}}).encode() + b"\x03")
        buf = b""
        while b"\x03" not in buf:
            buf += s.recv(65536)
        s.close()
        st = json.loads(buf.split(b"\x03")[0])["result"]["status"]["print_stats"]["state"]
        return st in ("printing", "paused")
    except Exception:
        return True      # dans le doute, on refuse : mieux vaut une touche ignoree qu'un crash


VERIF_RAYON = 100     # rayon des points peripheriques (mm)
VERIF_Z = 0.1         # hauteur de test (epaisseur d'une feuille)
VERIF_GARDE = 2       # hauteur des deplacements entre points


def verify(k):
    """Mesure l'ecart residuel de la calibration, a la feuille, sur 7 points.

    A chaque point la buse se pose a Z=0.1 ; on ajuste avec d/u (0.1 mm) et
    j/k (1 mm) jusqu'a ce que la feuille frotte juste, puis Entree. A la fin,
    la carte des Z de contact : 0.1 partout = calibration parfaite ; un point a
    -0.2 = plateau 0.3 mm plus bas que prevu a cet endroit.
    """
    import math
    pts = [(0.0, 0.0)] + [
        (round(VERIF_RAYON * math.cos(math.radians(a)), 3),
         round(VERIF_RAYON * math.sin(math.radians(a)), 3))
        for a in range(0, 360, 60)]
    nudges = {"d": -0.1, "u": 0.1, "j": -1.0, "k": 1.0}
    print("\nVERIFICATION : %d points. A chaque point : d/u = 0.1 mm, j/k = 1 mm,"
          " Entree = suivant, x = stop." % len(pts))
    contacts = []
    for i, (x, y) in enumerate(pts, start=1):
        if not k.run("G1 Z%.1f F1800" % VERIF_GARDE):
            return
        if not k.run("G1 X%.3f Y%.3f F3000" % (x, y)):
            return
        if not k.run("G1 Z%.2f F600" % VERIF_Z):
            return
        z = VERIF_Z
        print("--- point %d/%d  (X=%g Y=%g)  Z=%.2f  -> ajuster, puis Entree"
              % (i, len(pts), x, y, z))
        stop = False
        while True:
            key = read_key()
            if key == "x":
                stop = True
                break
            if key in ("\r", "\n", "a"):
                break
            dz = nudges.get(key)
            if dz is None:
                continue
            if k.run("G91\nG1 Z%.2f F300\nG90" % dz):
                z = round(z + dz, 2)
                print("    Z=%.2f" % z)
        contacts.append((x, y, z))
        if stop:
            break
    k.run("G1 Z%.1f F1800" % VERIF_GARDE)
    print("\n=== contact a la feuille, par point (cible : Z=0.00) ===")
    for i, (x, y, z) in enumerate(contacts, start=1):
        note = "" if abs(z) < 0.05 else "  (plateau %.2f mm %s que le zero)" % (
            abs(z), "plus bas" if z < 0 else "plus haut")
        print("  %d: X=%8.3f Y=%8.3f  contact Z=%+.2f%s" % (i, x, y, z, note))
    if contacts:
        zs = [c[2] for c in contacts]
        print("  etendue : %.2f mm" % (max(zs) - min(zs)))


REPET_POINTS = [(0.0, -100.0), (80.0, -80.0)]     # bord avant : la zone qui ne repond pas
REPET_APPROCHES = [                                # d'ou l'on vient avant de se poser
    ("du centre", "G1 X0 Y0 Z5 F3000"),
    ("de +X", "G1 X110 Y0 Z5 F3000"),
    ("de -X", "G1 X-110 Y0 Z5 F3000"),
    ("de +Y (tour C)", "G1 X0 Y110 Z5 F3000"),
    ("d'en haut", "G1 Z40 F3000"),
]


def repetabilite(k):
    """Meme point, cinq approches differentes : si le Z de contact au papier
    varie, c'est du jeu ou des courroies (non repetable) ; s'il est stable,
    l'erreur est geometrique ou thermique (repetable). d/u j/k, Entree, x."""
    nudges = {"d": -0.1, "u": 0.1, "j": -1.0, "k": 1.0}
    print("\nREPETABILITE : %d points x %d approches. A chaque pose : d/u = 0.1 mm,"
          " j/k = 1 mm, Entree = suivant, x = stop." % (len(REPET_POINTS), len(REPET_APPROCHES)))
    resultats = {}
    for (x, y) in REPET_POINTS:
        zs = []
        for nom, chemin in REPET_APPROCHES:
            if not (k.run(chemin) and k.run("G1 X%.1f Y%.1f Z%.1f F3000" % (x, y, VERIF_GARDE))
                    and k.run("G1 Z%.2f F600" % VERIF_Z)):
                return
            z = VERIF_Z
            print("--- (X=%g Y=%g) approche %s  Z=%.2f -> ajuster, puis Entree" % (x, y, nom, z))
            stop = False
            while True:
                key = read_key()
                if key == "x":
                    stop = True
                    break
                if key in ("\r", "\n", "a"):
                    break
                dz = nudges.get(key)
                if dz is None:
                    continue
                if k.run("G91\nG1 Z%.2f F300\nG90" % dz):
                    z = round(z + dz, 2)
                    print("    Z=%.2f" % z)
            zs.append((nom, z))
            if stop:
                break
        resultats[(x, y)] = zs
        if stop:
            break
    k.run("G1 Z%.1f F1800" % VERIF_GARDE)
    print("\n=== contact au papier selon l'approche ===")
    for (x, y), zs in resultats.items():
        vals = [z for _, z in zs]
        print("  (X=%g Y=%g) : %s   etendue %.2f mm" % (
            x, y, "  ".join("%s %+.2f" % (n, z) for n, z in zs), max(vals) - min(vals)))
    print("  etendue <= 0.05 : repetable (geometrie / thermique) ; > 0.1 : jeu ou courroies")


def main():
    try:
        k = kc.Klippy()
    except OSError as e:
        sys.exit("klippy injoignable (%s). Lancer d'abord ./start-klipper.sh" % e)
    print(MENU)
    try:
        while True:
            key = read_key()
            if key in ("q", "\x03", "\x04"):
                break
            if key == "?":
                print(MENU)
                continue
            if key == "v":
                if en_impression(k):
                    print("!!! IMPRESSION EN COURS")
                    continue
                verify(k)
                continue
            if key == "R":
                if en_impression(k):
                    print("!!! IMPRESSION EN COURS")
                    continue
                repetabilite(k)
                continue
            cmd = ACTIONS.get(key)
            if cmd is None:
                continue
            # Garde-fou : pendant une impression, seules les touches de
            # baby-step et de lecture sont permises. Un 's' tape dans le
            # mauvais terminal a envoye la tete a Z=2 dans un cube en cours
            # (2026-09-04) — plus jamais.
            if key not in ("[", "]", "p") and en_impression(k):
                print("!!! IMPRESSION EN COURS : touche '%s' refusee (seuls [ ] p sont permis)" % key)
                continue
            if key == "c":
                print("SAVE_CONFIG va ecrire printer.cfg et redemarrer Klipper.")
                print("Confirmer : appuyer a nouveau sur c   (autre touche = annuler)")
                if read_key() != "c":
                    print("annule")
                    continue
            k.run(cmd)
            if key == "c":
                print("Klipper redemarre : attendre ~15 s, puis relancer ./calib.py")
                break
    finally:
        k.close()


if __name__ == "__main__":
    main()
