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>
相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax