NMEA-GNSS-RTK 定位html小工具

LC76G

html 复制代码
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>LC76G PAIR 指令校验和工具</title>
  <style>
    :root { color-scheme: light dark; }
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, "PingFang SC", "Microsoft YaHei", sans-serif; margin: 16px; line-height: 1.4; }
    .wrap { max-width: 980px; margin: 0 auto; }
    h1 { font-size: 18px; margin: 0 0 10px; }
    .row { display: grid; grid-template-columns: 1fr; gap: 10px; }
    @media (min-width: 900px){ .row { grid-template-columns: 1fr 1fr; } }
    .card { border: 1px solid rgba(127,127,127,.35); border-radius: 12px; padding: 12px; }
    .card h2 { font-size: 14px; margin: 0 0 10px; opacity: .9; }
    label { display:block; font-size: 12px; opacity:.85; margin: 8px 0 6px; }
    input, select, textarea, button {
      width: 100%; box-sizing: border-box;
      border: 1px solid rgba(127,127,127,.35);
      border-radius: 10px; padding: 10px; font-size: 13px;
      background: transparent;
    }
    textarea { min-height: 110px; resize: vertical; }
    .btns { display:flex; gap:10px; margin-top:10px; flex-wrap: wrap; }
    button { cursor:pointer; width: auto; padding: 10px 14px; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
    .hint { font-size: 12px; opacity: .75; margin-top: 8px; }
    .out { display:flex; gap:10px; flex-wrap: wrap; align-items:center; }
    .pill { border: 1px solid rgba(127,127,127,.35); border-radius: 999px; padding: 6px 10px; font-size: 12px; opacity:.9; }
    .ok { color: #0a7; }
    .bad { color: #c33; }
    .grid2 { display:grid; grid-template-columns: 1fr 1fr; gap: 10px; }
    @media (max-width: 520px){ .grid2 { grid-template-columns: 1fr; } }
  </style>
</head>
<body>
<div class="wrap">
  <h1>LC76G / PAIR 指令校验和计算与配置生成</h1>

  <div class="row">
    <!-- Builder -->
    <div class="card">
      <h2>一键生成常用 PAIR 指令(自动带 *Checksum)</h2>

      <label>指令类型</label>
      <select id="cmdType">
        <option value="PAIR864">PAIR864 配置串口波特率</option>
        <option value="PAIR050">PAIR050 设置定位频率</option>
        <option value="PAIR062">PAIR062 配置 NMEA 语句输出类型/频率</option>
        <option value="PAIR066">PAIR066 配置星系</option>
        <option value="PAIR004">$PAIR004 热启动</option>
        <option value="PAIR005">$PAIR005 温启动</option>
        <option value="PAIR006">$PAIR006 冷启动</option>
        <option value="PAIR007">$PAIR007 清除配置并冷启动</option>
      </select>

      <div id="formArea"></div>

      <label>生成结果(可直接复制发送)</label>
      <textarea id="builtOut" class="mono" readonly></textarea>

      <div class="btns">
        <button id="btnCopyBuilt">复制结果</button>
        <button id="btnFillExample">填入示例</button>
      </div>
      <div class="hint">校验和算法:对 <span class="mono">$</span> 与 <span class="mono">*</span> 之间所有 ASCII 字符做 XOR,结果转两位十六进制大写。</div>
    </div>

    <!-- Checksum calc/verify -->
    <div class="card">
      <h2>校验和计算 / 校验(任意 NMEA/PAIR 语句)</h2>

      <label>输入语句(支持带或不带 $、*XX)</label>
      <textarea id="rawIn" class="mono" placeholder="$PAIR050,1000&#10;或 $PAIR864,0,0,115200*1B"></textarea>

      <div class="grid2">
        <div>
          <label>计算得到的 Checksum</label>
          <input id="csOut" class="mono" readonly />
        </div>
        <div>
          <label>拼接后的完整语句</label>
          <input id="fullOut" class="mono" readonly />
        </div>
      </div>

      <div class="out" style="margin-top:10px">
        <span class="pill">校验结果:<span id="verifyBadge" class="mono">---</span></span>
        <span class="pill">提取内容:<span id="payloadBadge" class="mono">---</span></span>
      </div>

      <div class="btns">
        <button id="btnCalc">计算</button>
        <button id="btnCopyFull">复制完整语句</button>
        <button id="btnClear">清空</button>
      </div>

      <div class="hint">提示:如果设定频率 &gt; 1Hz(Interval &lt; 1000),仅 RMC/GGA 会按设定频率输出,其余 NMEA 仍为 1Hz。</div>
    </div>
  </div>
</div>

<script>
  // ---------- checksum helpers ----------
  function toHex2(n) {
    return (n & 0xFF).toString(16).toUpperCase().padStart(2, '0');
  }
  function nmeaChecksum(payload) {
    // payload: string between $ and *
    let cs = 0;
    for (let i = 0; i < payload.length; i++) cs ^= payload.charCodeAt(i);
    return toHex2(cs);
  }
  function normalizeInput(s) {
    s = (s || "").trim();
    if (!s) return { payload: "", hasDollar:false, hasStar:false, providedCS:"" };

    let hasDollar = s.startsWith("$");
    if (hasDollar) s = s.slice(1);

    let providedCS = "";
    let payload = s;
    let hasStar = false;

    const starIdx = s.indexOf("*");
    if (starIdx >= 0) {
      hasStar = true;
      payload = s.slice(0, starIdx);
      providedCS = s.slice(starIdx + 1).trim().toUpperCase();
      // keep only first 2 hex chars if present
      if (providedCS.length >= 2) providedCS = providedCS.slice(0, 2);
    }
    payload = payload.trim();
    return { payload, hasDollar, hasStar, providedCS };
  }
  function buildFull(payload) {
    const cs = nmeaChecksum(payload);
    return { cs, full: `$${payload}*${cs}` };
  }

  // ---------- UI: builder ----------
  const cmdType = document.getElementById("cmdType");
  const formArea = document.getElementById("formArea");
  const builtOut = document.getElementById("builtOut");

  function el(tag, attrs={}, children=[]) {
    const e = document.createElement(tag);
    Object.entries(attrs).forEach(([k,v]) => {
      if (k === "class") e.className = v;
      else if (k === "text") e.textContent = v;
      else e.setAttribute(k, v);
    });
    children.forEach(c => e.appendChild(c));
    return e;
  }

  function renderForm() {
    formArea.innerHTML = "";
    const t = cmdType.value;

    if (t === "PAIR864") {
      // $PAIR864,<PortType>,<PortIndex>,<Baudrate>
      const portType = el("select", { id:"f_portType" }, [
        el("option", { value:"0", text:"0 (NMEA 串口)" }),
        el("option", { value:"1", text:"1 (保留/其他)" }),
      ]);
      const portIndex = el("select", { id:"f_portIndex" }, [
        el("option", { value:"0", text:"0" }),
        el("option", { value:"1", text:"1" }),
      ]);
      const baud = el("select", { id:"f_baud" }, [
        "9600","115200","230400","460800","921600","3000000"
      ].map(v => el("option", { value:v, text:v + (v==="115200" ? " (default)" : "") })));

      formArea.appendChild(el("label", { text:"PortType" }));
      formArea.appendChild(portType);
      formArea.appendChild(el("label", { text:"PortIndex" }));
      formArea.appendChild(portIndex);
      formArea.appendChild(el("label", { text:"Baudrate" }));
      formArea.appendChild(baud);

    } else if (t === "PAIR050") {
      const interval = el("input", { id:"f_interval", type:"number", min:"100", max:"1000", value:"1000" });
      formArea.appendChild(el("label", { text:"Interval (ms) 100--1000, 1000=1Hz, 500=2Hz, 100=10Hz" }));
      formArea.appendChild(interval);

    } else if (t === "PAIR062") {
      // $PAIR062,<Type>,<OutputRate>
      // Example: $PAIR062,0,3  => "GGAT" every 3 fixes (per user's text)
      const type = el("input", { id:"f_type", type:"number", value:"0", min:"0" });
      const rate = el("input", { id:"f_rate", type:"number", value:"3", min:"1" });
      formArea.appendChild(el("label", { text:"Type (按协议定义,例如 0=GGAT...)" }));
      formArea.appendChild(type);
      formArea.appendChild(el("label", { text:"OutputRate (例如 3=每定位3次输出一次)" }));
      formArea.appendChild(rate);

    } else if (t === "PAIR066") {
      // $PAIR066,<GPS>,<GLO>,<GAL>,<BDS>,<QZSS>,0
      const makeOnOff = (id, label) => {
        const sel = el("select", { id }, [
          el("option", { value:"1", text:"1 Enable" }),
          el("option", { value:"0", text:"0 Disable" })
        ]);
        return [el("label", { text: label }), sel];
      };
      const items = [
        ...makeOnOff("f_gps","GPS_Enabled"),
        ...makeOnOff("f_glo","GLONASS_Enabled"),
        ...makeOnOff("f_gal","Galileo_Enabled"),
        ...makeOnOff("f_bds","BDS_Enabled"),
        ...makeOnOff("f_qzss","QZSS_Enabled"),
      ];
      items.forEach(n => formArea.appendChild(n));
      formArea.appendChild(el("div", { class:"hint", text:"最后一个参数固定为 0(保持与示例一致)" }));

    } else {
      // PAIR004/005/006/007 no params
      formArea.appendChild(el("div", { class:"hint", text:"该指令无需参数,直接生成即可。" }));
    }

    generateBuilt();
    // attach change listeners
    formArea.querySelectorAll("input,select").forEach(x => x.addEventListener("input", generateBuilt));
    formArea.querySelectorAll("select").forEach(x => x.addEventListener("change", generateBuilt));
  }

  function generateBuilt() {
    const t = cmdType.value;
    let payload = "";

    if (t === "PAIR864") {
      const pt = document.getElementById("f_portType")?.value ?? "0";
      const pi = document.getElementById("f_portIndex")?.value ?? "0";
      const bd = document.getElementById("f_baud")?.value ?? "115200";
      payload = `PAIR864,${pt},${pi},${bd}`;
    } else if (t === "PAIR050") {
      const iv = document.getElementById("f_interval")?.value ?? "1000";
      payload = `PAIR050,${iv}`;
    } else if (t === "PAIR062") {
      const ty = document.getElementById("f_type")?.value ?? "0";
      const rt = document.getElementById("f_rate")?.value ?? "3";
      payload = `PAIR062,${ty},${rt}`;
    } else if (t === "PAIR066") {
      const gps = document.getElementById("f_gps")?.value ?? "1";
      const glo = document.getElementById("f_glo")?.value ?? "1";
      const gal = document.getElementById("f_gal")?.value ?? "1";
      const bds = document.getElementById("f_bds")?.value ?? "1";
      const qz  = document.getElementById("f_qzss")?.value ?? "0";
      payload = `PAIR066,${gps},${glo},${gal},${bds},${qz},0`;
    } else if (t === "PAIR004") payload = "PAIR004";
    else if (t === "PAIR005") payload = "PAIR005";
    else if (t === "PAIR006") payload = "PAIR006";
    else if (t === "PAIR007") payload = "PAIR007";

    const { full } = buildFull(payload);
    builtOut.value = full;
  }

  cmdType.addEventListener("change", renderForm);

  document.getElementById("btnCopyBuilt").addEventListener("click", async () => {
    if (!builtOut.value) return;
    await navigator.clipboard.writeText(builtOut.value);
  });

  document.getElementById("btnFillExample").addEventListener("click", () => {
    const t = cmdType.value;
    if (t === "PAIR864") {
      document.getElementById("f_portType").value = "0";
      document.getElementById("f_portIndex").value = "0";
      document.getElementById("f_baud").value = "115200";
    } else if (t === "PAIR050") {
      document.getElementById("f_interval").value = "1000";
    } else if (t === "PAIR062") {
      document.getElementById("f_type").value = "0";
      document.getElementById("f_rate").value = "3";
    } else if (t === "PAIR066") {
      document.getElementById("f_gps").value = "1";
      document.getElementById("f_glo").value = "1";
      document.getElementById("f_gal").value = "1";
      document.getElementById("f_bds").value = "1";
      document.getElementById("f_qzss").value = "0";
    }
    generateBuilt();
  });

  // ---------- UI: checksum calc ----------
  const rawIn = document.getElementById("rawIn");
  const csOut = document.getElementById("csOut");
  const fullOut = document.getElementById("fullOut");
  const verifyBadge = document.getElementById("verifyBadge");
  const payloadBadge = document.getElementById("payloadBadge");

  function calcNow() {
    const { payload, hasStar, providedCS } = normalizeInput(rawIn.value);
    payloadBadge.textContent = payload ? payload : "---";
    if (!payload) {
      csOut.value = "";
      fullOut.value = "";
      verifyBadge.textContent = "---";
      verifyBadge.className = "mono";
      return;
    }
    const cs = nmeaChecksum(payload);
    csOut.value = cs;
    fullOut.value = `$${payload}*${cs}`;

    if (hasStar && providedCS) {
      const ok = (providedCS === cs);
      verifyBadge.textContent = ok ? `OK (提供 ${providedCS})` : `FAIL (提供 ${providedCS})`;
      verifyBadge.className = "mono " + (ok ? "ok" : "bad");
    } else {
      verifyBadge.textContent = "未提供校验和";
      verifyBadge.className = "mono";
    }
  }

  document.getElementById("btnCalc").addEventListener("click", calcNow);
  rawIn.addEventListener("input", calcNow);

  document.getElementById("btnCopyFull").addEventListener("click", async () => {
    if (!fullOut.value) return;
    await navigator.clipboard.writeText(fullOut.value);
  });

  document.getElementById("btnClear").addEventListener("click", () => {
    rawIn.value = "";
    calcNow();
  });

  // init
  renderForm();
  calcNow();
</script>
</body>
</html>
相关推荐
Tony Bai2 小时前
【API 设计之道】04 字段掩码模式:让前端决定后端返回什么
前端
爱吃大芒果2 小时前
Flutter 主题与深色模式:全局样式统一与动态切换
开发语言·javascript·flutter·ecmascript·gitcode
苏打水com2 小时前
第十四篇:Day40-42 前端架构设计入门——从“功能实现”到“架构思维”(对标职场“大型项目架构”需求)
前端·架构
king王一帅2 小时前
流式渲染 Incremark、ant-design-x markdown、streammarkdown-vue 全流程方案对比
前端·javascript·人工智能
苏打水com3 小时前
第十八篇:Day52-54 前端跨端开发进阶——从“多端适配”到“跨端统一”(对标职场“全栈化”需求)
前端
Bigger3 小时前
后端拒写接口?前端硬核自救:纯前端实现静态资源下载全链路解析
前端·浏览器·vite
BD_Marathon3 小时前
【JavaWeb】路径问题_前端绝对路径问题
前端
whyfail4 小时前
Vue原理(暴力版)
前端·vue.js