Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# PlatformIO build artifacts
.pio/
/.vscode/
/.idea/

# Local heap monitoring / diagnostic scripts and CSV logs (not part of firmware)
tools/

# DuckDNS credentials — keep placeholders in the repo; never commit real domain/token.
# Local file may hold real values; use: git update-index --skip-worktree src/DuckDNSUpdater.h
src/DuckDNSUpdater.h

# Node/npm
node_modules/

# Backup/configs folder should not be uploaded
ayarlar yedek/

# OS files
Thumbs.db
desktop.ini

# Logs
*.log
.pio
.pioenvs
.piolibdeps
Expand All @@ -12,3 +36,7 @@ htmljs/node_modules/
.DS_Store
.vscode/*.db
.vscode/.browse.c_cpp.db*

# Cursor local agent rules (do not commit)
.cursor/

19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

Preamble

The GNU General Public License is a free, copyleft license for
software and other kinds of works.

(This file is the GNU GPL v3 license text. For the full and
official license text, please see https://www.gnu.org/licenses/gpl-3.0.txt)

------------------------------------------------------------------------

Additional attribution:
Copyright 2012-2013 BrewPi/Elco Jacobs and contributors.
473 changes: 238 additions & 235 deletions README.md

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions docs/L0.JsonCommand.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ _Don't forget to change **tempSetMin** and **tempSetMax** after chaning temperat
| Kd | Kd parameters of PID | The derivative of the beer temperature is multiplied by Kd to give the derivative part of the PID value. |
| pidMax | PID Maximum | You can define the maximum difference between the beer temp setting and fridge temp setting here. The fridge setting will be clipped to this range. |
| iMaxErr | Integrator: maximum temp error °C | The integral is only active when the temperature is close to the target temperature. This is the maximum error for which the integral is active. |
| idleRangeH | Temperature idle range top | When the fridge temperature is within this range, it won't heat or cool, regardless of other settings. |
| idleRangeL | Temperature idle range bottom | When the fridge temperature is within this range, it won't heat or cool, regardless of other settings. |
| heatTargetH | Heating target upper bound | When the overshoot lands under this value, the peak is within target range and the estimator is not adjusted. |
| heatTargetL | Heating target lower bound | When the overshoot lands above this value, the peak is within target range and the estimator is not adjusted. |
| coolTargetH | Cooling target upper bound | When the overshoot lands under this value, the peak is within target range and the estimator is not adjusted. |
| coolTargetL | Cooling target lower bound | When the overshoot lands above this value, the peak is within target range and the estimator is not adjusted. |
| hystMode | hysteresisMode (PID control) | Independent mode only. `0` = simple thermostat (heat/cool target bands). `1` = PID on heater (beer cascade into fridge demand); cooler still uses coolTargetH/L. Classic fridge/beer/profile ignore this flag. |
| idleRangeH | Idle range high (°C diff) | Classic modes: cooler starts if fridge rises above fridgeSetting + idleRangeH. |
| idleRangeL | Idle range low (°C diff) | Classic modes: heater starts if fridge drops below fridgeSetting + idleRangeL. |
| heatTargetH | Heating target upper bound | Classic peak/stop estimator. Independent: heater stop band (thermostat or PID fridge demand). |
| heatTargetL | Heating target lower bound | Classic peak estimator. Independent: heater start band. |
| coolTargetH | Cooling target upper bound | Classic peak estimator. Independent: cooler start band. |
| coolTargetL | Cooling target lower bound | Classic peak/stop estimator. Independent: cooler stop band. |
| maxHeatTimeForEst | Maximum time in seconds for heating overshoot estimator | The time the fridge has been heating is used to estimate overshoot. This is the maximum time that is taken into account. |
| maxCoolTimeForEst | Maximum time in seconds for cooling overshoot estimator | The time the fridge has been cooling is used to estimate overshoot. This is the maximum time that is taken into account. |
| fridgeFastFilt | Fridge fast filter delay time | The fridge fast filter is used for on-off control, display and logging. It needs to have a small delay. 0 = 9 Seconds, 1 = 18 Seconds, 2 = 39 Seconds, 3 = 78 Seconds, 4 = 159 Seconds, 5 = 318 Seconds, 6 = 639 Seconds|
Expand Down
4,349 changes: 4,348 additions & 1 deletion htmljs/dist/english/BPLLogViewer.htm

Large diffs are not rendered by default.

Binary file modified htmljs/dist/english/BPLLogViewer.htm.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion htmljs/dist/english/gravity.htm

Large diffs are not rendered by default.

Binary file modified htmljs/dist/english/gravity.htm.gz
Binary file not shown.
54 changes: 54 additions & 0 deletions htmljs/fix_config_divL.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Fix <divL from bad id=idleRangeFields* strip (prefix collision with idleRangeFieldsL).
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');

const wdocDir = path.join(__dirname, '..', 'wdoc');
const langs = ['english', 'spanish', 'slovak', 'portuguese-br', 'norwegian', 'italian', 'chinese'];

function parseHeader(text) {
const m = text.match(/const unsigned char (\w+)\[\] PROGMEM = \{([\s\S]*?)\};/);
if (!m) throw new Error('array not found');
return {
name: m[1],
bytes: Buffer.from(
m[2]
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((s) => parseInt(s, 16))
),
};
}

function formatHeader(name, buf) {
const lines = [`const unsigned char ${name}[] PROGMEM = {`];
for (let i = 0; i < buf.length; i += 12) {
const chunk = [];
for (let j = i; j < Math.min(i + 12, buf.length); j++) {
chunk.push('0x' + buf[j].toString(16).padStart(2, '0'));
}
lines.push(' ' + chunk.join(', ') + (i + 12 < buf.length ? ',' : ''));
}
lines.push('};');
lines.push(`unsigned int ${name}_len = ${buf.length};`);
lines.push('');
return lines.join('\n');
}

for (const lang of langs) {
const file = path.join(wdocDir, `${lang}_config_htm.h`);
if (!fs.existsSync(file)) continue;
const text = fs.readFileSync(file, 'utf8');
const { name, bytes } = parseHeader(text);
let html = zlib.gunzipSync(bytes).toString('utf8');
if (!html.includes('<divL')) {
console.log(lang + ': ok (no divL)');
continue;
}
const n = (html.match(/<divL/g) || []).length;
html = html.replace(/<divL\b/g, '<div');
const gz = zlib.gzipSync(Buffer.from(html, 'utf8'), { level: 9 });
fs.writeFileSync(file, formatHeader(name, gz));
console.log(lang + ': fixed ' + n + ' divL');
}
107 changes: 107 additions & 0 deletions htmljs/fix_control_apply_syntax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Fix broken Independent Apply patch in wdoc/*_control_htm.h
// Bug: surgical patch inserted `var pidRadio=...` inside a ternary, causing
// SyntaxError so modekeeper.init() never ran and all mode panels stayed visible.
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');

const wdocDir = path.join(__dirname, '..', 'wdoc');
const langs = ['english', 'spanish', 'slovak', 'portuguese-br', 'norwegian', 'italian', 'chinese'];

const BROKEN =
':var pidRadio=document.querySelector("input[name=hystMode]:checked"),hystMode=pidRadio?pidRadio.value:"0";BWF.send("j{mode:i, fridgeSet:"+l+", beerSet:"+o+", hystMode:"+hystMode+"}")';

const FIXED =
':void function(){var pidRadio=document.querySelector("input[name=hystMode]:checked"),hystMode=pidRadio?pidRadio.value:"0";BWF.send("j{mode:i, fridgeSet:"+l+", beerSet:"+o+", hystMode:"+hystMode+"}")}()';

// Alternate minified var names (e/t instead of l/o)
const BROKEN2 =
/:var pidRadio=document\.querySelector\("input\[name=hystMode\]:checked"\),hystMode=pidRadio\?pidRadio\.value:"0";BWF\.send\("j\{mode:i, fridgeSet:"\+(\w+)\+", beerSet:"\+(\w+)\+", hystMode:"\+hystMode\+"\}"\)/;

function parseHeader(text) {
const m = text.match(/const unsigned char (\w+)\[\] PROGMEM = \{([\s\S]*?)\};/);
if (!m) throw new Error('array not found');
return {
name: m[1],
bytes: Buffer.from(
m[2]
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((s) => parseInt(s, 16))
),
};
}

function formatHeader(name, buf) {
const lines = [`const unsigned char ${name}[] PROGMEM = {`];
for (let i = 0; i < buf.length; i += 12) {
const chunk = [];
for (let j = i; j < Math.min(i + 12, buf.length); j++) {
chunk.push('0x' + buf[j].toString(16).padStart(2, '0'));
}
lines.push(' ' + chunk.join(', ') + (i + 12 < buf.length ? ',' : ''));
}
lines.push('};');
lines.push(`unsigned int ${name}_len = ${buf.length};`);
lines.push('');
return lines.join('\n');
}

function fixHtml(html) {
if (html.includes(BROKEN)) {
return { html: html.replace(BROKEN, FIXED), how: 'l/o' };
}
const m = html.match(BROKEN2);
if (m) {
const fixed =
':void function(){var pidRadio=document.querySelector("input[name=hystMode]:checked"),hystMode=pidRadio?pidRadio.value:"0";BWF.send("j{mode:i, fridgeSet:"+' +
m[1] +
'+", beerSet:"+' +
m[2] +
'+", hystMode:"+hystMode+"}")}()';
return { html: html.replace(m[0], fixed), how: m[1] + '/' + m[2] };
}
if (html.includes(':void function(){var pidRadio=document.querySelector("input[name=hystMode]:checked")')) {
return { html, how: 'already-fixed' };
}
// Detect any remaining illegal `:var pidRadio`
if (html.includes(':var pidRadio=')) {
throw new Error('unrecognized :var pidRadio pattern');
}
return { html, how: 'no-broken-pattern' };
}

for (const lang of langs) {
const file = path.join(wdocDir, `${lang}_control_htm.h`);
if (!fs.existsSync(file)) {
console.log(lang + ': missing');
continue;
}
const text = fs.readFileSync(file, 'utf8');
const { name, bytes } = parseHeader(text);
const html = zlib.gunzipSync(bytes).toString('utf8');
const { html: out, how } = fixHtml(html);
if (how === 'already-fixed' || how === 'no-broken-pattern') {
console.log(lang + ': ' + how);
continue;
}
const gz = zlib.gzipSync(Buffer.from(out, 'utf8'), { level: 9 });
fs.writeFileSync(file, formatHeader(name, gz));
console.log(lang + ': fixed (' + how + ') ' + bytes.length + ' -> ' + gz.length);
}

// Also harden the source patch script so re-running won't break again
const patchFile = path.join(__dirname, 'patch_indep_pid_control.js');
if (fs.existsSync(patchFile)) {
let p = fs.readFileSync(patchFile, 'utf8');
const bad =
"'var pidRadio=document.querySelector(\"input[name=hystMode]:checked\"),hystMode=pidRadio?pidRadio.value:\"0\";BWF.send(\"j{mode:i, fridgeSet:\"+$1+\", beerSet:\"+$2+\", hystMode:\"+hystMode+\"}\")'";
const good =
"'void function(){var pidRadio=document.querySelector(\"input[name=hystMode]:checked\"),hystMode=pidRadio?pidRadio.value:\"0\";BWF.send(\"j{mode:i, fridgeSet:\"+$1+\", beerSet:\"+$2+\", hystMode:\"+hystMode+\"}\")}()'";
if (p.includes(bad)) {
p = p.replace(bad, good);
fs.writeFileSync(patchFile, p);
console.log('hardened patch_indep_pid_control.js');
}
}
104 changes: 104 additions & 0 deletions htmljs/fix_control_init_brace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Fix control page: all mode panels visible because modekeeper JS fails to parse.
// Two missing '}' from enable-switches surgical patch:
// 1) after wireIndep IIFE: need }}/,apply (close if + close init)
// 2) before saveprofile: need }}} (close apply's {{ + close modekeeper)
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');

const wdocDir = path.join(__dirname, '..', 'wdoc');
const langs = ['english', 'spanish', 'slovak', 'portuguese-br', 'norwegian', 'italian', 'chinese'];

const INIT_BROKEN = 'wireIndepActuator("beer-set","independent-heat-t")})()},apply:function(){';
const INIT_FIXED = 'wireIndepActuator("beer-set","independent-heat-t")})()}},apply:function(){';
const END_BROKEN = '})}};function saveprofile';
const END_FIXED = '})}}};function saveprofile';

function parseHeader(text) {
const m = text.match(/const unsigned char (\w+)\[\] PROGMEM = \{([\s\S]*?)\};/);
if (!m) throw new Error('array not found');
return {
name: m[1],
bytes: Buffer.from(
m[2]
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((s) => parseInt(s, 16))
),
};
}

function formatHeader(name, buf) {
const lines = [`const unsigned char ${name}[] PROGMEM = {`];
for (let i = 0; i < buf.length; i += 12) {
const chunk = [];
for (let j = i; j < Math.min(i + 12, buf.length); j++) {
chunk.push('0x' + buf[j].toString(16).padStart(2, '0'));
}
lines.push(' ' + chunk.join(', ') + (i + 12 < buf.length ? ',' : ''));
}
lines.push('};');
lines.push(`unsigned int ${name}_len = ${buf.length};`);
lines.push('');
return lines.join('\n');
}

for (const lang of langs) {
const file = path.join(wdocDir, `${lang}_control_htm.h`);
if (!fs.existsSync(file)) {
console.log(lang + ': missing');
continue;
}
const text = fs.readFileSync(file, 'utf8');
const { name, bytes } = parseHeader(text);
let html = zlib.gunzipSync(bytes).toString('utf8');
let n = 0;

if (html.includes(INIT_FIXED)) {
console.log(lang + ': init already');
} else if (html.includes(INIT_BROKEN)) {
html = html.replace(INIT_BROKEN, INIT_FIXED);
n++;
console.log(lang + ': fixed init');
} else {
console.log(lang + ': init pattern missing');
}

if (html.includes(END_FIXED)) {
console.log(lang + ': end already');
} else if (html.includes(END_BROKEN)) {
html = html.replace(END_BROKEN, END_FIXED);
n++;
console.log(lang + ': fixed end');
} else {
// localized? try regex
const re = /\)\}\};function saveprofile/;
if (re.test(html)) {
html = html.replace(re, ')}}};function saveprofile');
n++;
console.log(lang + ': fixed end (re)');
} else {
console.log(lang + ': end pattern missing');
}
}

const scripts = [];
html.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, (_, s) => {
if (s && s.length > 200) scripts.push(s);
});
let ok = true;
for (let i = 0; i < scripts.length; i++) {
try {
new Function(scripts[i]);
} catch (e) {
console.log(lang + ': PARSE FAIL', i, e.message);
ok = false;
}
}
if (!ok) continue;

const gz = zlib.gzipSync(Buffer.from(html, 'utf8'), { level: 9 });
fs.writeFileSync(file, formatHeader(name, gz));
console.log(lang + ': wrote OK', n, bytes.length, '->', gz.length);
}
Loading