[
    {
        "id": "tagnav_tab",
        "type": "tab",
        "label": "DEXI Tag Navigation",
        "disabled": false,
        "info": "Tag-to-tag navigation as a flow.\n\nOne generic command node ('DEXI command → request' + 'DEXI services') calls both on-board services. The mission is a list of steps in the 'mission' function node. Start it from the RC hand-off (channel 7 to Offboard), the ENGAGE button, or START on the bench or in the sim.\n\nOn the aircraft: ARM hand-off runs once at deploy. The pilot takes off, hovers over a tag and switches to Offboard; tag_nav reports engaged, the mission runs and the aircraft lands. Leaving Offboard stops the mission."
    },
    {
        "type": "function",
        "name": "DEXI command → request",
        "x": 560,
        "y": 300,
        "outputs": 1,
        "func": "\n// msg.cmd = { command, parameter, timeout, north, east, down, yaw }\n// Tag commands go to tag_nav (parameter = tag id, north/east/down = body velocity);\n// everything else is a flight step for the offboard manager (parameter = meters or degrees).\nconst TAG_NAV = ['center_on_tag', 'hold_on_tag', 'fly_until_tag', 'wait_for_tag', 'wait_for_offboard'];\nconst c = msg.cmd || {};\nconst tag = TAG_NAV.includes(c.command);\nmsg.payload = tag ? {\n  serviceName: '/dexi/tag_nav/command',\n  serviceType: 'dexi_interfaces/srv/TagNavCommand',\n  serviceRequest: { command: c.command, tag_id: Math.round(Number(c.parameter ?? -1)), hold_s: Number(c.hold_s ?? 0),\n                    forward: Number(c.north ?? 0), right: Number(c.east ?? 0), down: Number(c.down ?? 0),\n                    timeout: Number(c.timeout ?? 30) },\n} : {\n  serviceName: '/dexi/flight/command',\n  serviceType: 'dexi_interfaces/srv/FlightCommand',\n  serviceRequest: { command: c.command, value: Number(c.parameter ?? 0), north: Number(c.north ?? 0),\n                    east: Number(c.east ?? 0), down: Number(c.down ?? 0), yaw: Number(c.yaw ?? 0),\n                    timeout: Number(c.timeout ?? 30) },\n};\nnode.status({ fill: 'blue', shape: 'dot', text: `${c.command} ${c.parameter ?? ''}` });\nreturn msg;",
        "id": "tagnav_build",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_call"
            ]
        ]
    },
    {
        "type": "ros2-service-call",
        "name": "DEXI services",
        "server": "27129f80a96ce509",
        "x": 790,
        "y": 300,
        "id": "tagnav_call",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_route"
            ]
        ]
    },
    {
        "type": "switch",
        "name": "success?",
        "property": "payload.success",
        "propertyType": "msg",
        "rules": [
            {
                "t": "true"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "false",
        "repair": false,
        "outputs": 2,
        "x": 990,
        "y": 300,
        "id": "tagnav_route",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_dbg_ok",
                "tagnav_gate_ok",
                "tagnav_ui_resultfn",
                "tagnav_rearm_after_land"
            ],
            [
                "tagnav_dbg_fail",
                "tagnav_gate_fail",
                "tagnav_ui_resultfn",
                "tagnav_rearm_after_land"
            ]
        ]
    },
    {
        "type": "debug",
        "name": "step ok",
        "active": true,
        "tosidebar": true,
        "complete": "payload.message",
        "targetType": "msg",
        "x": 1200,
        "y": 260,
        "id": "tagnav_dbg_ok",
        "z": "tagnav_tab",
        "wires": []
    },
    {
        "type": "debug",
        "name": "step FAILED",
        "active": true,
        "tosidebar": true,
        "complete": "payload.message",
        "targetType": "msg",
        "x": 1200,
        "y": 340,
        "id": "tagnav_dbg_fail",
        "z": "tagnav_tab",
        "wires": []
    },
    {
        "type": "function",
        "name": "mission (steps as data)",
        "x": 330,
        "y": 300,
        "outputs": 2,
        "func": "\n// The route: tag ids in the order they are flown. The steps are built from it.\n// The first step centers on ROUTE[0], so hand over above that tag; use -1 to start on whichever tag is in view.\nconst ROUTE = [0, 1, 2, 3, 4];   // AVR 2026 flies [0, 1, 2, 3, 4, 5, 6]; a course with tags 0, 2, 4 flies [0, 2, 4]\nconst FWD = 0.25;   // m/s transit speed; faster can outrun the tag detector in low light\n\n// Tag ids increase along the corridor, so a higher id is ahead (fly forward) and a lower id\n// is behind (fly backward). [0, 2, 4, 2, 0] works.\nconst STEPS = [{ command: 'center_on_tag', parameter: ROUTE[0], timeout: 25 }];\nfor (let k = 1; k < ROUTE.length; k++) {\n  const tag = ROUTE[k], dir = Math.sign(tag - ROUTE[k - 1]) || 1;\n  STEPS.push({ command: 'fly_until_tag', parameter: tag, timeout: 25, north: dir * FWD });\n  STEPS.push({ command: 'center_on_tag', parameter: tag, timeout: 25 });\n}\nSTEPS.push({ command: 'land', parameter: 0, timeout: 30 });\n\nlet i = context.get('i');\nconst running = i !== undefined && i !== null && i < STEPS.length;\nif (msg.topic === 'start') {\n  if (running) return null;\n  i = 0;\n} else if (msg.topic === 'ok') {\n  if (!running) return null;\n  i = i + 1;\n} else if (msg.topic === 'failed' || msg.topic === 'land_now') {\n  context.set('i', null);\n  // Land only if the mission still has the aircraft. If the pilot took it back (left Offboard,\n  // disengaged), a failed step means \"stood down\", and landing under a pilot is wrong.\n  if (!flow.get('tagnav_go') && msg.topic !== 'land_now') { node.status({ fill: 'grey', shape: 'ring', text: 'failed: pilot has the aircraft' }); return null; }\n  node.status({ fill: 'red', shape: 'ring', text: 'failed → land' });\n  return [null, { cmd: { command: 'land', parameter: 0, timeout: 30 }, topic: 'land' }];\n} else if (msg.topic === 'reset') {\n  context.set('i', null); node.status({}); return null;\n} else { return null; }\nif (i >= STEPS.length) { context.set('i', null); node.status({ fill: 'green', shape: 'dot', text: 'mission complete' }); return null; }\ncontext.set('i', i);\nnode.status({ fill: 'blue', shape: 'dot', text: `step ${i + 1}/${STEPS.length}: ${STEPS[i].command}` });\nreturn [{ cmd: STEPS[i], topic: 'step' }, null];",
        "id": "tagnav_mission",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_build",
                "tagnav_ui_stepfn"
            ],
            [
                "tagnav_build"
            ]
        ]
    },
    {
        "type": "switch",
        "name": "route result",
        "property": "topic",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "step",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "pilot_arm",
                "vt": "str"
            }
        ],
        "checkall": "false",
        "repair": false,
        "outputs": 2,
        "x": 1380,
        "y": 200,
        "id": "tagnav_gate_ok",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_ok"
            ],
            [
                "tagnav_pilot_takeoff"
            ]
        ]
    },
    {
        "type": "switch",
        "name": "mission step?",
        "property": "topic",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "step",
                "vt": "str"
            }
        ],
        "checkall": "false",
        "repair": false,
        "outputs": 1,
        "x": 1380,
        "y": 400,
        "id": "tagnav_gate_fail",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_fail"
            ]
        ]
    },
    {
        "type": "change",
        "name": "→ ok",
        "rules": [
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "ok",
                "tot": "str"
            }
        ],
        "x": 1560,
        "y": 200,
        "id": "tagnav_ok",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_mission"
            ]
        ]
    },
    {
        "type": "change",
        "name": "→ failed",
        "rules": [
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "failed",
                "tot": "str"
            }
        ],
        "x": 1560,
        "y": 400,
        "id": "tagnav_fail",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_mission"
            ]
        ]
    },
    {
        "type": "ros2-subscribe",
        "name": "/dexi/tag_nav/status",
        "server": "27129f80a96ce509",
        "topicname": "/dexi/tag_nav/status",
        "messagetype": "std_msgs/msg/String",
        "x": 120,
        "y": 120,
        "id": "tagnav_sub",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_edge",
                "tagnav_ui_statefn"
            ]
        ]
    },
    {
        "type": "function",
        "name": "engaged + OFFBOARD edge",
        "x": 360,
        "y": 120,
        "outputs": 2,
        "func": "// tag_nav publishes JSON at 5 Hz. Output 1: START on the rising edge of \"engaged, armed, in OFFBOARD\"\n// (RC channel 7 or the ENGAGE button), RESET on the falling edge. Output 2: re-arm the hand-off\n// stream whenever the aircraft disarms, because landing stops the manager's setpoint stream.\nlet st = {}; try { st = JSON.parse(msg.payload.data || '{}'); } catch (e) { return null; }\nconst go = st.engaged === true && st.nav_state === 14 && st.armed === true;\nconst was = context.get('go') || false;\ncontext.set('go', go); flow.set('tagnav_go', go);   // the mission node reads this on a failure\nconst armed = st.armed === true, wasArmed = context.get('armed');\ncontext.set('armed', armed); flow.set('tagnav_armed', armed);\nlet out1 = null, out2 = null;\n// trace: last 40 samples the trigger saw (read with GET /context/node/tagnav_edge)\nconst tr = context.get('trace') || [];\ntr.push([new Date().toISOString().slice(11, 23), st.engaged ? 1 : 0, st.nav_state, st.armed ? 1 : 0, go ? 1 : 0]);\nwhile (tr.length > 40) tr.shift();\ncontext.set('trace', tr);\nif (go && !was) { node.status({ fill: 'green', shape: 'dot', text: 'hand-off → mission' }); out1 = { topic: 'start' }; }\nif (!go && was) { node.status({ fill: 'grey', shape: 'ring', text: 'stood down' }); out1 = { topic: 'reset' }; }\nif (wasArmed === true && !armed) out2 = { topic: 'rearm' };\nreturn [out1, out2];",
        "id": "tagnav_edge",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_mission"
            ],
            [
                "tagnav_arm_fn"
            ]
        ]
    },
    {
        "type": "inject",
        "name": "ARM hand-off (runs once at deploy)",
        "props": [
            {
                "p": "topic",
                "vt": "str",
                "v": "armhandoff"
            }
        ],
        "repeat": "",
        "once": true,
        "onceDelay": 5,
        "topic": "armhandoff",
        "x": 160,
        "y": 460,
        "id": "tagnav_inj_arm",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_arm_fn"
            ]
        ]
    },
    {
        "type": "function",
        "name": "start_setpoint_stream",
        "x": 440,
        "y": 460,
        "outputs": 1,
        "func": "\n// Stream-only heartbeat: the setpoint follows the aircraft until the pilot enters\n// Offboard, then latches where the aircraft is. Never commands Offboard itself.\nreturn { cmd: { command: 'start_setpoint_stream', parameter: 0, timeout: 5 }, topic: 'prep' };",
        "id": "tagnav_arm_fn",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_build"
            ]
        ]
    },
    {
        "type": "inject",
        "name": "ENGAGE (button hand-off)",
        "props": [
            {
                "p": "payload",
                "v": "{\"data\":true}",
                "vt": "json"
            }
        ],
        "repeat": "",
        "once": false,
        "payload": "{\"data\":true}",
        "payloadType": "json",
        "x": 140,
        "y": 560,
        "id": "tagnav_inj_engage",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_pub_engage",
                "tagnav_engage_fn"
            ]
        ]
    },
    {
        "type": "inject",
        "name": "DISENGAGE",
        "props": [
            {
                "p": "payload",
                "v": "{\"data\":false}",
                "vt": "json"
            }
        ],
        "repeat": "",
        "once": false,
        "payload": "{\"data\":false}",
        "payloadType": "json",
        "x": 110,
        "y": 600,
        "id": "tagnav_inj_disengage",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_pub_engage",
                "tagnav_engage_fn"
            ]
        ]
    },
    {
        "type": "ros2-publish",
        "name": "/dexi/tag_nav/engage",
        "server": "27129f80a96ce509",
        "topicname": "/dexi/tag_nav/engage",
        "messagetype": "std_msgs/msg/Bool",
        "stampheader": false,
        "x": 420,
        "y": 580,
        "id": "tagnav_pub_engage",
        "z": "tagnav_tab",
        "wires": []
    },
    {
        "type": "function",
        "name": "engage → switch_offboard_mode / disengage → switch_hold_mode",
        "x": 460,
        "y": 640,
        "outputs": 1,
        "func": "// The button does what the RC switch does: engage tag_nav AND put PX4 in Offboard.\n// Offboard needs setpoints flowing, so (re)start the stream first, then switch 600 ms later.\n// Disengage drops back to Hold so the pilot has a sane mode to fly out of.\nconst on = msg.payload && msg.payload.data === true;\nif (!on) return { cmd: { command: 'switch_hold_mode', parameter: 0, timeout: 5 }, topic: 'prep' };\nnode.send({ cmd: { command: 'start_setpoint_stream', parameter: 0, timeout: 5 }, topic: 'prep' });\nsetTimeout(() => node.send({ cmd: { command: 'switch_offboard_mode', parameter: 0, timeout: 5 }, topic: 'prep' }), 600);\nreturn null;",
        "id": "tagnav_engage_fn",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_build"
            ]
        ]
    },
    {
        "type": "inject",
        "name": "PILOT: arm + takeoff 1.5 m (sim only)",
        "props": [
            {
                "p": "topic",
                "vt": "str",
                "v": "pilot"
            }
        ],
        "repeat": "",
        "once": false,
        "topic": "pilot",
        "x": 160,
        "y": 700,
        "id": "tagnav_inj_pilot",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_pilot_fn"
            ]
        ]
    },
    {
        "type": "function",
        "name": "arm (takeoff follows on success)",
        "x": 440,
        "y": 700,
        "outputs": 1,
        "func": "\n// Stand-in for the pilot in the simulator. On the aircraft the pilot takes off by hand.\n// Sends arm; the takeoff goes out only after PX4 confirms the arm (see 'then takeoff').\nreturn { cmd: { command: 'arm', parameter: 0, timeout: 10 }, topic: 'pilot_arm' };",
        "id": "tagnav_pilot_fn",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_build"
            ]
        ]
    },
    {
        "type": "inject",
        "name": "START mission now (bench/sim)",
        "props": [
            {
                "p": "topic",
                "vt": "str",
                "v": "start"
            }
        ],
        "repeat": "",
        "once": false,
        "topic": "start",
        "x": 140,
        "y": 240,
        "id": "tagnav_inj_start",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_mission"
            ]
        ]
    },
    {
        "type": "inject",
        "name": "RESET",
        "props": [
            {
                "p": "topic",
                "vt": "str",
                "v": "reset"
            }
        ],
        "repeat": "",
        "once": false,
        "topic": "reset",
        "x": 100,
        "y": 360,
        "id": "tagnav_inj_reset",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_mission"
            ]
        ]
    },
    {
        "type": "inject",
        "name": "LAND now",
        "props": [
            {
                "p": "topic",
                "vt": "str",
                "v": "land_now"
            }
        ],
        "repeat": "",
        "once": false,
        "topic": "land_now",
        "x": 100,
        "y": 400,
        "id": "tagnav_inj_land",
        "z": "tagnav_tab",
        "wires": [
            [
                "tagnav_mission"
            ]
        ]
    },
    {
        "id": "tagnav_pilot_takeoff",
        "type": "function",
        "z": "tagnav_tab",
        "name": "then takeoff 1.5 m",
        "x": 1560,
        "y": 120,
        "outputs": 1,
        "func": "return { cmd: { command: 'takeoff', parameter: 1.5, timeout: 30 }, topic: 'pilot' };",
        "wires": [
            [
                "tagnav_build"
            ]
        ]
    },
    {
        "id": "tagnav_ui_base",
        "type": "ui_base",
        "theme": {
            "name": "theme-dark",
            "lightTheme": {
                "default": "#0094CE",
                "baseColor": "#0094CE",
                "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
                "edited": true,
                "reset": false
            },
            "darkTheme": {
                "default": "#097479",
                "baseColor": "#2A9D8F",
                "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
                "edited": true,
                "reset": false
            },
            "themeState": {
                "base-color": {
                    "default": "#097479",
                    "value": "#2A9D8F",
                    "edited": true
                },
                "page-titlebar-backgroundColor": {
                    "value": "#2A9D8F",
                    "edited": false
                },
                "page-backgroundColor": {
                    "value": "#0b0f14",
                    "edited": false
                },
                "page-sidebar-backgroundColor": {
                    "value": "#000000",
                    "edited": false
                },
                "group-textColor": {
                    "value": "#5fd4c7",
                    "edited": false
                },
                "group-borderColor": {
                    "value": "#1f2a30",
                    "edited": false
                },
                "group-backgroundColor": {
                    "value": "#10161c",
                    "edited": false
                },
                "widget-textColor": {
                    "value": "#eeeeee",
                    "edited": false
                },
                "widget-backgroundColor": {
                    "value": "#2A9D8F",
                    "edited": false
                },
                "widget-borderColor": {
                    "value": "#10161c",
                    "edited": false
                },
                "base-font": {
                    "value": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
                }
            },
            "angularTheme": {
                "primary": "indigo",
                "accents": "blue",
                "warn": "red",
                "background": "grey",
                "palette": "light"
            }
        },
        "site": {
            "name": "DEXI",
            "hideToolbar": "false",
            "allowSwipe": "false",
            "lockMenu": "false",
            "allowTempTheme": "true",
            "dateFormat": "M/D/YYYY",
            "sizes": {
                "sx": 48,
                "sy": 48,
                "gx": 6,
                "gy": 6,
                "cx": 6,
                "cy": 6,
                "px": 0,
                "py": 0
            }
        }
    },
    {
        "id": "tagnav_ui_tab",
        "type": "ui_tab",
        "name": "DEXI April Tag Navigation",
        "icon": "dashboard",
        "disabled": false,
        "hidden": false
    },
    {
        "id": "tagnav_ui_cam",
        "type": "ui_group",
        "name": "Downward camera",
        "tab": "tagnav_ui_tab",
        "order": 1,
        "disp": true,
        "width": "12",
        "collapse": false,
        "className": ""
    },
    {
        "id": "tagnav_ui_grp",
        "type": "ui_group",
        "name": "Mission",
        "tab": "tagnav_ui_tab",
        "order": 2,
        "disp": true,
        "width": "12",
        "collapse": false,
        "className": ""
    },
    {
        "id": "tagnav_ui_camera",
        "type": "ui_template",
        "z": "tagnav_tab",
        "group": "tagnav_ui_cam",
        "name": "camera",
        "order": 1,
        "width": "12",
        "height": "9",
        "format": "<div style=\"margin:-6px;position:relative\"><img id=\"tagnav-cam\" style=\"width:100%;display:block;background:#000;aspect-ratio:4/3;border-radius:6px\" alt=\"\">\n<div id=\"tagnav-cam-note\" style=\"position:absolute;right:8px;bottom:8px;font:12px/1.4 sans-serif;color:#fff;background:rgba(0,0,0,.45);padding:2px 8px;border-radius:10px\">connecting…</div></div>\n<script>\n(function () {\n  var img = document.getElementById('tagnav-cam'), note = document.getElementById('tagnav-cam-note');\n  var url = 'ws://' + location.hostname + ':9090', n = 0, t0 = Date.now(), ws;\n  function connect() {\n    ws = new WebSocket(url);\n    ws.onopen = function () {\n      note.textContent = 'live';\n      ws.send(JSON.stringify({ op: 'subscribe', id: 'tagnav-cam', topic: '/cam0/image_raw/compressed',\n        type: 'sensor_msgs/msg/CompressedImage', throttle_rate: 200, queue_length: 1 }));\n    };\n    ws.onmessage = function (ev) {\n      var m = JSON.parse(ev.data); if (m.op !== 'publish' || !m.msg || !m.msg.data) return;\n      img.src = 'data:image/jpeg;base64,' + m.msg.data; n++;\n      if (Date.now() - t0 > 1000) { note.textContent = 'live · ' + n + ' fps'; n = 0; t0 = Date.now(); }\n    };\n    ws.onclose = function () { note.textContent = 'reconnecting…'; setTimeout(connect, 2000); };\n    ws.onerror = function () { ws.close(); };\n  }\n  connect();\n})();\n</script>",
        "storeOutMessages": false,
        "fwdInMessages": false,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 1180,
        "y": 620,
        "wires": [
            []
        ]
    },
    {
        "id": "tagnav_ui_tagsub",
        "type": "ros2-subscribe",
        "z": "tagnav_tab",
        "name": "/apriltag_detections",
        "server": "27129f80a96ce509",
        "topicname": "/apriltag_detections",
        "messagetype": "apriltag_msgs/msg/AprilTagDetectionArray",
        "x": 140,
        "y": 820,
        "wires": [
            [
                "tagnav_ui_tagfn"
            ]
        ]
    },
    {
        "id": "tagnav_ui_tagfn",
        "type": "function",
        "z": "tagnav_tab",
        "name": "tag ids",
        "x": 360,
        "y": 820,
        "outputs": 1,
        "func": "const ids = (msg.payload.detections || []).map(d => d.id);\nconst txt = ids.length ? ids.join(', ') : '—';\nconst now = Date.now(), force = now - (context.get('t') || 0) > 2000;\nif (force) context.set('t', now);\nif (!force && txt === context.get('last')) return null;\ncontext.set('last', txt);\nreturn { payload: txt };",
        "wires": [
            [
                "tagnav_ui_tag"
            ]
        ]
    },
    {
        "id": "tagnav_ui_tag",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 1,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "Tag in view",
        "format": "<b>{{msg.payload}}</b>",
        "layout": "row-spread",
        "className": "",
        "x": 560,
        "y": 820,
        "wires": []
    },
    {
        "id": "tagnav_ui_statefn",
        "type": "function",
        "z": "tagnav_tab",
        "name": "aircraft state text",
        "x": 380,
        "y": 180,
        "outputs": 5,
        "func": "let st = {}; try { st = JSON.parse(msg.payload.data || '{}'); } catch (e) { return null; }\nconst NAV = { 0: 'Manual', 1: 'Altitude', 2: 'Position', 3: 'Mission', 4: 'Hold', 14: 'Offboard', 15: 'Stabilized', 17: 'Takeoff', 18: 'Land' };\nconst vals = [st.armed ? 'armed' : 'disarmed', NAV[st.nav_state] || String(st.nav_state), st.engaged ? 'yes' : 'no', st.state || '—', st.alt != null ? st.alt.toFixed(2) + ' m' : '—'];\nconst now = Date.now(), force = now - (context.get('t') || 0) > 2000;   // send changed rows at once and every row every 2 s so new pages and redeploys fill in\nif (force) context.set('t', now);\nconst last = context.get('last') || [];\ncontext.set('last', vals);\nreturn vals.map((v, i) => (!force && v === last[i]) ? null : { payload: v });",
        "wires": [
            [
                "tagnav_ui_armed"
            ],
            [
                "tagnav_ui_mode"
            ],
            [
                "tagnav_ui_engaged"
            ],
            [
                "tagnav_ui_navstate"
            ],
            [
                "tagnav_ui_alt"
            ]
        ]
    },
    {
        "id": "tagnav_ui_stepfn",
        "type": "function",
        "z": "tagnav_tab",
        "name": "step text",
        "x": 560,
        "y": 380,
        "outputs": 1,
        "func": "const c = msg.cmd || {};\nconst p = c.parameter === -1 ? 'any tag' : (c.command.includes('tag') ? 'tag ' + c.parameter : '');\nconst v = c.north ? ` · ${c.north > 0 ? 'forward' : 'backward'} ${Math.abs(c.north)} m/s` : '';\nreturn { payload: `${c.command.replace(/_/g, ' ')} ${p}` + v };",
        "wires": [
            [
                "tagnav_ui_step"
            ]
        ]
    },
    {
        "id": "tagnav_ui_step",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 7,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "Mission step",
        "format": "{{msg.payload}}",
        "layout": "row-spread",
        "className": "",
        "x": 760,
        "y": 380,
        "wires": []
    },
    {
        "id": "tagnav_ui_resultfn",
        "type": "function",
        "z": "tagnav_tab",
        "name": "result text",
        "x": 1200,
        "y": 460,
        "outputs": 1,
        "func": "if (msg.topic !== 'step' && msg.topic !== 'land') return null;\nreturn { payload: (msg.payload.success ? '✓ ' : '✗ ') + (msg.payload.message || '') };",
        "wires": [
            [
                "tagnav_ui_result"
            ]
        ]
    },
    {
        "id": "tagnav_ui_result",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 8,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "Last result",
        "format": "{{msg.payload}}",
        "layout": "row-spread",
        "className": "",
        "x": 1400,
        "y": 460,
        "wires": []
    },
    {
        "id": "tagnav_ui_engage",
        "type": "ui_button",
        "z": "tagnav_tab",
        "name": "",
        "group": "tagnav_ui_grp",
        "order": 9,
        "width": "6",
        "height": "1",
        "passthru": false,
        "label": "ENGAGE",
        "tooltip": "",
        "color": "",
        "bgcolor": "#2A9D8F",
        "className": "",
        "icon": "",
        "payload": "{\"data\":true}",
        "payloadType": "json",
        "topic": "",
        "topicType": "str",
        "x": 140,
        "y": 1100,
        "wires": [
            [
                "tagnav_pub_engage",
                "tagnav_engage_fn"
            ]
        ]
    },
    {
        "id": "tagnav_ui_disengage",
        "type": "ui_button",
        "z": "tagnav_tab",
        "name": "",
        "group": "tagnav_ui_grp",
        "order": 10,
        "width": "6",
        "height": "1",
        "passthru": false,
        "label": "DISENGAGE",
        "tooltip": "",
        "color": "",
        "bgcolor": "#555555",
        "className": "",
        "icon": "",
        "payload": "{\"data\":false}",
        "payloadType": "json",
        "topic": "",
        "topicType": "str",
        "x": 140,
        "y": 1140,
        "wires": [
            [
                "tagnav_pub_engage",
                "tagnav_engage_fn"
            ]
        ]
    },
    {
        "id": "tagnav_ui_land",
        "type": "ui_button",
        "z": "tagnav_tab",
        "name": "",
        "group": "tagnav_ui_grp",
        "order": 11,
        "width": "6",
        "height": "1",
        "passthru": false,
        "label": "LAND",
        "tooltip": "",
        "color": "",
        "bgcolor": "#E76F51",
        "className": "",
        "icon": "",
        "payload": "",
        "payloadType": "str",
        "topic": "land_now",
        "topicType": "str",
        "x": 140,
        "y": 1180,
        "wires": [
            [
                "tagnav_mission"
            ]
        ]
    },
    {
        "id": "tagnav_ui_reset",
        "type": "ui_button",
        "z": "tagnav_tab",
        "name": "",
        "group": "tagnav_ui_grp",
        "order": 12,
        "width": "6",
        "height": "1",
        "passthru": false,
        "label": "RESET",
        "tooltip": "",
        "color": "",
        "bgcolor": "#555555",
        "className": "",
        "icon": "",
        "payload": "",
        "payloadType": "str",
        "topic": "reset",
        "topicType": "str",
        "x": 140,
        "y": 1220,
        "wires": [
            [
                "tagnav_mission"
            ]
        ]
    },
    {
        "id": "tagnav_ui_armed",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 2,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "Armed",
        "format": "<b>{{msg.payload}}</b>",
        "layout": "row-spread",
        "className": "",
        "x": 620,
        "y": 140,
        "wires": []
    },
    {
        "id": "tagnav_ui_mode",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 3,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "Flight mode",
        "format": "<b>{{msg.payload}}</b>",
        "layout": "row-spread",
        "className": "",
        "x": 620,
        "y": 180,
        "wires": []
    },
    {
        "id": "tagnav_ui_engaged",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 4,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "Engaged",
        "format": "<b>{{msg.payload}}</b>",
        "layout": "row-spread",
        "className": "",
        "x": 620,
        "y": 220,
        "wires": []
    },
    {
        "id": "tagnav_ui_navstate",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 5,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "tag_nav",
        "format": "<b>{{msg.payload}}</b>",
        "layout": "row-spread",
        "className": "",
        "x": 620,
        "y": 260,
        "wires": []
    },
    {
        "id": "tagnav_ui_alt",
        "type": "ui_text",
        "z": "tagnav_tab",
        "group": "tagnav_ui_grp",
        "order": 6,
        "width": "12",
        "height": "1",
        "name": "",
        "label": "Height",
        "format": "<b>{{msg.payload}}</b>",
        "layout": "row-spread",
        "className": "",
        "x": 620,
        "y": 300,
        "wires": []
    },
    {
        "id": "tagnav_rearm_after_land",
        "type": "function",
        "z": "tagnav_tab",
        "name": "re-arm stream after land",
        "x": 1200,
        "y": 540,
        "outputs": 1,
        "func": "if ((msg.cmd || {}).command !== 'land') return null;\n// the manager stops its setpoint stream when a landing starts; bring it back for the next hand-off\nsetTimeout(() => node.send({ cmd: { command: 'start_setpoint_stream', parameter: 0, timeout: 5 }, topic: 'prep' }), 2000);\nreturn null;",
        "wires": [
            [
                "tagnav_build"
            ]
        ]
    }
]
