Skip to content
Merged
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
22 changes: 22 additions & 0 deletions src/ApplicationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <WiFi.h>
#include <HTTPClient.h>
#include <Preferences.h>
#include "U1Manager.h"
extern LEDManager ledManager;
#else
#include "platform/NativePlatform.h"
Expand Down Expand Up @@ -525,6 +526,14 @@ void ApplicationManager::handleSpoolDetected(const AppMessage& msg) {
lastHAStateJson_[sizeof(lastHAStateJson_) - 1] = '\0';
}

#ifndef NATIVE_TEST
// Snapmaker U1 direct-mode: publish on-tag data to the U1 immediately. Smart tags
// already carry vendor/material/color/temps so this gives Fluidd a near-instant
// update without waiting on Spoolman. U1Manager handles per-material defaults
// for missing fields and registers a pending augment if Spoolman might fill gaps.
U1Manager::getInstance().publishFromDetection(msg.payload.spoolDetected);
#endif

#ifndef NATIVE_TEST
// Spoolman sync: auto-update remaining weight (SELF_DIRECTED mode only)
// Suppress flag can gate this per-tag (e.g., after batch write to Spoolman)
Expand Down Expand Up @@ -847,6 +856,18 @@ void ApplicationManager::handleSpoolmanSynced(const AppMessage& msg) {
strncpy(materialName, msg.payload.spoolmanSynced.material_name, sizeof(materialName) - 1);
float kgRemaining = msg.payload.spoolmanSynced.kg_remaining;

#ifndef NATIVE_TEST
// Snapmaker U1 direct-mode: hand the sync result to U1Manager. It handles
// the generic-UID single-POST path AND the smart-tag augment path (POST 2)
// when a pending augment was registered by handleSpoolDetected. No-op when
// U1 integration is disabled.
{
CurrentSpoolState u1State;
NFCManager::getInstance().getCurrentSpoolState(u1State);
U1Manager::getInstance().publishFromSpoolmanSync(msg.payload.spoolmanSynced, u1State);
}
#endif

#ifndef NATIVE_TEST
// Generic tag writeback: populate NFC tag with Spoolman data if lookup succeeded
if (msg.payload.spoolmanSynced.is_uid_lookup && msg.payload.spoolmanSynced.success) {
Expand Down Expand Up @@ -1440,3 +1461,4 @@ bool ApplicationManager::sendAssignSpool(const char* toolNumber) {
return true; // Native test: pretend success
#endif
}

48 changes: 47 additions & 1 deletion src/ConfigHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,30 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
</div>
</section>

<section>
<h2 class="section-title">Snapmaker U1 Integration</h2>
<div class="hint" style="margin-bottom:12px">Push scan results directly to a Snapmaker U1 toolchanger. Requires <a href="https://github.com/paxx12/SnapmakerU1-Extended-Firmware" target="_blank">paxx12 Extended Firmware</a> with <strong>Filament Detection: External</strong> set in <code>http://&lt;printer-ip&gt;/firmware-config/</code>. Set the Moonraker URL above to your U1's IP.</div>
<div class="toggle-row" style="margin-bottom:14px">
<span id="u1_enabled_label" class="toggle-label">Enable U1 Integration</span>
<label class="toggle-switch">
<input type="checkbox" id="u1_enabled" aria-labelledby="u1_enabled_label" />
<span class="toggle-track"></span>
</label>
</div>
<div id="u1_fields" style="display:none">
<div class="field">
<label for="u1_channel">Toolhead Channel</label>
<select id="u1_channel" style="padding:6px 10px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--text);font-size:0.95em">
<option value="0">Channel 0 (T0)</option>
<option value="1">Channel 1 (T1)</option>
<option value="2">Channel 2 (T2)</option>
<option value="3">Channel 3 (T3)</option>
</select>
<div style="font-size:11px;color:#71717A;margin-top:4px">Each scanner posts to one channel. Use four scanners (one per toolhead) for a full U1 setup, or one scanner if you only load filament into one slot.</div>
</div>
</div>
</section>

<section>
<h2 class="section-title">PrusaLink</h2>
<div class="hint" style="margin-bottom:12px">Connect to a Prusa printer for automatic filament tracking. Get the API key from your printer's web interface.</div>
Expand Down Expand Up @@ -296,6 +320,10 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
document.getElementById('bambu_dashboard').checked = !!cfg.bambu_dashboard;
document.getElementById('wifi_keep_awake').checked = !!cfg.wifi_keep_awake;
maybeSetValue('moonraker_url', cfg.moonraker_url);
// Snapmaker U1 integration
document.getElementById('u1_enabled').checked = !!cfg.u1_enabled;
if (cfg.u1_channel !== undefined) document.getElementById('u1_channel').value = cfg.u1_channel;
document.getElementById('u1_fields').style.display = cfg.u1_enabled ? '' : 'none';
// Password placeholders
if (cfg.wifi_pass_set) document.getElementById('wifi_pass').placeholder = '(set) Leave blank to keep';
if (cfg.mqtt_pass_set) document.getElementById('mqtt_pass').placeholder = '(set) Leave blank to keep';
Expand All @@ -318,6 +346,22 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
document.getElementById('prusalink_fields').style.display = this.checked ? '' : 'none';
});

// Show/hide U1 fields based on toggle
document.getElementById('u1_enabled').addEventListener('change', function() {
document.getElementById('u1_fields').style.display = this.checked ? '' : 'none';
});

// Auto-suggest hostname when U1 channel changes — only if user hasn't customized
// beyond the default ("spoolsense" or empty). Saves the "what should I name this scanner?" decision.
document.getElementById('u1_channel').addEventListener('change', function() {
var hostInput = document.getElementById('hostname');
var current = (hostInput.value || '').trim();
var defaults = ['', 'spoolsense', 'spoolsense-t0', 'spoolsense-t1', 'spoolsense-t2', 'spoolsense-t3'];
if (defaults.indexOf(current) >= 0) {
hostInput.value = 'spoolsense-t' + this.value;
}
});

function normalizeHostname(v) {
return (v || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '').replace(/^-+|-+$/g, '').slice(0, 32) || 'spoolsense';
}
Expand Down Expand Up @@ -354,7 +398,9 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
prusalink_url: document.getElementById('prusalink_url').value.trim(),
prusalink_api_key: document.getElementById('prusalink_api_key').value,
bambu_dashboard: document.getElementById('bambu_dashboard').checked ? 1 : 0,
wifi_keep_awake: document.getElementById('wifi_keep_awake').checked ? 1 : 0
wifi_keep_awake: document.getElementById('wifi_keep_awake').checked ? 1 : 0,
u1_enabled: document.getElementById('u1_enabled').checked ? 1 : 0,
u1_channel: parseInt(document.getElementById('u1_channel').value) || 0
};

fetch('/api/config', {
Expand Down
23 changes: 23 additions & 0 deletions src/ConfigurationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ static const char* NVS_KEY_HOSTNAME = "hostname";
static const char* NVS_KEY_LOW_SPOOL = "low_spool_g";
static const char* NVS_KEY_BAMBU_DASH = "bambu_dash";
static const char* NVS_KEY_WIFI_AWAKE = "wifi_awake";
static const char* NVS_KEY_U1_ON = "u1_on";
static const char* NVS_KEY_U1_CHANNEL = "u1_channel";

// Sanitize hostname: enforce mDNS naming constraints (lowercase alphanum + hyphens,
// no leading/trailing hyphens) and reject empty strings to avoid boot-time errors.
Expand Down Expand Up @@ -258,6 +260,15 @@ bool ConfigurationManager::loadFromNVS() {
_wifiKeepAwake = prefs.getBool(NVS_KEY_WIFI_AWAKE, false);
anyOverride = true;
}
if (prefs.isKey(NVS_KEY_U1_ON)) {
_u1Enabled = prefs.getBool(NVS_KEY_U1_ON, false);
anyOverride = true;
}
if (prefs.isKey(NVS_KEY_U1_CHANNEL)) {
uint8_t ch = prefs.getUChar(NVS_KEY_U1_CHANNEL, 0);
_u1Channel = (ch <= 3) ? ch : 0; // clamp invalid values from NVS
anyOverride = true;
}

prefs.end();
return anyOverride;
Expand Down Expand Up @@ -368,6 +379,14 @@ bool ConfigurationManager::isWifiKeepAwakeEnabled() const {
return _wifiKeepAwake;
}

bool ConfigurationManager::isU1Enabled() const {
return _u1Enabled;
}

uint8_t ConfigurationManager::getU1Channel() const {
return _u1Channel;
}

void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const {
memset(&out, 0, sizeof(out));
strncpy(out.wifi_ssid, _ssid, sizeof(out.wifi_ssid) - 1);
Expand All @@ -393,6 +412,8 @@ void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const {
out.low_spool_threshold_g = _lowSpoolThreshold;
out.bambu_dashboard = _bambuDashboard ? 1 : 0;
out.wifi_keep_awake = _wifiKeepAwake ? 1 : 0;
out.u1_enabled = _u1Enabled ? 1 : 0;
out.u1_channel = _u1Channel;
}

#ifndef NATIVE_TEST
Expand Down Expand Up @@ -438,6 +459,8 @@ bool ConfigurationManager::saveToNVS(const ConfigUpdate& update) {
prefs.putUShort(NVS_KEY_LOW_SPOOL, update.low_spool_threshold_g);
prefs.putBool(NVS_KEY_BAMBU_DASH, update.bambu_dashboard != 0);
prefs.putBool(NVS_KEY_WIFI_AWAKE, update.wifi_keep_awake != 0);
prefs.putBool(NVS_KEY_U1_ON, update.u1_enabled != 0);
prefs.putUChar(NVS_KEY_U1_CHANNEL, (update.u1_channel <= 3) ? update.u1_channel : 0);

// Invalidate Spoolman enrichment cache on config change to force re-fetch
// (config change could invalidate cached spool lookups)
Expand Down
11 changes: 11 additions & 0 deletions src/ConfigurationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ struct ConfigUpdate {
uint16_t low_spool_threshold_g; // grams below which LED breathes (default 100)
uint8_t bambu_dashboard;
uint8_t wifi_keep_awake; // disable WiFi modem sleep (better RSSI, more power)
// Snapmaker U1 direct-mode integration (extended firmware, Filament Detection: External)
uint8_t u1_enabled; // 0 = off, 1 = post to /printer/filament_detect/set on scans
uint8_t u1_channel; // 0..3 — toolhead this scanner is bound to
};

class ConfigurationManager {
Expand Down Expand Up @@ -94,6 +97,10 @@ class ConfigurationManager {
// radio stays fully powered. Trades idle current for better RSSI/latency.
bool isWifiKeepAwakeEnabled() const;

// Snapmaker U1 direct-mode (Phase 1 / fixed-channel)
bool isU1Enabled() const;
uint8_t getU1Channel() const;

// Web config support
void getCurrentConfig(ConfigUpdate& out) const;
bool saveToNVS(const ConfigUpdate& update);
Expand Down Expand Up @@ -146,6 +153,10 @@ class ConfigurationManager {
bool _bambuDashboard = false;
bool _wifiKeepAwake = false;

// Snapmaker U1 direct-mode
bool _u1Enabled = false;
uint8_t _u1Channel = 0;

bool _initialized = false;
};

Expand Down
Loading