351 lines
11 KiB
HTML
351 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>地磅协议解析/生成工具</title>
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: "微软雅黑", "Segoe UI", sans-serif;
|
||
}
|
||
|
||
html {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
}
|
||
|
||
body {
|
||
background: #cfe3ee;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.container {
|
||
width: 90%;
|
||
background: #ffffff;
|
||
padding: 30px;
|
||
border-radius: 16px;
|
||
box-shadow: 0 8px 24px rgba(82, 146, 192, 0.08);
|
||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||
}
|
||
|
||
.container:hover {
|
||
box-shadow: 0 12px 32px rgba(82, 146, 192, 0.12);
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
margin-bottom: 35px;
|
||
font-size: 26px;
|
||
font-weight: 600;
|
||
padding-bottom: 15px;
|
||
border-bottom: 1px solid #bedeed;
|
||
color: #355389;
|
||
}
|
||
|
||
/* 新增:卡片左右布局容器 */
|
||
.cards-wrapper {
|
||
display: flex;
|
||
gap: 25px; /* 两个卡片之间的间距 */
|
||
flex-wrap: wrap; /* 移动端自动换行,保证响应式 */
|
||
}
|
||
|
||
.section {
|
||
/* 调整:左右布局,平分宽度,不换行时占比均等 */
|
||
flex: 1;
|
||
min-width: 300px; /* 最小宽度,保证移动端显示正常 */
|
||
margin-bottom: 0; /* 移除原有底部间距,改用wrapper的gap */
|
||
padding: 20px;
|
||
background: #f5fafe;
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(82, 146, 192, 0.1);
|
||
transition: box-shadow 0.3s ease;
|
||
}
|
||
|
||
.section:hover {
|
||
box-shadow: 0 4px 12px rgba(82, 146, 192, 0.05);
|
||
}
|
||
|
||
h2 {
|
||
/* 调整:标题居中显示 */
|
||
text-align: center;
|
||
color: #355389;
|
||
font-size: 19px;
|
||
margin-bottom: 18px;
|
||
padding: 6px 0;
|
||
/* 移除左侧边框,改为底部边框,居中更协调 */
|
||
border-left: none;
|
||
border-bottom: 2px solid #5292c0;
|
||
display: inline-block; /* 底部边框适配文字宽度 */
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
input[type="text"] {
|
||
width: 100%;
|
||
padding: 14px 16px;
|
||
border: 1px solid #94d3f2;
|
||
border-radius: 10px;
|
||
font-size: 15px;
|
||
resize: vertical;
|
||
min-height: 60px;
|
||
margin-bottom: 15px;
|
||
background: #ffffff;
|
||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||
}
|
||
|
||
textarea:focus, input[type="text"]:focus {
|
||
outline: none;
|
||
border-color: #5292c0;
|
||
box-shadow: 0 0 0 3px rgba(148, 211, 242, 0.1);
|
||
background: #ffffff;
|
||
}
|
||
|
||
button {
|
||
/* 调整:按钮宽度充满父元素 */
|
||
width: 100%;
|
||
background: #3f689e;
|
||
color: #ffffff;
|
||
border: none;
|
||
padding: 11px 22px;
|
||
border-radius: 10px;
|
||
font-size: 15px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-weight: 500;
|
||
box-shadow: 0 4px 12px rgba(63, 104, 158, 0.15);
|
||
}
|
||
|
||
button:hover {
|
||
background: #355389;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px rgba(63, 104, 158, 0.2);
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(0);
|
||
box-shadow: 0 4px 8px rgba(63, 104, 158, 0.15);
|
||
}
|
||
|
||
.result {
|
||
margin-top: 18px;
|
||
padding: 16px;
|
||
background: #ffffff;
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(82, 146, 192, 0.1);
|
||
min-height: 60px;
|
||
white-space: pre-wrap;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
color: #355389;
|
||
transition: box-shadow 0.3s ease;
|
||
height: 300px;
|
||
}
|
||
|
||
.result:hover {
|
||
box-shadow: 0 4px 12px rgba(82, 146, 192, 0.05);
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.container {
|
||
padding: 20px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 22px;
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.section {
|
||
padding: 15px;
|
||
min-width: 100%; /* 移动端独占一行 */
|
||
}
|
||
|
||
/* 移动端按钮保持100%宽度,无需额外修改 */
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>地磅协议解析/生成工具</h1>
|
||
|
||
<!-- 新增:卡片左右布局包裹容器 -->
|
||
<div class="cards-wrapper">
|
||
<!-- 协议解析区域 -->
|
||
<form class="section" id="parseForm">
|
||
<h2>1. 协议解析</h2>
|
||
<input spellcheck="false" type="text" id="parseInput" placeholder="请输入 16 进制字节序列" autocomplete="off">
|
||
<button type="submit">解析</button>
|
||
<div class="result" id="parseResult">解析结果将显示在这里</div>
|
||
</form>
|
||
|
||
<!-- 测试数据生成区域 -->
|
||
<form class="section" id="generateForm">
|
||
<h2>2. 测试数据生成</h2>
|
||
<input spellcheck="false" type="text" id="generateInput" placeholder="请输入重量" autocomplete="off">
|
||
<button type="submit">生成</button>
|
||
<div class="result" id="generateResult">生成结果将显示在这里</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function parseProtocol() {
|
||
const input = document.getElementById('parseInput').value.trim();
|
||
const resultEl = document.getElementById('parseResult');
|
||
if (!input) {
|
||
resultEl.textContent = "请输入16进制数据";
|
||
return false;
|
||
}
|
||
|
||
const bytes = input.split(/\s+/);
|
||
if (bytes.length !== 12) {
|
||
resultEl.textContent = "错误:数据必须是12个16进制字节";
|
||
return false;
|
||
}
|
||
|
||
// 验证起始和结束字节
|
||
if (bytes[0] !== '02' || bytes[11] !== '03') {
|
||
resultEl.textContent = "错误:起始字节必须是02,结束字节必须是03";
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
// 1. 符号位(第2字节)
|
||
const signByte = bytes[1];
|
||
const sign = signByte === '2B' ? '+' : signByte === '2D' ? '-' : '';
|
||
if (!sign) {
|
||
throw new Error("符号位无效(应为2B或2D)");
|
||
}
|
||
|
||
// 2. 称量数据(第3-8字节):转为数字字符
|
||
const weightChars = bytes.slice(2, 8).map(b => String.fromCharCode(parseInt(b, 16))).join('');
|
||
const weightNum = parseFloat(weightChars);
|
||
if (isNaN(weightNum)) {
|
||
throw new Error("称量数据无效");
|
||
}
|
||
|
||
// 3. 小数点位(第9字节):从右到左的位数
|
||
const decimalByte = bytes[8];
|
||
const decimalDigit = parseInt(decimalByte, 16) - 0x30;
|
||
if (decimalDigit < 0 || decimalDigit > 4) {
|
||
throw new Error("小数点位无效(应为0-4)");
|
||
}
|
||
|
||
// 计算最终重量
|
||
const weight = (sign === '-' ? -1 : 1) * (weightNum / Math.pow(10, decimalDigit));
|
||
|
||
// 4. 校验异或(可选验证)
|
||
const xorHigh = String.fromCharCode(parseInt(bytes[9], 16));
|
||
const xorLow = String.fromCharCode(parseInt(bytes[10], 16));
|
||
const xorVal = xorHigh + xorLow;
|
||
const calcXor = bytes.slice(1, 9).reduce((acc, b) => acc ^ parseInt(b, 16), 0).toString(16).padStart(2, '0').toUpperCase()
|
||
const checkPass = xorVal === calcXor;
|
||
|
||
resultEl.textContent = `解析成功:
|
||
协议字节序列:${input}
|
||
重量:${weight} 千克
|
||
校验结果:${checkPass ? '通过' : '不通过'}
|
||
详细信息:
|
||
- 起始字节:02
|
||
- 符号:${sign}(${signByte})
|
||
- 称量数据:${weightChars}(${bytes.slice(2, 8).join(' ')})
|
||
- 小数位数:${decimalDigit} 位(${decimalByte})
|
||
- 异或校验:${calcXor}(${bytes[9]} ${bytes[10]})
|
||
- 结束字节:03`;
|
||
} catch (e) {
|
||
resultEl.textContent = `解析失败:${e.message}`;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function generateProtocol() {
|
||
let input = document.getElementById('generateInput').value.trim();
|
||
const resultEl = document.getElementById('generateResult');
|
||
if (!input) {
|
||
resultEl.textContent = "请输入重量数据";
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 1. 符号位
|
||
const sign = input.startsWith('-') ? '-' : '+';
|
||
const signByte = sign === '+' ? '2B' : '2D';
|
||
let rawinput = input
|
||
if (input.startsWith('+') || input.startsWith('-')) {
|
||
input = input.substring(1);
|
||
}
|
||
|
||
// 2. 处理重量数值(转为字符串,提取整数+小数部分)
|
||
const [integerPart, decimalPart = ''] = input.split('.');
|
||
console.log(input,integerPart,decimalPart)
|
||
|
||
const fullNumStr = (integerPart + decimalPart).padStart(6, '0'); // 补0到6位
|
||
console.log(fullNumStr,444444)
|
||
|
||
if (fullNumStr.length !== 6) {
|
||
throw new Error("重量数据超出6位有效数字");
|
||
}
|
||
// 3. 小数点位(从右到左的位数)
|
||
const decimalDigit = decimalPart.length;
|
||
console.log(decimalDigit);
|
||
if (decimalDigit < 0 || decimalDigit > 4) {
|
||
throw new Error("小数位数必须在0-4之间");
|
||
}
|
||
const decimalByte = (0x30 + decimalDigit).toString(16).toUpperCase();
|
||
|
||
// 4. 构造数据字节(第3-8字节:每个字符转ASCII的16进制)
|
||
const dataBytes = fullNumStr.split('').map(c => c.charCodeAt(0).toString(16).toUpperCase());
|
||
|
||
// 5. 构造前9字节(用于计算异或)
|
||
const preBytes = [signByte, ...dataBytes, decimalByte];
|
||
// 计算异或校验值
|
||
const xorVal = preBytes.reduce((acc, b) => acc ^ parseInt(b, 16), 0);
|
||
|
||
// 6. 异或校验转ASCII(高4位和低4位分别处理)
|
||
const xorHigh = (xorVal >> 4) & 0x0F;
|
||
const xorLow = xorVal & 0x0F;
|
||
const xorHighByte = (xorHigh <= 9 ? 0x30 : 0x37) + xorHigh;
|
||
const xorLowByte = (xorLow <= 9 ? 0x30 : 0x37) + xorLow;
|
||
const xorBytes = [
|
||
xorHighByte.toString(16).toUpperCase(),
|
||
xorLowByte.toString(16).toUpperCase()
|
||
];
|
||
|
||
// 7. 完整字节序列
|
||
const fullBytes = ['02', ...preBytes, ...xorBytes, '03'];
|
||
|
||
resultEl.textContent = `生成成功:
|
||
原始重量:${rawinput} 千克
|
||
协议字节序列:${fullBytes.join(' ')}
|
||
详细信息:
|
||
- 起始字节:02
|
||
- 符号:${sign}(${signByte})
|
||
- 称量数据:${fullNumStr}(${dataBytes.join(' ')})
|
||
- 小数位数:${decimalDigit} 位(${decimalByte})
|
||
- 异或校验:${xorVal.toString(16).toUpperCase().padStart(2, '0')}(${xorBytes[0]} ${xorBytes[1]})
|
||
- 结束字节:03`;
|
||
} catch (e) {
|
||
resultEl.textContent = `生成失败:${e.message}`;
|
||
}
|
||
}
|
||
|
||
document.getElementById('parseForm').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
parseProtocol();
|
||
});
|
||
document.getElementById('generateForm').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
generateProtocol();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |