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 或 $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">提示:如果设定频率 > 1Hz(Interval < 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>