<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>M-Bus Parser (Wired & Wireless)</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/languages/json.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #ffffff;
}
.header {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
margin-bottom: 20px;
max-width: 1100px;
margin-left: auto;
margin-right: auto;
}
.header-text {
text-align: left;
margin-left: 16px;
}
.header-controls {
display: flex;
gap: 10px;
margin-top: 10px;
margin-left: 0;
}
.header-text h1 {
margin: 0;
font-size: 2em;
color: #333;
}
.header-text .subtitle {
font-size: 0.9em;
color: #666;
margin-top: 4px;
}
.header-text .subtitle a {
color: #666;
text-decoration: none;
}
.parser-version {
font-size: 0.7em;
color: #888;
font-weight: normal;
margin-left: 8px;
}
form {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
max-width: 1100px;
margin: 0 auto;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 10px;
font-family: monospace;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 10px;
font-family: monospace;
}
.key-input-container {
position: relative;
margin-bottom: 15px;
}
.key-help {
font-size: 0.85em;
color: #666;
margin-top: 4px;
margin-bottom: 8px;
}
.key-toggle {
position: absolute;
right: 10px;
top: 10px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: #666;
}
.key-toggle:hover {
color: #333;
}
input[type="button"] {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
input[type="button"]:hover {
background-color: #45a049;
}
pre {
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
white-space: pre-wrap;
word-wrap: break-word;
}
#output {
margin-top: 20px;
max-width: 1100px;
margin: 20px auto 0;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background: #f0f0f0;
color: #222;
}
#output-container {
position: relative;
max-width: 1100px;
margin: 20px auto 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
#copy_output {
position: absolute;
top: 10px;
right: 45px;
background: #fff;
border: 1px solid #ccc;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
cursor: pointer;
padding: 8px;
z-index: 10;
transition: background 0.2s, box-shadow 0.2s;
}
#download_output {
position: absolute;
top: 10px;
right: 10px;
background: #fff;
border: 1px solid #ccc;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
cursor: pointer;
padding: 8px;
z-index: 10;
transition: background 0.2s, box-shadow 0.2s;
}
#copy_output:hover,
#download_output:hover {
background: #f0f0f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
#copy_output svg,
#download_output svg {
display: block;
}
body.dark-mode #share_button svg {
stroke: #aaa;
}
body.dark-mode {
background-color: #181a1b;
color: #e0e0e0;
}
body.dark-mode .header-text h1 {
color: #e0e0e0;
}
body.dark-mode .header-text .subtitle,
body.dark-mode .parser-version {
color: #aaa;
}
body.dark-mode form {
background: #23272a;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
body.dark-mode #output-container {
background: #23272a;
border-radius: 8px;
}
body.dark-mode textarea {
background: #23272a;
color: #e0e0e0;
border: 1px solid #444;
}
body.dark-mode input[type="text"] {
background: #23272a;
color: #e0e0e0;
border: 1px solid #444;
}
body.dark-mode .key-help {
color: #aaa;
}
body.dark-mode .key-toggle {
color: #aaa;
}
body.dark-mode .key-toggle:hover {
color: #e0e0e0;
}
body.dark-mode pre {
background: #23272a;
color: #e0e0e0;
}
body.dark-mode #output {
background: #23272a;
color: #e0e0e0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
body.dark-mode #copy_output,
body.dark-mode #download_output {
background: #23272a;
border: 1px solid #444;
}
body.dark-mode #copy_output:hover,
body.dark-mode #download_output:hover {
background: #181a1b;
}
body.dark-mode input[type="button"] {
background-color: #444;
color: #e0e0e0;
}
body.dark-mode input[type="button"]:hover {
background-color: #333;
}
body.dark-mode pre code,
body.dark-mode .hljs {
background: #23272a !important;
color: #e0e0e0 !important;
}
body.dark-mode .hljs-keyword,
body.dark-mode .hljs-selector-tag,
body.dark-mode .hljs-literal,
body.dark-mode .hljs-section,
body.dark-mode .hljs-link {
color: #ffcb6b !important;
}
body.dark-mode .hljs-string,
body.dark-mode .hljs-title,
body.dark-mode .hljs-name,
body.dark-mode .hljs-type,
body.dark-mode .hljs-attribute,
body.dark-mode .hljs-symbol,
body.dark-mode .hljs-bullet,
body.dark-mode .hljs-addition {
color: #c3e88d !important;
}
body.dark-mode .hljs-comment,
body.dark-mode .hljs-quote,
body.dark-mode .hljs-deletion {
color: #616161 !important;
}
body.dark-mode .hljs-meta {
color: #82aaff !important;
}
</style>
</head>
<body>
<div class="header">
<img id="banner" src="meter.png" alt="Meter Banner">
<div class="header-text">
<h1>Online M-Bus Parser <span class="parser-version" id="parser-version"></span></h1>
<div class="subtitle">
<a href="https://maebli.github.io/m-bus-parser" target="_blank">maebli.github.io/m-bus-parser</a> • <a href="https://github.com/maebli" target="_blank">Michael Aebli</a>
</div>
</div>
<div class="header-controls">
<button id="dark_mode_toggle" title="Toggle dark mode" style="background:none;border:none;cursor:pointer;padding:8px;">
<svg id="dark_mode_icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
</button>
<button id="share_button" title="Copy share link" style="background:none;border:none;cursor:pointer;padding:8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</button>
<button id="report_issue" title="Report Issue" style="background:none;border:none;cursor:pointer;padding:8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#e74c3c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<circle cx="12" cy="16" r="1" />
</svg>
</button>
</div>
</div>
<form>
<label for="inputstring">Input String:</label>
<textarea rows="5" cols="80"
id="inputstring">68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16</textarea>
<label for="aeskey">AES-128 Decryption Key (optional):</label>
<div class="key-input-container">
<input type="password" id="aeskey" placeholder="Enter 32 hex characters (16 bytes)" maxlength="32" pattern="[0-9A-Fa-f]{32}" />
<button type="button" class="key-toggle" id="key-toggle" title="Show/Hide key">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<div class="key-help">Leave empty for unencrypted frames. Format: 32 hexadecimal characters (e.g., 0123456789ABCDEF0123456789ABCDEF)</div>
<input id="parse_json" type="button" value="Parse to JSON" />
<input id="parse_yaml" type="button" value="Parse to YAML" />
<input id="parse_table" type="button" value="Parse to Table" />
<input id="parse_csv" type="button" value="Parse to CSV" />
<input id="parse_mermaid" type="button" value="Parse to Diagram" />
</form>
<div id="output-container">
<button id="copy_output" title="Copy Output">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1-2 2v1" />
</svg>
</button>
<a id="download_output" title="Download Output" href="#" download="">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</a>
<pre id="output"></pre>
</div>
<script type="module">
import init, { m_bus_parse, m_bus_parse_with_key, version } from "./m_bus_parser_wasm_pack.js";
let currentFormat = "json";
mermaid.initialize({ startOnLoad: false, theme: 'default' });
async function setup() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const savedMode = localStorage.getItem('darkMode');
const body = document.body;
const icon = document.getElementById('dark_mode_icon');
function setDarkMode(on) {
if (on) {
body.classList.add('dark-mode');
icon.innerHTML = '<path d="M21.64 13.64A9 9 0 1 1 12 3v0a7 7 0 0 0 9.64 10.64z" />';
} else {
body.classList.remove('dark-mode');
icon.innerHTML = '<circle cx="12" cy="12" r="5" /><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />';
}
}
let darkMode = savedMode === null ? prefersDark : savedMode === 'true';
setDarkMode(darkMode);
document.getElementById('dark_mode_toggle').addEventListener('click', () => {
darkMode = !body.classList.contains('dark-mode');
setDarkMode(darkMode);
localStorage.setItem('darkMode', darkMode);
});
const keyInput = document.getElementById('aeskey');
const keyToggle = document.getElementById('key-toggle');
keyToggle.addEventListener('click', () => {
const type = keyInput.getAttribute('type');
if (type === 'password') {
keyInput.setAttribute('type', 'text');
keyToggle.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>';
} else {
keyInput.setAttribute('type', 'password');
keyToggle.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
}
});
await init(); document.getElementById('parser-version').textContent = 'v' + version();
const urlParams = new URLSearchParams(window.location.search);
const urlData = urlParams.get('data');
const urlFormat = urlParams.get('format');
if (urlData) {
document.getElementById('inputstring').value = urlData;
parseInput(urlFormat || 'mermaid');
}
document.getElementById('parse_json').addEventListener('click', () => {
parseInput("json");
});
document.getElementById('parse_yaml').addEventListener('click', () => {
parseInput("yaml");
});
document.getElementById('parse_table').addEventListener('click', () => {
parseInput("table_format");
});
document.getElementById('parse_csv').addEventListener('click', () => {
parseInput("csv");
});
document.getElementById('parse_mermaid').addEventListener('click', () => {
parseInput("mermaid");
});
document.getElementById('copy_output').addEventListener('click', () => {
let text;
if (currentFormat === 'mermaid' && window._mermaidSource) {
text = window._mermaidSource;
} else {
const codeElem = document.getElementById('output_code');
text = codeElem ? codeElem.textContent : document.getElementById('output').textContent;
}
navigator.clipboard.writeText(text);
});
document.getElementById('share_button').addEventListener('click', () => {
const inputString = document.getElementById('inputstring').value;
const params = new URLSearchParams({ data: inputString, format: currentFormat });
const shareUrl = `${location.origin}${location.pathname}?${params}`;
navigator.clipboard.writeText(shareUrl);
const btn = document.getElementById('share_button');
btn.title = 'Copied!';
setTimeout(() => { btn.title = 'Copy share link'; }, 2000);
});
document.getElementById('report_issue').addEventListener('click', () => {
window.open('https://github.com/maebli/m-bus-parser/issues/new', '_blank');
});
async function parseInput(format) {
currentFormat = format; const inputString = document.getElementById('inputstring').value;
const aesKey = document.getElementById('aeskey').value.trim();
const params = new URLSearchParams({ data: inputString, format });
history.replaceState(null, '', `?${params}`);
let formattedResult;
if (aesKey && aesKey.length > 0) {
if (!/^[0-9A-Fa-f]{32}$/.test(aesKey)) {
alert('Invalid AES key format. Please enter exactly 32 hexadecimal characters (0-9, A-F).');
return;
}
formattedResult = m_bus_parse_with_key(inputString, format, aesKey);
} else {
formattedResult = m_bus_parse(inputString, format);
}
const outputContainer = document.getElementById('output');
const outputCode = document.getElementById('output_code');
let lang = '', extension = 'txt', mimeType = 'text/plain';
if (format === 'json') { lang = 'json'; extension = 'json'; mimeType = 'application/json'; }
else if (format === 'yaml') { lang = 'yaml'; extension = 'yaml'; mimeType = 'application/x-yaml'; }
else if (format === 'csv') { extension = 'csv'; mimeType = 'text/csv'; }
else if (format === 'table_format') { lang = 'plaintext'; extension = 'txt'; }
else if (format === 'mermaid') { extension = 'mmd'; mimeType = 'text/plain'; }
outputContainer.style.background = '';
outputContainer.style.whiteSpace = '';
if (format === 'csv') {
outputContainer.innerHTML = formattedResult;
} else if (format === 'mermaid') {
window._mermaidSource = formattedResult;
outputContainer.style.background = 'none';
outputContainer.style.whiteSpace = 'normal';
outputContainer.innerHTML = '';
const mermaidDiv = document.createElement('div');
mermaidDiv.id = 'mermaid_diagram';
mermaidDiv.textContent = formattedResult;
outputContainer.appendChild(mermaidDiv);
await mermaid.run({ nodes: [mermaidDiv] });
} else {
outputContainer.innerHTML = '<code id="output_code"></code>';
const newOutputCode = document.getElementById('output_code');
newOutputCode.className = lang ? `language-${lang}` : '';
newOutputCode.textContent = formattedResult;
if (window.hljs && lang) hljs.highlightElement(newOutputCode);
}
if (window._lastDownloadUrl) URL.revokeObjectURL(window._lastDownloadUrl);
const blob = new Blob([formattedResult], { type: mimeType });
const url = URL.createObjectURL(blob);
const downloadLink = document.getElementById('download_output');
downloadLink.href = url;
downloadLink.download = `m-bus-output.${extension}`;
window._lastDownloadUrl = url;
}
}
setup(); </script>
</body>
</html>