// mc-ch5.jsx — D7 CH5: Homing & the PLCopen state diagram — interactive
// diagram: issue commands, watch the axis walk its legal states.
const { useState: u5m, useEffect: e5m, useRef: r5m } = React;

// ── PLCopen State Walker ────────────────────────────────────
function StateWalker() {
  const [st, setSt] = u5m("DISABLED");
  const [homed, setHomed] = u5m(false);
  const [msg, setMsg] = u5m(LX("axis is torque-off — enable it first", "axe hors couple — validez d'abord", "eje sin par — habilítalo primero"));
  const tmr = r5m(null);

  const STATES = [
    { k: "DISABLED", d: LX("no torque", "hors couple", "sin par") },
    { k: "STANDSTILL", d: LX("powered, idle", "sous couple, repos", "con par, reposo") },
    { k: "HOMING", d: LX("finding zero", "recherche du zéro", "buscando cero") },
    { k: "DISCRETE MOTION", d: LX("point-to-point", "point à point", "punto a punto") },
    { k: "CONTINUOUS MOTION", d: LX("velocity move", "mouvement en vitesse", "movimiento en velocidad") },
    { k: "ERRORSTOP", d: LX("latched fault", "défaut verrouillé", "fallo enganchado"), err: true },
  ];

  const later = (fn, ms) => { clearTimeout(tmr.current); tmr.current = setTimeout(fn, ms); };
  e5m(() => () => clearTimeout(tmr.current), []);

  const cmd = (c) => {
    const deny = (why) => setMsg("✗ " + why);
    switch (c) {
      case "enable":
        if (st === "DISABLED") { setSt("STANDSTILL"); setMsg("MC_Power.Status TRUE → StandStill"); }
        else deny(LX("already powered", "déjà sous couple", "ya con par")); break;
      case "home":
        if (st === "STANDSTILL") {
          setSt("HOMING"); setMsg("MC_Home.Busy…");
          later(() => { setSt("STANDSTILL"); setHomed(true); setMsg("MC_Home.Done → " + LX("zero established", "zéro établi", "cero establecido")); }, 1800);
        } else deny(LX("MC_Home is only legal from StandStill", "MC_Home n'est légal que depuis StandStill", "MC_Home solo es legal desde StandStill")); break;
      case "moveabs":
        if (st === "STANDSTILL") {
          setSt("DISCRETE MOTION"); setMsg("MC_MoveAbsolute.Busy…" + (homed ? "" : "  ⚠ " + LX("not homed — positions are lies!", "non référencé — positions mensongères !", "sin homing — ¡posiciones falsas!")));
          later(() => { setSt("STANDSTILL"); setMsg("MC_MoveAbsolute.Done → StandStill"); }, 1800);
        } else deny(LX("no moves from ", "pas de mouvement depuis ", "sin movimientos desde ") + st); break;
      case "movevel":
        if (st === "STANDSTILL") { setSt("CONTINUOUS MOTION"); setMsg("MC_MoveVelocity → InVelocity"); }
        else deny(LX("no moves from ", "pas de mouvement depuis ", "sin movimientos desde ") + st); break;
      case "halt":
        if (st === "CONTINUOUS MOTION" || st === "DISCRETE MOTION") {
          setMsg("MC_Halt…"); later(() => { setSt("STANDSTILL"); setMsg("MC_Halt.Done → StandStill"); }, 900);
        } else deny(LX("nothing to halt", "rien à arrêter", "nada que parar")); break;
      case "fault":
        if (st !== "DISABLED" && st !== "ERRORSTOP") { clearTimeout(tmr.current); setSt("ERRORSTOP"); setMsg("🚨 " + LX("drive fault! axis latched in ErrorStop", "défaut variateur ! axe verrouillé en ErrorStop", "¡fallo de variador! eje enganchado en ErrorStop")); }
        else deny(LX("axis already off / faulted", "axe déjà hors couple / en défaut", "eje ya sin par / en fallo")); break;
      case "reset":
        if (st === "ERRORSTOP") { setSt("STANDSTILL"); setMsg("MC_Reset → StandStill " + LX("(cause cleared)", "(cause supprimée)", "(causa eliminada)")); }
        else deny(LX("nothing to reset", "rien à réinitialiser", "nada que resetear")); break;
    }
  };

  const BTNS = [
    { c: "enable", t: "MC_Power", bg: "var(--mint)" },
    { c: "home", t: "MC_Home", bg: "#fff" },
    { c: "moveabs", t: "MC_MoveAbsolute", bg: "#fff" },
    { c: "movevel", t: "MC_MoveVelocity", bg: "#fff" },
    { c: "halt", t: "MC_Halt", bg: "#fff" },
    { c: "fault", t: LX("⚡ fault!", "⚡ défaut !", "⚡ ¡fallo!"), bg: "#FFE2EA" },
    { c: "reset", t: "MC_Reset", bg: "#fff" },
  ];

  return (
    <div className="card" style={{ background: "#fff" }}>
      <div className="row" style={{ justifyContent: "space-between", marginBottom: 12 }}>
        <span className="tag" style={{ background: "var(--coral)", color: "#fff" }}>AxisX · PLCopen</span>
        <span className="tag" style={{ fontSize: 11, background: homed ? "#D6F6EC" : "#FFF1B8" }}>{homed ? LX("homed ✓", "référencé ✓", "con homing ✓") : LX("NOT homed", "NON référencé", "SIN homing")}</span>
      </div>
      <div className="mc-states">
        {STATES.map(s => (
          <div key={s.k} className={"mc-state" + (s.err ? " err" : "") + (st === s.k ? " cur" : "")}>
            <span className="t">{st === s.k ? "◉ " : "○ "}{s.k}</span>
            <span className="d">{s.d}</span>
          </div>
        ))}
      </div>
      <div className="row" style={{ gap: 7, marginTop: 14, flexWrap: "wrap" }}>
        {BTNS.map(b => <button key={b.c} className="btn sm" style={{ background: b.bg }} onClick={() => cmd(b.c)}>{b.t}</button>)}
      </div>
      <div className="readout" style={{ marginTop: 14 }}><span className="lbl">log »</span> <span className={msg.startsWith("✗") || msg.startsWith("🚨") ? "miss" : "ok"}>{msg}</span></div>
    </div>
  );
}

defineChapter({
  id: "states", act: "States & Homing", num: 5, emoji: "🧭", navTitle: "Homing & the PLCopen State Diagram",
  title: "Homing & the PLCopen State Diagram",
  sub: "An incremental encoder doesn't know where it is at power-up — MC_Home establishes the machine zero. And every axis lives inside the PLCopen state diagram: Disabled, StandStill, Homing, Discrete Motion, Continuous Motion, ErrorStop. Commands are only legal from certain states.",
  Body() {
    return (
      <div className="ch-body">
        <H>{LX("Why homing exists", "Pourquoi le homing existe", "Por qué existe el homing")}</H>
        <P>{LX("With an incremental encoder, the axis wakes up knowing only 'I am where I am' — position zero is wherever it happened to be standing. Command MoveAbsolute(250) now and you'll get 250 mm from a meaningless point. MC_Home runs a reference procedure (drive to a reference switch, detect it, optionally fine-tune on the encoder's index pulse) and declares that spot the machine zero. From then on, absolute positions mean something. Axes with absolute encoders skip the ritual — they remember across power cycles.",
          "Avec un codeur incrémental, l'axe se réveille en sachant seulement « je suis où je suis » — le zéro est l'endroit où il se trouvait par hasard. Commandez MoveAbsolute(250) maintenant et vous aurez 250 mm depuis un point sans signification. MC_Home exécute une procédure de référence (aller vers un capteur de référence, le détecter, affiner éventuellement sur le top zéro codeur) et déclare cet endroit zéro machine. Dès lors, les positions absolues ont un sens. Les axes à codeur absolu sautent le rituel — ils se souviennent à travers les coupures.",
          "Con un encoder incremental, el eje despierta sabiendo solo «estoy donde estoy» — el cero es donde casualmente estaba parado. Comanda MoveAbsolute(250) ahora y tendrás 250 mm desde un punto sin sentido. MC_Home ejecuta un procedimiento de referencia (ir a un sensor de referencia, detectarlo, afinar opcionalmente con el pulso índice del encoder) y declara ese punto el cero máquina. Desde entonces, las posiciones absolutas significan algo. Los ejes con encoder absoluto se saltan el ritual — recuerdan entre apagados.")}</P>

        <H>{LX("The state diagram is the law", "Le diagramme d'états fait loi", "El diagrama de estados es la ley")}</H>
        <P>{LX("PLCopen defines a state machine every axis obeys — yes, the same pattern you built in the D6 module, now applied to the axis itself. Disabled (no torque) → StandStill (powered, idle) → motion states — and ErrorStop, the latched fault state that only MC_Reset leaves. Every MC command is legal only from certain states: you can't home a moving axis, you can't move a disabled one. When a command returns an error the first question is always: what state was the axis in?",
          "PLCopen définit une machine à états que tout axe respecte — oui, le même schéma que vous avez bâti au module D6, appliqué à l'axe lui-même. Disabled (hors couple) → StandStill (sous couple, repos) → états de mouvement — et ErrorStop, l'état de défaut verrouillé dont seul MC_Reset sort. Chaque commande MC n'est légale que depuis certains états : on ne référence pas un axe en mouvement, on ne déplace pas un axe désactivé. Quand une commande renvoie une erreur, la première question est toujours : dans quel état était l'axe ?",
          "PLCopen define una máquina de estados que todo eje obedece — sí, el mismo patrón que construiste en el módulo D6, aplicado al propio eje. Disabled (sin par) → StandStill (con par, reposo) → estados de movimiento — y ErrorStop, el estado de fallo enganchado del que solo sale MC_Reset. Cada comando MC solo es legal desde ciertos estados: no puedes hacer homing a un eje en movimiento, ni mover uno deshabilitado. Cuando un comando devuelve error, la primera pregunta siempre es: ¿en qué estado estaba el eje?")}</P>
        <P>{LX("Walk the diagram yourself. Try illegal moves — home while moving, move while disabled — and read the refusals:",
          "Parcourez le diagramme vous-même. Tentez l'illégal — homing en mouvement, mouvement hors couple — et lisez les refus :",
          "Recorre el diagrama tú mismo. Prueba lo ilegal — homing en movimiento, mover sin par — y lee los rechazos:")}</P>
        <StateWalker />

        <H>{LX("MC_Home in code", "MC_Home en code", "MC_Home en código")}</H>
        <CodePanel file="PRG_Motion.TcPOU" badge="reference once, trust forever" badgeBg="var(--coral)">
          <CL n={1}><span className="st-id">fbHome</span><span className="st-op">(</span></CL>
          <CL n={2}>{"  "}<span className="st-id">Axis</span> <span className="st-op">:=</span> <span className="st-id">AxisX</span><span className="st-op">,</span></CL>
          <CL n={3} hl>{"  "}<span className="st-id">Execute</span> <span className="st-op">:=</span> <span className="st-id">bDoHome</span><span className="st-op">,</span>{"        "}<span className="st-com">(* from StandStill only *)</span></CL>
          <CL n={4}>{"  "}<span className="st-id">Position</span> <span className="st-op">:=</span> <span className="st-lit">0.0</span><span className="st-op">,</span>{"           "}<span className="st-com">(* value assigned at the reference point *)</span></CL>
          <CL n={5}>{"  "}<span className="st-id">Done</span> <span className="st-op">=&gt;</span> <span className="st-id">bHomed</span><span className="st-op">,</span></CL>
          <CL n={6}>{"  "}<span className="st-id">Error</span> <span className="st-op">=&gt;</span> <span className="st-id">bHomeErr</span><span className="st-op">);</span></CL>
          <CL n={7}> </CL>
          <CL n={8} hl><span className="st-com">(* gate every absolute move in the machine on bHomed *)</span></CL>
          <CL n={9}><span className="st-kw">IF</span> <span className="st-id">bHomed</span> <span className="st-kw">AND</span> <span className="st-id">bAxisXPowered</span> <span className="st-kw">THEN</span> <span className="st-com">(* …MoveAbsolute allowed… *)</span> <span className="st-kw">END_IF</span></CL>
        </CodePanel>
        <Callout kind="key">{LX(<>Recovery after a fault is a fixed litany — learn it as a sequence (D6 skills): <b>cause cleared → MC_Reset (leaves ErrorStop) → MC_Power confirms StandStill → re-home if the reference was lost → resume</b>. Skipping a verse is why 'it faults again immediately'.</>,
          <>La reprise après défaut est une litanie fixe — apprenez-la comme une séquence (acquis D6) : <b>cause supprimée → MC_Reset (sort d'ErrorStop) → MC_Power confirme StandStill → re-homing si la référence est perdue → reprise</b>. Sauter un couplet, c'est « ça refaute aussitôt ».</>,
          <>La recuperación tras un fallo es una letanía fija — apréndela como secuencia (destrezas D6): <b>causa eliminada → MC_Reset (sale de ErrorStop) → MC_Power confirma StandStill → re-homing si se perdió la referencia → reanudar</b>. Saltarse un verso es por qué «vuelve a fallar al instante».</>)}</Callout>

        <WhyMatters machine={LX("A gantry that forgot where zero was", "Un portique qui a oublié son zéro", "Un pórtico que olvidó dónde estaba el cero")}>
          {LX(<>A storage-and-retrieval gantry loses power mid-shift. On restart, the incremental encoders read zero <i>right where the carriage stopped</i> — three metres into the rack. If the software allows an absolute move now, the gantry will happily drive 'to position 500' measured from the wrong origin and bury itself in shelf 12. This exact scenario — moving an unhomed axis — has bent more machine frames than almost any other motion bug. That's why production code <b>latches a bHomed flag per axis, clears it on power loss or encoder fault, and refuses every MoveAbsolute until MC_Home has re-established truth</b>. The PLCopen state diagram plus a homed flag is the axis's memory of reality; respect both and absolute positions are facts, not folklore.</>,
            <>Un portique de stockage perd l'alimentation en plein poste. Au redémarrage, les codeurs incrémentaux lisent zéro <i>là où le chariot s'est arrêté</i> — trois mètres dans le rayonnage. Si le logiciel autorise un mouvement absolu maintenant, le portique ira gaiement « à la position 500 » mesurée depuis la mauvaise origine et s'encastrera dans l'étagère 12. Ce scénario exact — déplacer un axe non référencé — a tordu plus de châssis que presque tout autre bug de mouvement. Voilà pourquoi le code de production <b>verrouille un drapeau bHomed par axe, l'efface sur coupure ou défaut codeur, et refuse tout MoveAbsolute tant que MC_Home n'a pas rétabli la vérité</b>. Le diagramme PLCopen plus un drapeau homed, c'est la mémoire du réel de l'axe ; respectez les deux et les positions absolues sont des faits, pas du folklore.</>,
            <>Un pórtico de almacén pierde alimentación a mitad de turno. Al rearrancar, los encoders incrementales leen cero <i>justo donde paró el carro</i> — tres metros dentro de la estantería. Si el software permite ahora un movimiento absoluto, el pórtico irá feliz «a la posición 500» medida desde el origen equivocado y se incrustará en el estante 12. Este escenario exacto — mover un eje sin homing — ha doblado más bastidores que casi cualquier otro bug de movimiento. Por eso el código de producción <b>engancha una bandera bHomed por eje, la borra en corte de energía o fallo de encoder, y rechaza todo MoveAbsolute hasta que MC_Home restablezca la verdad</b>. El diagrama PLCopen más una bandera homed es la memoria de la realidad del eje; respeta ambos y las posiciones absolutas son hechos, no folclore.</>)}
        </WhyMatters>

        <DualTrack
          beg={{
            lead: LX("Homing is like setting a wall clock after a power cut: until you do, the hands point somewhere, but the time is meaningless.", "Le homing, c'est régler une horloge après une coupure : avant ça, les aiguilles pointent quelque part, mais l'heure n'a aucun sens.", "El homing es como poner en hora un reloj tras un apagón: hasta entonces, las agujas apuntan a algún sitio, pero la hora no significa nada."),
            points: [
              LX("Incremental encoders forget at power-off; absolute encoders remember.", "Les codeurs incrémentaux oublient à l'extinction ; les absolus se souviennent.", "Los encoders incrementales olvidan al apagar; los absolutos recuerdan."),
              LX("ErrorStop only opens with the MC_Reset key.", "ErrorStop ne s'ouvre qu'avec la clé MC_Reset.", "ErrorStop solo se abre con la llave MC_Reset."),
            ],
            extra: { label: LX("Picture it", "Imagez-le", "Imagínalo"), text: LX("The state diagram is a building's rooms: some doors only open from certain rooms.", "Le diagramme d'états est un bâtiment : certaines portes ne s'ouvrent que depuis certaines pièces.", "El diagrama de estados es un edificio: algunas puertas solo abren desde ciertas salas.") },
          }}
          adv={{
            lead: LX("Read the live state via MC_ReadStatus (or the AXIS_REF status bits) and surface it on the HMI — 'Axis X: ErrorStop' beats 'machine stopped' for a 2 a.m. diagnosis.", "Lisez l'état en direct via MC_ReadStatus (ou les bits de l'AXIS_REF) et affichez-le sur l'IHM — « Axe X : ErrorStop » bat « machine arrêtée » pour un diagnostic à 2 h.", "Lee el estado en vivo vía MC_ReadStatus (o los bits del AXIS_REF) y muéstralo en el HMI — «Eje X: ErrorStop» supera a «máquina parada» para un diagnóstico a las 2 a.m."),
            points: [
              LX("Homing modes vary by drive (switch, switch+index, hard-stop, set-position) — chosen in the drive/NC config, not in the block.", "Les modes de homing varient selon le variateur (capteur, capteur+top zéro, butée, set-position) — choisis dans la config variateur/NC, pas dans le bloc.", "Los modos de homing varían según el variador (sensor, sensor+índice, tope, set-position) — se eligen en la config del variador/NC, no en el bloque."),
              LX("MC_SetPosition re-labels the current spot without motion — useful, and dangerous for the same reason.", "MC_SetPosition réétiquette la position courante sans mouvement — utile, et dangereux pour la même raison.", "MC_SetPosition reetiqueta el punto actual sin movimiento — útil, y peligroso por la misma razón."),
              LX("ErrorStop from ANY state on a drive fault is the diagram's 'catch' — mirror it in your machine sequence (D6 CH4).", "ErrorStop depuis N'IMPORTE quel état sur défaut variateur est le « catch » du diagramme — reflétez-le dans votre séquence machine (D6 CH4).", "ErrorStop desde CUALQUIER estado en fallo de variador es el «catch» del diagrama — refldéjalo en tu secuencia de máquina (D6 CH4)."),
            ],
          }}
        />

        <H>{LX("Common mistakes", "Erreurs courantes", "Errores comunes")}</H>
        <Mistakes items={[
          { bad: LX(<>Allowing <b>absolute moves on an unhomed axis</b> — positions are measured from wherever the axis happened to wake up, and the machine drives into itself.</>,
              <>Autoriser des <b>mouvements absolus sur un axe non référencé</b> — les positions partent de là où l'axe s'est réveillé, et la machine se rentre dedans.</>,
              <>Permitir <b>movimientos absolutos en un eje sin homing</b> — las posiciones se miden desde donde despertó el eje, y la máquina se choca consigo misma.</>),
            fix: LX(<>Latch <code>bHomed</code> per axis, clear it on power-up and encoder faults, and gate every MoveAbsolute on it. Offer only jog and MC_Home until then.</>,
              <>Verrouillez <code>bHomed</code> par axe, effacez-le à la mise sous tension et sur défaut codeur, conditionnez chaque MoveAbsolute dessus. N'offrez que jog et MC_Home avant.</>,
              <>Engancha <code>bHomed</code> por eje, bórralo al encender y en fallos de encoder, y condiciona cada MoveAbsolute a él. Ofrece solo jog y MC_Home hasta entonces.</>) },
          { bad: LX(<>Issuing <code>MC_Home</code> while the axis is <b>moving</b> or unpowered, then puzzling over the error output — homing is only legal from StandStill.</>,
              <>Lancer <code>MC_Home</code> pendant que l'axe <b>bouge</b> ou hors couple, puis s'étonner de l'erreur — le homing n'est légal que depuis StandStill.</>,
              <>Lanzar <code>MC_Home</code> con el eje <b>en movimiento</b> o sin par, y extrañarse del error — el homing solo es legal desde StandStill.</>),
            fix: LX(<>Sequence it: power on → confirm StandStill → home → wait Done. The state diagram tells you the legal order; follow it explicitly in your steps.</>,
              <>Séquencez : mise sous couple → confirmer StandStill → homing → attendre Done. Le diagramme donne l'ordre légal ; suivez-le explicitement.</>,
              <>Secuéncialo: dar par → confirmar StandStill → homing → esperar Done. El diagrama da el orden legal; síguelo explícitamente en tus pasos.</>) },
          { bad: LX(<>After an E-stop, retrying moves against an axis stuck in <b>ErrorStop</b> — every command errors and the log fills with red.</>,
              <>Après un arrêt d'urgence, réessayer des mouvements sur un axe coincé en <b>ErrorStop</b> — chaque commande erre et le journal vire au rouge.</>,
              <>Tras un paro de emergencia, reintentar movimientos contra un eje atascado en <b>ErrorStop</b> — cada orden da error y el log se llena de rojo.</>),
            fix: LX(<>ErrorStop exits only via <code>MC_Reset</code>. Build the recovery as a sequence: reset → re-enable → re-home (if needed) → resume.</>,
              <>ErrorStop ne se quitte que via <code>MC_Reset</code>. Bâtissez la reprise en séquence : reset → ré-enable → re-homing (si besoin) → reprise.</>,
              <>ErrorStop solo se sale vía <code>MC_Reset</code>. Construye la recuperación como secuencia: reset → re-habilitar → re-homing (si hace falta) → reanudar.</>) },
        ]} />

        <H>{LX("Quick checks", "Vérifs rapides", "Comprobaciones rápidas")}</H>
        <Check q={LX("Why does an incremental-encoder axis need homing after power-up?", "Pourquoi un axe à codeur incrémental doit-il être référencé après mise sous tension ?", "¿Por qué un eje con encoder incremental necesita homing tras encender?")} correct={2}
          options={[LX("To warm up the motor", "Pour chauffer le moteur", "Para calentar el motor"),
            LX("To test the encoder", "Pour tester le codeur", "Para probar el encoder"),
            LX("It lost its reference — zero is wherever it stopped, so absolute positions are meaningless until re-referenced", "Il a perdu sa référence — le zéro est où il s'est arrêté, les positions absolues n'ont aucun sens avant re-référencement", "Perdió su referencia — el cero es donde paró, las posiciones absolutas no significan nada hasta re-referenciar")]}
          explain={LX("Incremental encoders count pulses from wherever they woke up. MC_Home drives to a known physical reference and assigns it a position, making absolute coordinates true again.", "Les codeurs incrémentaux comptent depuis leur réveil. MC_Home va à une référence physique connue et lui affecte une position, rendant les coordonnées absolues vraies.", "Los encoders incrementales cuentan desde donde despertaron. MC_Home va a una referencia física conocida y le asigna una posición, haciendo verdaderas las coordenadas absolutas.")} />
        <Check q={LX("An axis is latched in ErrorStop. What's the ONLY way out?", "Un axe est verrouillé en ErrorStop. Quelle est la SEULE sortie ?", "Un eje está enganchado en ErrorStop. ¿Cuál es la ÚNICA salida?")} correct={1}
          options={[LX("Command a new move", "Commander un nouveau mouvement", "Comandar un movimiento nuevo"),
            LX("MC_Reset (after the cause is cleared)", "MC_Reset (après suppression de la cause)", "MC_Reset (tras eliminar la causa)"),
            LX("Toggle MC_Power.Enable", "Basculer MC_Power.Enable", "Conmutar MC_Power.Enable")]}
          explain={LX("ErrorStop is deliberately sticky: moves are refused and Enable alone won't clear it. Fix the cause, then MC_Reset transitions the axis back toward StandStill.", "ErrorStop est volontairement collant : les mouvements sont refusés et Enable seul ne l'efface pas. Supprimez la cause, puis MC_Reset ramène l'axe vers StandStill.", "ErrorStop es pegajoso a propósito: los movimientos se rechazan y Enable solo no lo borra. Elimina la causa, luego MC_Reset devuelve el eje hacia StandStill.")} />
      </div>
    );
  }
});
