From db55d1db85f0f1cd9f5939fd87bf31f5131fd7f2 Mon Sep 17 00:00:00 2001 From: BastiMila Date: Fri, 30 Oct 2026 09:55:26 +0100 Subject: [PATCH 01/10] initial --- firmware/.gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 firmware/.gitignore diff --git a/firmware/.gitignore b/firmware/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/firmware/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch From 8cf9e3d78a77687145dc4c5c357eab58925467eb Mon Sep 17 00:00:00 2001 From: BastiMila Date: Fri, 30 Oct 2026 10:25:12 +0100 Subject: [PATCH 02/10] Add Klipper backend support with AFC integration and update printer types --- firmware/src/backend_klipper.cpp | 247 +++++++++++++++++++++++++++++++ firmware/src/backend_klipper.h | 53 +++++++ firmware/src/printer.h | 3 +- firmware/src/printer_budget.h | 3 + 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 firmware/src/backend_klipper.cpp create mode 100644 firmware/src/backend_klipper.h diff --git a/firmware/src/backend_klipper.cpp b/firmware/src/backend_klipper.cpp new file mode 100644 index 0000000..115655c --- /dev/null +++ b/firmware/src/backend_klipper.cpp @@ -0,0 +1,247 @@ +#include "backend_klipper.h" +#include +#include + +namespace { + const char* SLOTS[4] = { "T0", "T1", "T2", "T3" }; + + // AFC reports colours as "#RRGGBB". + bool hexRGB(const char* s, uint8_t& r, uint8_t& g, uint8_t& b) { + if (!s) return false; + if (*s == '#') s++; + if (strlen(s) < 6) return false; + auto hx = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + c |= 0x20; return (c >= 'a' && c <= 'f') ? c - 'a' + 10 : 0; + }; + r = hx(s[0]) * 16 + hx(s[1]); + g = hx(s[2]) * 16 + hx(s[3]); + b = hx(s[4]) * 16 + hx(s[5]); + return true; + } + + // G-code arguments are separated by spaces and a quote would break the + // value stored in save_variables, so strip both out of vendor and type. + String clean(const String& in) { + String o; o.reserve(in.length()); + for (size_t i = 0; i < in.length(); i++) { + char c = in[i]; + if (c == ' ') o += '_'; + else if (c == '\'' || c == '"' || c == '#' || c == ';') continue; + else o += c; + } + return o; + } + + String unclean(const char* s) { + String o(s ? s : ""); + o.replace('_', ' '); + return o; + } +} + +// Rebuild what the UI shows for one slot from the pieces we keep. +void KlipperBackend::recompute(int i) { + SlotState& s = slots_[i]; + s.type = sub_[i].length() ? (mat_[i] + " " + sub_[i]) : mat_[i]; + s.known = mat_[i].length() > 0; + float pct = (initW_[i] > 0) ? (weight_[i] * 100.0f / initW_[i]) : 0.0f; + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + s.percent = (uint8_t)(pct + 0.5f); + s.selected = (i == currentLane_); +} + +void KlipperBackend::applyLane(int i, JsonObjectConst lane) { + SlotState& s = slots_[i]; + // Pushes carry only what changed, so every field is read on its own. + if (lane["material"].is()) mat_[i] = (const char*)lane["material"]; + if (lane["color"].is()) { + uint8_t r = s.r, g = s.g, b = s.b; + if (hexRGB((const char*)lane["color"], r, g, b)) { s.r = r; s.g = g; s.b = b; } + } + if (lane["weight"].is()) weight_[i] = lane["weight"].as(); + if (lane["initial_weight"].is()) initW_[i] = lane["initial_weight"].as(); + if (s.brand.length() == 0 && lane["spool_vendor"].is()) + s.brand = (const char*)lane["spool_vendor"]; + + if (pendIdx_ == i) { + if (s.r == pendR_ && s.g == pendG_ && s.b == pendB_) { + status_ = String("Klipper: ") + SLOTS[i] + " confirmed"; + pendIdx_ = -1; + } + } +} + +void KlipperBackend::applyStatus(JsonObjectConst st) { + JsonObjectConst afc = st["AFC"]; + if (!afc.isNull() && afc.containsKey("current_load")) { + currentLane_ = -1; + if (afc["current_load"].is()) { + const char* cl = (const char*)afc["current_load"]; + size_t n = strlen(cl); + if (n > 0 && cl[n - 1] >= '0' && cl[n - 1] <= '3') currentLane_ = cl[n - 1] - '0'; + } + } + + const char* ps = st["print_stats"]["state"] | ""; + if (*ps) printing_ = !strcmp(ps, "printing") || !strcmp(ps, "paused"); + + JsonObjectConst vars = st["save_variables"]["variables"]; + for (int i = 0; i < 4; i++) { + if (!vars.isNull()) { + String kb = String("tt_t") + i + "_brand"; + String ks = String("tt_t") + i + "_sub"; + if (vars[kb].is()) slots_[i].brand = unclean((const char*)vars[kb]); + if (vars[ks].is()) sub_[i] = unclean((const char*)vars[ks]); + } + String key = String("AFC_lane extruder") + i; + JsonObjectConst lane = st[key]; + if (!lane.isNull()) applyLane(i, lane); + recompute(i); + } + if (pendIdx_ < 0) status_ = "Klipper: slots updated"; +} + +void KlipperBackend::onMsg(uint8_t* payload, size_t len) { + JsonDocument d; + if (deserializeJson(d, payload, len)) return; + + // A refused command comes back as an error object. Saying so is the only + // way to tell "sent" from "done". + if (!d["error"].isNull()) { + const char* m = d["error"]["message"] | "error"; + status_ = String("Klipper: ") + m; + Serial.printf("[klipper] error: %s\n", m); + pendIdx_ = -1; + return; + } + + JsonObjectConst st = d["result"]["status"]; + if (st.isNull() && String(d["method"] | "") == "notify_status_update") + st = d["params"][0]; + if (st.isNull()) return; + applyStatus(st); +} + +void KlipperBackend::onEvent(WStype_t type, uint8_t* payload, size_t len) { + switch (type) { + case WStype_CONNECTED: + connected_ = true; status_ = "Klipper: connected"; + Serial.println("[klipper] websocket connected"); + break; + case WStype_DISCONNECTED: + if (connected_) Serial.println("[klipper] websocket closed"); + connected_ = false; status_ = "Klipper: disconnected"; + break; + case WStype_TEXT: onMsg(payload, len); break; + case WStype_ERROR: + Serial.printf("[klipper] websocket error: %.*s\n", (int)len, + payload ? (const char*)payload : ""); + break; + default: break; + } +} + +bool KlipperBackend::sendRaw(String s) { + if (!connected_) return false; + Serial.printf("[klipper] -> %s\n", s.c_str()); + return ws_.sendTXT(s); +} + +void KlipperBackend::begin(const PrinterCfg& cfg) { + for (int i = 0; i < 4; i++) { + slots_[i] = SlotState{}; + mat_[i] = ""; sub_[i] = ""; + weight_[i] = 0; initW_[i] = 0; + } + connected_ = false; + currentLane_ = -1; + printing_ = false; + pendIdx_ = -1; + status_ = "Klipper: connecting..."; + // Same as the Snapmaker backend: no Origin header, no subprotocol. + // Moonraker answers 403 to the library's default "Origin: file://" and the + // client then fires no event at all. + ws_.setExtraHeaders(""); + ws_.begin(cfg.host, 7125, "/websocket", ""); + ws_.onEvent([this](WStype_t t, uint8_t* p, size_t l) { onEvent(t, p, l); }); + ws_.setReconnectInterval(10000); + ws_.enableHeartbeat(15000, 3000, 2); +} + +void KlipperBackend::loop() { + ws_.loop(); + if (connected_ && millis() - lastReq_ > 5000) { lastReq_ = millis(); refresh(); } + if (pendIdx_ >= 0 && (int32_t)(millis() - pendUntil_) > 0) { + status_ = String("Klipper: ") + SLOTS[pendIdx_] + " sent, not confirmed"; + pendIdx_ = -1; + } +} + +void KlipperBackend::stop() { + ws_.disconnect(); + connected_ = false; + status_ = "Klipper: stopped"; +} + +bool KlipperBackend::connected() { return connected_; } +String KlipperBackend::status() { return status_; } +const char* KlipperBackend::slotLabel(int i) { return SLOTS[(i >= 0 && i < 4) ? i : 0]; } +const SlotState& KlipperBackend::slot(int i) { return slots_[(i >= 0 && i < 4) ? i : 0]; } + +void KlipperBackend::refresh() { + JsonDocument d; + d["jsonrpc"] = "2.0"; + d["method"] = "printer.objects.query"; + JsonObject o = d["params"]["objects"].to(); + for (int i = 0; i < 4; i++) { + String key = String("AFC_lane extruder") + i; // String: the key is copied + o[key] = nullptr; + } + o["AFC"] = nullptr; + o["save_variables"] = nullptr; + o["print_stats"] = nullptr; + d["id"] = 1001; + String out; serializeJson(d, out); + sendRaw(out); +} + +bool KlipperBackend::assign(int idx, const TagInfo& t) { + if (idx < 0 || idx >= 4 || !connected_) return false; + // AFC keeps lane data per tool. Rewriting a lane while the printer is + // working is not something this code has been checked against. + if (printing_) { status_ = "Klipper: printer is printing"; return false; } + + // A tag carries one string, "PLA High Speed". AFC gets the family only, + // the variant goes to save_variables as the subtype. + String full = t.material.length() ? t.material : String("PLA"); + int sp = full.indexOf(' '); + String mat = clean(sp > 0 ? full.substring(0, sp) : full); + String sub = clean(sp > 0 ? full.substring(sp + 1) : String()); + String vend = clean(t.brand); + + char col[8]; + snprintf(col, sizeof(col), "%02X%02X%02X", t.r, t.g, t.b); + + String script = String("TT_SET_LANE TOOL=") + idx + " MATERIAL=" + mat + " COLOR=" + col; + if (sub.length()) script += " SUBTYPE=" + sub; + if (vend.length()) script += " BRAND=" + vend; + + JsonDocument d; + d["jsonrpc"] = "2.0"; + d["method"] = "printer.gcode.script"; + d["params"]["script"] = script; + d["id"] = 201; + String out; serializeJson(d, out); + + bool ok = sendRaw(out); + if (!ok) { status_ = "Klipper: send failed"; return false; } + + pendIdx_ = idx; pendR_ = t.r; pendG_ = t.g; pendB_ = t.b; + pendUntil_ = millis() + 12000; + status_ = String("Klipper: sent -> ") + SLOTS[idx]; + delay(200); + refresh(); // read it back; applyLane() confirms or the timeout reports it + return true; +} diff --git a/firmware/src/backend_klipper.h b/firmware/src/backend_klipper.h new file mode 100644 index 0000000..f34969c --- /dev/null +++ b/firmware/src/backend_klipper.h @@ -0,0 +1,53 @@ +#pragma once +#include "printer.h" +#include +#include + +// Klipper with AFC (e.g. OpenCreator on a FlashForge Creator 5 Pro) - Moonraker +// over WebSocket, same transport as the Snapmaker backend: +// ws://:7125/websocket , JSON-RPC 2.0 , no authentication. +// 4 slots = AFC lanes extruder0..extruder3 = tools T0..T3. +// Assigning filament: printer.gcode.script -> "TT_SET_LANE TOOL=n MATERIAL=.. +// SUBTYPE=.. BRAND=.. COLOR=RRGGBB" (macro in tigertag.cfg on the printer, +// it calls AFC's SET_MATERIAL / SET_COLOR and stores brand + subtype in +// save_variables). +// Slot state: objects "AFC_lane extruderN" (material, color "#RRGGBB", weight, +// initial_weight), "AFC" (current_load), "save_variables" (tt_tN_brand, +// tt_tN_sub) and "print_stats" (state). +class KlipperBackend : public PrinterBackend { +public: + void begin(const PrinterCfg& cfg) override; + void loop() override; + void stop() override; + bool connected() override; + bool firstIsExternal() override { return false; } // four tools, no external spool + int slotCount() override { return 4; } + const char* slotLabel(int i) override; + const SlotState& slot(int i) override; + bool assign(int i, const TagInfo& t) override; + String status() override; + void refresh() override; +private: + void applyStatus(JsonObjectConst st); + void applyLane(int i, JsonObjectConst lane); + void recompute(int i); + void onMsg(uint8_t* payload, size_t len); + void onEvent(WStype_t type, uint8_t* payload, size_t len); + bool sendRaw(String s); + + WebSocketsClient ws_; + bool connected_ = false; + String status_ = "Klipper: connecting..."; + SlotState slots_[4]; + String mat_[4], sub_[4]; // AFC material, subtype from save_variables + float weight_[4] = {0, 0, 0, 0}; + float initW_[4] = {0, 0, 0, 0}; + int currentLane_ = -1; + bool printing_ = false; + uint32_t lastReq_ = 0; + + // Read-back check after assign(): did the colour actually land? + int pendIdx_ = -1; + uint8_t pendR_ = 0, pendG_ = 0, pendB_ = 0; + uint32_t pendUntil_ = 0; +}; diff --git a/firmware/src/printer.h b/firmware/src/printer.h index 39cbdce..eb63c30 100644 --- a/firmware/src/printer.h +++ b/firmware/src/printer.h @@ -24,7 +24,8 @@ enum PrinterType : uint8_t { PT_BAMBU = 3, // MQTT/TLS :8883 PT_SNAPMAKER = 4, // Moonraker WebSocket :7125 PT_ELEGOO = 5, // MQTT :1883, no TLS - PT_ANYCUBIC = 6 // MQTT/TLS :9883, self-signed + PT_ANYCUBIC = 6, // MQTT/TLS :9883, self-signed + PT_KLIPPER = 7 // Moonraker WebSocket :7125, AFC lanes (OpenCreator) }; // One printer, as imported from the user's TigerTag account. diff --git a/firmware/src/printer_budget.h b/firmware/src/printer_budget.h index eeea7cf..51aca08 100644 --- a/firmware/src/printer_budget.h +++ b/firmware/src/printer_budget.h @@ -69,6 +69,9 @@ inline uint16_t loadSlotsFor(const PrinterCfg& p, bool sharesCloud) { // Replace them with readings. case PT_ANYCUBIC: return 6; case PT_SNAPMAKER: return 5; + // Klipper/AFC uses the same Moonraker WebSocket as Snapmaker. Charged + // like it; NOT measured on this device yet. + case PT_KLIPPER: return 5; case PT_CREALITY: return 6; case PT_ELEGOO: return 3; case PT_FF_C5: return 2; From ddc064fa03013a204244dce52053cf956eb06bc2 Mon Sep 17 00:00:00 2001 From: BastiMila Date: Fri, 30 Oct 2026 10:37:16 +0100 Subject: [PATCH 03/10] Add Klipper support: update main.cpp and implement nozzle temperature and weight handling in backend_klipper --- firmware/patch_main.py | 50 ++++++++++++++++++++++++++++++++ firmware/src/backend_klipper.cpp | 13 +++++++++ firmware/src/main.cpp | 7 ++++- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 firmware/patch_main.py diff --git a/firmware/patch_main.py b/firmware/patch_main.py new file mode 100644 index 0000000..4e41700 --- /dev/null +++ b/firmware/patch_main.py @@ -0,0 +1,50 @@ +# patch_main.py - traegt PT_KLIPPER in src/main.cpp ein. +# Aufruf im Ordner firmware: python patch_main.py +# Es aendert nur src/main.cpp. Jede Stelle muss genau einmal gefunden werden, +# sonst bricht das Skript ab und schreibt NICHTS. +import sys + +PATH = "src/main.cpp" +raw = open(PATH, "rb").read().decode("utf-8") +nl = "\r\n" if "\r\n" in raw else "\n" +lines = raw.split(nl) + +def find_one(pred, what): + hits = [i for i, l in enumerate(lines) if pred(l)] + if len(hits) != 1: + sys.exit("ABBRUCH: '%s' wurde %d mal gefunden (erwartet: 1). Nichts geaendert." % (what, len(hits))) + return hits[0] + +def indent_of(l): + return l[:len(l) - len(l.lstrip())] + +edits = [] # (index, kind, payload) + +# 1. Include +i = find_one(lambda l: l.strip() == '#include "backend_snapmaker.h"', 'include backend_snapmaker.h') +edits.append((i, "after", '#include "backend_klipper.h"')) + +# 2. typeTag +i = find_one(lambda l: 't == PT_SNAPMAKER ? "Snap" : "--";' in l, 'typeTag') +edits.append((i, "replace", lines[i].replace('t == PT_SNAPMAKER ? "Snap" : "--";', + 't == PT_SNAPMAKER ? "Snap" : t == PT_KLIPPER ? "Klip" : "--";'))) + +# 3..6. switch-Zeilen +def after_case(anchor, newline_text, what): + i = find_one(lambda l: l.strip() == anchor, what) + edits.append((i, "after", indent_of(lines[i]) + newline_text)) + +after_case("case PT_SNAPMAKER: return 7125;", "case PT_KLIPPER: return 7125;", "ctrlPort") +after_case("case PT_SNAPMAKER: return new SnapmakerBackend();", "case PT_KLIPPER: return new KlipperBackend();", "newBackend") +after_case("case PT_SNAPMAKER: return 8000;", "case PT_KLIPPER: return 8000;", "linkCost") +after_case('case PT_SNAPMAKER: return "snapmaker";', 'case PT_KLIPPER: return "klipper";', "brandName") + +# Anwenden von hinten nach vorn, damit die Indizes stimmen +for i, kind, payload in sorted(edits, key=lambda e: -e[0]): + if kind == "after": + lines.insert(i + 1, payload) + else: + lines[i] = payload + +open(PATH, "wb").write(nl.join(lines).encode("utf-8")) +print("OK: %d Stellen in %s angepasst (Zeilenende: %s)" % (len(edits), PATH, "CRLF" if nl == "\r\n" else "LF")) diff --git a/firmware/src/backend_klipper.cpp b/firmware/src/backend_klipper.cpp index 115655c..bca38e1 100644 --- a/firmware/src/backend_klipper.cpp +++ b/firmware/src/backend_klipper.cpp @@ -228,6 +228,19 @@ bool KlipperBackend::assign(int idx, const TagInfo& t) { if (sub.length()) script += " SUBTYPE=" + sub; if (vend.length()) script += " BRAND=" + vend; + // Nozzle temperature: the middle of the tag's window. A choice, not a rule - + // the macro stores it and TT_LOAD uses it for loading. + if (t.nozMin > 0 && t.nozMax >= t.nozMin) + script += String(" TEMP=") + (int)((t.nozMin + t.nozMax) / 2); + + // Remaining weight, only when the tag's unit looks like grams. The tag + // keeps it up to date when a Tiger Scale is used. AFC's initial weight is + // not set here, so the percentage the UI shows is relative to AFC's own + // initial_weight (1000 g on this printer). + if (t.available > 0 && t.available <= 10000 && t.unitLabel.length() && + (t.unitLabel[0] == 'g' || t.unitLabel[0] == 'G')) + script += String(" WEIGHT=") + (int)t.available; + JsonDocument d; d["jsonrpc"] = "2.0"; d["method"] = "printer.gcode.script"; diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index b62a26d..9e06b61 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -31,6 +31,7 @@ #include "backend_ff.h" #include "backend_bambu.h" #include "backend_snapmaker.h" +#include "backend_klipper.h" #include "backend_elegoo.h" #include "backend_anycubic.h" #include "webcfg.h" @@ -146,7 +147,7 @@ static int printerCount() { } static const char* typeTag(PrinterType t) { return t == PT_CREALITY ? "K2" : t == PT_FF_C5 ? "FF C5" : t == PT_BAMBU ? "Bambu" - : t == PT_SNAPMAKER ? "Snap" : "--"; + : t == PT_SNAPMAKER ? "Snap" : t == PT_KLIPPER ? "Klip" : "--"; } // ---- "is it online?" probe (TCP connect to the control port) ------------ @@ -176,6 +177,7 @@ static uint16_t ctrlPort(PrinterType t) { case PT_FF_C5: return 8898; case PT_BAMBU: return 8883; case PT_SNAPMAKER: return 7125; + case PT_KLIPPER: return 7125; case PT_ELEGOO: return 1883; case PT_ANYCUBIC: return 9883; default: return 9999; // K2 @@ -780,6 +782,7 @@ static PrinterBackend* newBackend(PrinterType t) { case PT_FF_C5: return new FlashForgeC5Backend(); case PT_BAMBU: return new BambuBackend(); case PT_SNAPMAKER: return new SnapmakerBackend(); + case PT_KLIPPER: return new KlipperBackend(); case PT_ELEGOO: return new ElegooBackend(); case PT_ANYCUBIC: return new AnycubicBackend(); default: return new CrealityBackend(); @@ -861,6 +864,7 @@ static uint32_t linkCost(PrinterType t, bool foreground) { case PT_BAMBU: return 56000; case PT_ANYCUBIC: return 48000; case PT_SNAPMAKER: return 8000; + case PT_KLIPPER: return 8000; case PT_CREALITY: return 6000; case PT_ELEGOO: return 4000; case PT_FF_C5: return 3000; @@ -1221,6 +1225,7 @@ static const char* brandName(PrinterType t) { case PT_BAMBU: return "bambu"; case PT_ANYCUBIC: return "anycubic"; case PT_SNAPMAKER: return "snapmaker"; + case PT_KLIPPER: return "klipper"; case PT_CREALITY: return "creality"; case PT_ELEGOO: return "elegoo"; case PT_FF_C5: return "flashforge"; From d73cd061649be3248eab4659b770c0941d2d8e44 Mon Sep 17 00:00:00 2001 From: BastiMila Date: Fri, 30 Oct 2026 10:43:27 +0100 Subject: [PATCH 04/10] Add Klipper support: update brand handling in tigertag_cloud and webcfg --- firmware/patch_cloud.py | 51 +++++++++++++++++++++++++++++++++ firmware/src/tigertag_cloud.cpp | 5 ++-- firmware/src/webcfg.cpp | 2 +- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 firmware/patch_cloud.py diff --git a/firmware/patch_cloud.py b/firmware/patch_cloud.py new file mode 100644 index 0000000..8a4aa0e --- /dev/null +++ b/firmware/patch_cloud.py @@ -0,0 +1,51 @@ +# patch_cloud.py - traegt PT_KLIPPER in src/tigertag_cloud.cpp und src/webcfg.cpp ein. +# Aufruf im Ordner firmware: python patch_cloud.py +# Jede Stelle muss genau einmal gefunden werden, sonst bricht das Skript ab und +# schreibt NICHTS (auch nicht in die andere Datei). +import sys + +def load(path): + raw = open(path, "rb").read().decode("utf-8") + return raw, ("\r\n" if "\r\n" in raw else "\n") + +def replace_once(text, old, new, what): + n = text.count(old) + if n != 1: + sys.exit("ABBRUCH: '%s' wurde %d mal gefunden (erwartet: 1). Nichts geaendert." % (what, n)) + return text.replace(old, new) + +def insert_after_line(text, nl, needle, new_line, what): + lines = text.split(nl) + hits = [i for i, l in enumerate(lines) if needle in l] + if len(hits) != 1: + sys.exit("ABBRUCH: '%s' wurde %d mal gefunden (erwartet: 1). Nichts geaendert." % (what, len(hits))) + i = hits[0] + ind = lines[i][:len(lines[i]) - len(lines[i].lstrip())] + lines.insert(i + 1, ind + new_line) + return nl.join(lines) + +# ---- tigertag_cloud.cpp +cp = "src/tigertag_cloud.cpp" +c, nl = load(cp) +c = replace_once(c, '"snapmaker", "elegoo", "anycubic" };', + '"snapmaker", "elegoo", "anycubic", "klipper" };', "BRANDS-Liste") +c = replace_once(c, 'PT_SNAPMAKER, PT_ELEGOO, PT_ANYCUBIC };', + 'PT_SNAPMAKER, PT_ELEGOO, PT_ANYCUBIC, PT_KLIPPER };', "BRAND_TYPE-Liste") +c = insert_after_line(c, nl, 'if (brand == "snapmaker")', + 'if (brand == "klipper") return PT_KLIPPER; // Moonraker ws :7125, AFC', + "mapType snapmaker") + +# ---- webcfg.cpp +wp = "src/webcfg.cpp" +w, wnl = load(wp) +w = replace_once(w, "'Elegoo','Anycubic'],", "'Elegoo','Anycubic','Klipper'],", "Markenliste im Web-Skript") + +# Erst nach erfolgreichen Pruefungen schreiben +open(cp, "wb").write(c.encode("utf-8")) +open(wp, "wb").write(w.encode("utf-8")) +print("OK: tigertag_cloud.cpp und webcfg.cpp angepasst.") +print() +print("Zur Kontrolle, Stellen die von der Markenzahl abhaengen koennten:") +for i, l in enumerate(c.split(nl), 1): + if "okBrands" in l or "BRAND_TYPE" in l or "BRANDS[" in l or "sizeof(BRANDS" in l: + print(" tigertag_cloud.cpp:%d: %s" % (i, l.strip())) diff --git a/firmware/src/tigertag_cloud.cpp b/firmware/src/tigertag_cloud.cpp index d486e75..04c3490 100644 --- a/firmware/src/tigertag_cloud.cpp +++ b/firmware/src/tigertag_cloud.cpp @@ -257,6 +257,7 @@ namespace { if (brand == "flashforge") return (m == 5 || m == 6) ? PT_FF_C5 : PT_NONE; if (brand == "bambulab") return PT_BAMBU; // protocolo LAN comum if (brand == "snapmaker") return PT_SNAPMAKER; // Moonraker ws :7125 + if (brand == "klipper") return PT_KLIPPER; // Moonraker ws :7125, AFC // Elegoo speaks one protocol across the range - the Centauri Carbon // family and everything after it - so there is no model to check. if (brand == "elegoo") return PT_ELEGOO; // MQTT :1883 @@ -503,12 +504,12 @@ bool ttcloud::syncNow(String& summary) { // fetched anyway so the log can say why they do not appear rather than // leaving the user to guess. See docs/PRINTER-COMPATIBILITY.md. const char* BRANDS[] = { "creality", "flashforge", "bambulab", - "snapmaker", "elegoo", "anycubic" }; + "snapmaker", "elegoo", "anycubic", "klipper" }; // Each brand maps to exactly one backend type, which is what makes it // possible to say "these stored printers came from that brand" when its // request fails. Kept beside the list above so the two cannot drift. const PrinterType BRAND_TYPE[] = { PT_CREALITY, PT_FF_C5, PT_BAMBU, - PT_SNAPMAKER, PT_ELEGOO, PT_ANYCUBIC }; + PT_SNAPMAKER, PT_ELEGOO, PT_ANYCUBIC, PT_KLIPPER }; String base = String("https://firestore.googleapis.com/v1/projects/") + PROJECT + "/databases/(default)/documents"; // The fields this import reads, in ONE list. diff --git a/firmware/src/webcfg.cpp b/firmware/src/webcfg.cpp index 351325a..d647dbf 100644 --- a/firmware/src/webcfg.cpp +++ b/firmware/src/webcfg.cpp @@ -1156,7 +1156,7 @@ namespace { // seconds; a switch that waited for it felt dead. main.cpp decides in // the same loop pass that takes the request, so the read-back right // after it is the verdict: a refusal (the load budget) puts it back. - h += F("