323 lines
20 KiB
HTML
323 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>🔋 6S LiPo Charger – Blockly Simulation (SAFE v4)</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<!-- Blockly -->
|
||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||
<script src="https://unpkg.com/blockly/javascript.min.js"></script>
|
||
<script src="https://unpkg.com/blockly/msg/de.js"></script>
|
||
<style>
|
||
:root{--bg:#1e1e1e;--panel:#121212;--txt:#e8e8e8;--accent:#4caf50;--muted:#a0a0a0;--line:#2a2a2a}
|
||
*{box-sizing:border-box}
|
||
html,body{height:100%;margin:0;background:var(--bg);color:var(--txt);font-family:ui-monospace, Menlo, monospace}
|
||
header{display:flex;gap:.5rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line)}
|
||
header h2{margin:0;font-size:1rem;font-weight:700}
|
||
header .sp{flex:1}
|
||
button{padding:.55rem .8rem;background:var(--accent);color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer}
|
||
.btn-sec{background:#3c99dc}.btn-warn{background:#e67e22}.btn-gray{background:#4d4d4d}
|
||
#wrap{display:flex;gap:.8rem;height:calc(100vh - 56px);padding:.6rem .8rem}
|
||
#left{flex:3;display:flex;flex-direction:column;gap:.6rem}
|
||
#right{flex:2;display:flex;flex-direction:column;gap:.6rem}
|
||
#blocklyDiv{height:60vh;width:100%}
|
||
#output{flex:1;min-height:28vh;background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;word-break:break-word;overflow:auto}
|
||
#cfg{background:var(--panel);padding:10px;border-radius:10px;white-space:pre-wrap;overflow:auto}
|
||
.hint{color:var(--muted);font-size:.9rem}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h2>🧁 Crumbforest • 6S LiPo Charger – Simulation (SAFE v4)</h2>
|
||
<div class="sp"></div>
|
||
<button id="btnDoc" class="btn-gray">📄 Doku</button>
|
||
<button id="btnT1" class="btn-sec" title="Storage 3,8V/Z">T1</button>
|
||
<button id="btnT2" class="btn-sec" title="Full 4,2V/Z">T2</button>
|
||
<button id="btnT3" class="btn-sec" title="Safety: Drift">T3</button>
|
||
<button id="btnT4" class="btn-sec" title="HV 4,35V/Z">T4</button>
|
||
<button id="btnRnd" class="btn-warn">🎲 Zufall</button>
|
||
<button id="btnClear" class="btn-gray">🗑️ Neu</button>
|
||
<button id="btnRun">▶️ Ausführen</button>
|
||
</header>
|
||
|
||
<div id="wrap">
|
||
<div id="left">
|
||
<div id="blocklyDiv"></div>
|
||
<pre id="output">🌲 Bereit. Lade „Doku“ / Testfälle oder baue eigene Logik und drücke ▶️.</pre>
|
||
</div>
|
||
<div id="right">
|
||
<div id="cfg"></div>
|
||
<div class="hint">
|
||
Marker (6S): 19,2 V (leer) • 22,8 V (Storage 3,8/Z) • 25,2 V (Full 4,2/Z) • 26,1 V (HV 4,35/Z).
|
||
<br><b>Nur Simulation / Didaktik. Keine echte Ladefunktion.</b>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toolbox -->
|
||
<xml id="toolbox" style="display:none">
|
||
<category name="Variablen" custom="VARIABLE"></category>
|
||
<category name="Logik">
|
||
<block type="controls_if"></block>
|
||
<block type="logic_compare"></block>
|
||
</category>
|
||
<category name="Schleifen">
|
||
<block type="controls_repeat_ext"></block>
|
||
<block type="controls_whileUntil"></block>
|
||
<block type="controls_forEach"></block>
|
||
</category>
|
||
<category name="Mathe">
|
||
<block type="math_number"></block>
|
||
<block type="math_arithmetic"></block>
|
||
<block type="math_on_list"></block>
|
||
</category>
|
||
<category name="Listen">
|
||
<block type="lists_create_with"></block>
|
||
<block type="lists_getIndex"></block>
|
||
<block type="lists_setIndex"></block>
|
||
<block type="lists_length"></block>
|
||
</category>
|
||
<category name="Text">
|
||
<block type="text"></block>
|
||
<block type="text_join"></block>
|
||
<block type="text_print"></block>
|
||
</category>
|
||
</xml>
|
||
|
||
<script>
|
||
// === CFG (Gedankenstütze) ===
|
||
const CFG = {
|
||
cells: 6,
|
||
v_full: 4.20,
|
||
v_storage: 3.80,
|
||
v_hv: 4.35,
|
||
v_min: 3.20,
|
||
drift_reject: 0.30,
|
||
tick_s: 10,
|
||
dv_charge: 0.01,
|
||
dv_discharge: -0.01,
|
||
capacity_mAh: 2200,
|
||
c_rate: 1.0
|
||
};
|
||
const cfgEl = document.getElementById('cfg');
|
||
cfgEl.textContent = 'CFG (Simulation):\n' + JSON.stringify(CFG, null, 2);
|
||
|
||
// === Blockly Setup ===
|
||
const workspace = Blockly.inject('blocklyDiv', {
|
||
toolbox: document.getElementById('toolbox'),
|
||
theme: Blockly.Themes.Dark,
|
||
renderer: 'zelos',
|
||
grid: {spacing:24, length:3, colour:'#474747', snap:true},
|
||
trashcan: true,
|
||
zoom: {startScale:1.1, maxScale:2.0, minScale:.6, controls:false, wheel:true},
|
||
move: {scrollbars:true, drag:true, wheel:true}
|
||
});
|
||
|
||
// Variablen, die in den Demos genutzt werden
|
||
['mode','target_cell_v','time_s','tick_s','stop_reason','pack_v','current_a','capacity_mAh','c_rate','dv','avg','max_v','min_v','cells','i','new_v']
|
||
.forEach(v => { try { workspace.createVariable(v); } catch(_){} });
|
||
|
||
// Output/Print
|
||
const $ = s => document.querySelector(s);
|
||
function setOutput(t){ $('#output').textContent = t }
|
||
function appendOutput(t){ const el=$('#output'); el.textContent += (el.textContent.endsWith('\n')?'':'\n') + t; el.scrollTop = el.scrollHeight; }
|
||
window.appendOutput = appendOutput;
|
||
|
||
// text_print → Panel & Alerts abfangen
|
||
(function hardenPrint(){
|
||
const gen = Blockly.JavaScript;
|
||
gen['text_print'] = function(block){
|
||
const arg0 = gen.valueToCode(block, 'TEXT', gen.ORDER_NONE) || "''";
|
||
return 'appendOutput(String(' + arg0 + '));\n';
|
||
};
|
||
const origAlert = window.alert;
|
||
window.alert = function(msg){ try { appendOutput('⚠️ alert abgefangen: ' + msg); } catch(_) { origAlert(msg); } };
|
||
})();
|
||
|
||
// *** LISTEN-PATCH (1-basiert → korrektes JS-Indexing & Schreiben) ***
|
||
(function patchListsGenerators(){
|
||
const gen = Blockly.JavaScript;
|
||
gen['lists_getIndex'] = function(block){
|
||
const list = gen.valueToCode(block, 'VALUE', gen.ORDER_MEMBER) || '[]';
|
||
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
|
||
const code = `${list}[(${at}) - 1]`;
|
||
return [code, gen.ORDER_MEMBER];
|
||
};
|
||
gen['lists_setIndex'] = function(block){
|
||
const list = gen.valueToCode(block, 'LIST', gen.ORDER_MEMBER) || '[]';
|
||
const at = gen.valueToCode(block, 'AT', gen.ORDER_NONE) || '1';
|
||
const to = gen.valueToCode(block, 'TO', gen.ORDER_NONE) || 'null';
|
||
return `${list}[(${at}) - 1] = ${to};\n`;
|
||
};
|
||
})();
|
||
|
||
// XML utils
|
||
function textToDom(xmlText){
|
||
try { if (Blockly.utils?.xml?.textToDom) return Blockly.utils.xml.textToDom(xmlText); } catch(_){}
|
||
return new DOMParser().parseFromString(xmlText, 'text/xml').documentElement;
|
||
}
|
||
function loadXml(xml){ const dom = textToDom(xml); workspace.clear(); Blockly.Xml.domToWorkspace(dom, workspace); }
|
||
|
||
// --- Demos (kompakt gehalten) ---
|
||
const DOC_XML = `
|
||
<xml xmlns="https://developers.google.com/blockly/xml">
|
||
<variables>
|
||
<variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable>
|
||
<variable>stop_reason</variable><variable>pack_v</variable><variable>current_a</variable><variable>capacity_mAh</variable><variable>c_rate</variable>
|
||
<variable>cells</variable><variable>avg</variable><variable>max_v</variable><variable>min_v</variable>
|
||
</variables>
|
||
<block type="text_print" x="20" y="20"><value name="TEXT"><block type="text"><field name="TEXT">DOC: 6S LiPo Charger – Logik-Simulation (kein echtes Laden!).</field></block></value></block>
|
||
<block type="text_print" x="20" y="56"><value name="TEXT"><block type="text"><field name="TEXT">Ziele: Full 4,20V/Z • Storage 3,80V/Z • HV 4,35V/Z • Ende ~3,20V/Z.</field></block></value></block>
|
||
<block type="variables_set" x="20" y="110"><field name="VAR">capacity_mAh</field><value name="VALUE"><block type="math_number"><field name="NUM">2200</field></block></value>
|
||
<next><block type="variables_set"><field name="VAR">c_rate</field><value name="VALUE"><block type="math_number"><field name="NUM">1</field></block></value><next>
|
||
<block type="variables_set"><field name="VAR">current_a</field><value name="VALUE"><block type="math_arithmetic"><field name="OP">DIVIDE</field>
|
||
<value name="A"><block type="math_arithmetic"><field name="OP">MULTIPLY</field>
|
||
<value name="A"><block type="variables_get"><field name="VAR">capacity_mAh</field></block></value>
|
||
<value name="B"><block type="variables_get"><field name="VAR">c_rate</field></block></value>
|
||
</block></value>
|
||
<value name="B"><block type="math_number"><field name="NUM">1000</field></block></value>
|
||
</block></value><next>
|
||
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value></block>
|
||
</next></next>
|
||
</block>
|
||
<block type="text_print" x="20" y="230"><value name="TEXT"><block type="text"><field name="TEXT">Regel: Zell-Drift ≥ 0,30V → STOP (reject_imbalance).</field></block></value></block>
|
||
</xml>`.trim();
|
||
|
||
const T1_XML = `
|
||
<xml xmlns="https://developers.google.com/blockly/xml">
|
||
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>time_s</variable><variable>tick_s</variable><variable>stop_reason</variable><variable>dv</variable><variable>cells</variable><variable>i</variable><variable>new_v</variable></variables>
|
||
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">storage</field></block></value><next>
|
||
<block type="variables_set"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value><next>
|
||
<block type="variables_set"><field name="VAR">tick_s</field><value name="VALUE"><block type="math_number"><field name="NUM">10</field></block></value><next>
|
||
<block type="variables_set"><field name="VAR">time_s</field><value name="VALUE"><block type="math_number"><field name="NUM">0</field></block></value><next>
|
||
<block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">unknown</field></block></value></next></next></next></next></block>
|
||
<block type="variables_set" x="20" y="140"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>
|
||
<value name="ADD0"><block type="math_number"><field name="NUM">3.90</field></block></value>
|
||
<value name="ADD1"><block type="math_number"><field name="NUM">3.92</field></block></value>
|
||
<value name="ADD2"><block type="math_number"><field name="NUM">3.88</field></block></value>
|
||
<value name="ADD3"><block type="math_number"><field name="NUM">3.91</field></block></value>
|
||
<value name="ADD4"><block type="math_number"><field name="NUM">3.89</field></block></value>
|
||
<value name="ADD5"><block type="math_number"><field name="NUM">3.87</field></block></value>
|
||
</block></value></block>
|
||
<block type="variables_set" x="20" y="290"><field name="VAR">dv</field><value name="VALUE"><block type="math_number"><field name="NUM">-0.01</field></block></value></block>
|
||
<block type="controls_whileUntil" x="20" y="330"><field name="MODE">WHILE</field>
|
||
<value name="BOOL"><block type="logic_compare"><field name="OP">GT</field>
|
||
<value name="A"><block type="math_on_list"><mutation op="MAX"></mutation><field name="OP">MAX</field><value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
|
||
<value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value>
|
||
</block></value>
|
||
<statement name="DO">
|
||
<block type="variables_set"><field name="VAR">time_s</field>
|
||
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field><value name="A"><block type="variables_get"><field name="VAR">time_s</field></block></value><value name="B"><block type="variables_get"><field name="VAR">tick_s</field></block></value></block></value>
|
||
<next><block type="controls_forEach"><field name="VAR">i</field>
|
||
<value name="LIST"><block type="lists_create_with"><mutation items="6"></mutation>
|
||
<value name="ADD0"><block type="math_number"><field name="NUM">1</field></block></value>
|
||
<value name="ADD1"><block type="math_number"><field name="NUM">2</field></block></value>
|
||
<value name="ADD2"><block type="math_number"><field name="NUM">3</field></block></value>
|
||
<value name="ADD3"><block type="math_number"><field name="NUM">4</field></block></value>
|
||
<value name="ADD4"><block type="math_number"><field name="NUM">5</field></block></value>
|
||
<value name="ADD5"><block type="math_number"><field name="NUM">6</field></block></value>
|
||
</block></value>
|
||
<statement name="DO">
|
||
<block type="variables_set"><field name="VAR">new_v</field>
|
||
<value name="VALUE"><block type="math_arithmetic"><field name="OP">ADD</field>
|
||
<value name="A"><block type="lists_getIndex"><mutation at="true" statement="false"></mutation><field name="MODE">GET</field><field name="WHERE">FROM_START</field><value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value><value name="VALUE"><block type="variables_get"><field name="VAR">cells</field></block></value></block></value>
|
||
<value name="B"><block type="variables_get"><field name="VAR">dv</field></block></value>
|
||
</block></value>
|
||
</block>
|
||
<next><block type="controls_if">
|
||
<value name="IF0"><block type="logic_compare"><field name="OP">LT</field><value name="A"><block type="variables_get"><field name="VAR">new_v</field></block></value><value name="B"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></value>
|
||
<statement name="DO0"><block type="variables_set"><field name="VAR">new_v</field><value name="VALUE"><block type="variables_get"><field name="VAR">target_cell_v</field></block></value></block></statement>
|
||
</block></next>
|
||
<next><block type="lists_setIndex"><mutation at="true"></mutation><field name="MODE">SET</field><field name="WHERE">FROM_START</field>
|
||
<value name="AT"><block type="variables_get"><field name="VAR">i</field></block></value>
|
||
<value name="LIST"><block type="variables_get"><field name="VAR">cells</field></block></value>
|
||
<value name="TO"><block type="variables_get"><field name="VAR">new_v</field></block></value>
|
||
</block></next>
|
||
</statement>
|
||
</block></next>
|
||
</block>
|
||
</statement>
|
||
<next><block type="variables_set"><field name="VAR">stop_reason</field><value name="VALUE"><block type="text"><field name="TEXT">storage_reached</field></block></value></block></next>
|
||
</block>
|
||
<block type="text_print" x="20" y="800"><value name="TEXT"><block type="text_join"><mutation items="6"></mutation>
|
||
<value name="ADD0"><block type="text"><field name="TEXT">{ "mode":"storage", "target_cell_v":3.8, "stop_reason":"</field></block></value>
|
||
<value name="ADD1"><block type="variables_get"><field name="VAR">stop_reason</field></block></value>
|
||
<value name="ADD2"><block type="text"><field name="TEXT">", "time_s": </field></block></value>
|
||
<value name="ADD3"><block type="variables_get"><field name="VAR">time_s</field></block></value>
|
||
<value name="ADD4"><block type="text"><field name="TEXT">, "cells_v":"[/* 'cells' */]" }</field></block></value>
|
||
</block></value></block>
|
||
</xml>`.trim();
|
||
|
||
const T2_XML = /* wie T1, nur 4.2V + dv=+0.01 + MIN/Clamp im If */;
|
||
const T3_XML = /* Drift-Check */;
|
||
const T4_XML = /* HV 4.35V */;
|
||
|
||
// Buttons
|
||
document.getElementById('btnDoc').onclick = ()=>{ loadXml(DOC_XML); setOutput('📄 Doku geladen.'); };
|
||
document.getElementById('btnT1').onclick = ()=>{ loadXml(T1_XML); setOutput('🧪 T1: Storage 3,8V/Z. ▶️ für Report.'); };
|
||
document.getElementById('btnT2').onclick = ()=>{ loadXml(T2_XML); setOutput('🧪 T2: Full 4,2V/Z. ▶️ für Report.'); };
|
||
document.getElementById('btnT3').onclick = ()=>{ loadXml(T3_XML); setOutput('🧪 T3: Drift-Check. ▶️ für Report.'); };
|
||
document.getElementById('btnT4').onclick = ()=>{ loadXml(T4_XML); setOutput('🧪 T4: HV 4,35V/Z (nur Demo). ▶️ für Report.'); };
|
||
document.getElementById('btnRnd').onclick = ()=>{
|
||
const cells = Array.from({length:6},()=> (3.40 + Math.random()*0.65).toFixed(2));
|
||
const items = cells.map((v,i)=>`<value name="ADD${i}"><block type="math_number"><field name="NUM">${v}</field></block></value>`).join('');
|
||
const XML = `<xml xmlns="https://developers.google.com/blockly/xml">
|
||
<variables><variable>mode</variable><variable>target_cell_v</variable><variable>cells</variable></variables>
|
||
<block type="variables_set" x="20" y="20"><field name="VAR">mode</field><value name="VALUE"><block type="text"><field name="TEXT">inspect</field></block></value></block>
|
||
<block type="variables_set" x="20" y="60"><field name="VAR">target_cell_v</field><value name="VALUE"><block type="math_number"><field name="NUM">3.8</field></block></value></block>
|
||
<block type="variables_set" x="20" y="100"><field name="VAR">cells</field><value name="VALUE"><block type="lists_create_with"><mutation items="6"></mutation>${items}</block></value></block>
|
||
<block type="text_print" x="20" y="220"><value name="TEXT"><block type="text"><field name="TEXT">🎲 Startzellen geladen. Wähle T1/T2/T4 für Loop-Logik.</field></block></value></block>
|
||
</xml>`;
|
||
loadXml(XML);
|
||
setOutput('🎲 Zufallspack geladen.');
|
||
};
|
||
document.getElementById('btnClear').onclick = ()=>{ workspace.clear(); setOutput('🧹 Workspace geleert.'); };
|
||
|
||
// === RUN: Watchdog + Alerts→Panel + Writeback&Clamp-Fix ===
|
||
document.getElementById('btnRun').onclick = () => {
|
||
let code = Blockly.JavaScript.workspaceToCode(workspace);
|
||
|
||
const guardHeader = `
|
||
let __wf = 0, __wf_cap = 5000;
|
||
function __guard(){ if((__wf+=1) > __wf_cap){ throw new Error('Watchdog: zu viele Schleifen-Schritte (> ' + __wf_cap + ')'); } }
|
||
`;
|
||
const clampHeader = `
|
||
function __clamp(v, t, mode){
|
||
if(mode==='charge' || mode==='hv_charge') return Math.min(v, t);
|
||
if(mode==='storage') return Math.max(v, t);
|
||
return v;
|
||
}
|
||
`;
|
||
|
||
// 1) Watchdog in jede while(...)
|
||
code = guardHeader + clampHeader + code.replace(/while\s*\(/g, 'while(__guard(), ');
|
||
|
||
// 2) Alerts → Panel
|
||
code = code.replace(/window\.alert\s*\(/g, 'appendOutput(');
|
||
|
||
// 3) Erzwinge Schreibvorgang + Clamp nach "new_v = cells[...] + dv;"
|
||
// - Variante mit 1-basiertem Index: cells[(i - 1)]
|
||
code = code.replace(
|
||
/new_v\s*=\s*cells\s*\[\s*\(\s*i\s*(?:-\s*1)?\s*\)\s*\]\s*\+\s*dv\s*;/g,
|
||
"new_v = __clamp(cells[(i - 1)] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[(i - 1)] = new_v;"
|
||
);
|
||
// - Variante mit 0-basiertem Index: cells[i]
|
||
code = code.replace(
|
||
/new_v\s*=\s*cells\s*\[\s*i\s*\]\s*\+\s*dv\s*;/g,
|
||
"new_v = __clamp(cells[i] + dv, (typeof target_cell_v!=='undefined'?target_cell_v:4.2), (typeof mode!=='undefined'?mode:'charge')); cells[i] = new_v;"
|
||
);
|
||
|
||
setOutput('▶️ Ausführung…\n\n' + code);
|
||
try { new Function(code)(); appendOutput('\n✅ Fertig.'); }
|
||
catch(e){ appendOutput('\n❌ Fehler: ' + (e && e.message ? e.message : e)); }
|
||
};
|
||
</script>
|
||
|
||
<!-- Sicherheit -->
|
||
<div style="padding:.4rem .8rem;color:#fff;background:#8a0000">
|
||
⚠️ <b>Sicherheits-Hinweis:</b> Das ist nur eine <b>Simulation</b>. Echte LiPos nur mit geeignetem Ladegerät & Balancer laden.
|
||
Packs mit starker Zellabweichung (≥0,30 V) nicht verwenden. Keine echten Ströme werden hier geschaltet.
|
||
</div>
|
||
</body>
|
||
</html>
|