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
14 changes: 14 additions & 0 deletions fluidd/doc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.1.6

- New FilaMan spool card, bottom right corner, showing the filament name and colour assigned to
each extruder (falling back to the bare spool id if the filament lookup fails), a button to
clear that assignment, and a button to manually repeat the startup repush instead of restarting
Moonraker. No spool picker: assigning a spool to an extruder stays FilaMan's own job. FilaMan is
an NFC/RFID filament tracking system with its own Moonraker component (the separate filaman
Bespok3d plugin), independent of Spoolman, so the card polls that component's own endpoints
directly rather than reading Fluidd's Spoolman panel or its Vuex store. Adds one script tag and
one new file to the vendored bundle; no Vuex or dialog patch, and no manifest or config change.
Inert on a printer without the filaman plugin installed: the card's first request fails and it
never appears. The repush button needs filaman 0.2.0 or newer (its `/repush` endpoint); on an
older filaman it silently does nothing. Fluidd stays v1.37.3.

## 0.1.5

- Vendored Fluidd bumped v1.37.2 to v1.37.3. Upstream adds an AFC print-start dialog, a
Expand Down
19 changes: 19 additions & 0 deletions fluidd/doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Swaps the Snapmaker-shipped Fluidd web UI for the current upstream release.
- Tool-colour preview.
- A modern bed-mesh visualizer.
- A print-start dialog that maps the file's tools onto your AFC lanes before the print begins.
- A small FilaMan spool card, if the separate filaman Moonraker plugin is installed. See below.

## Mapping lanes for a print sent from the slicer

Expand All @@ -34,6 +35,24 @@ printer actually has. Nothing is removed from the printer: the tools are still t
still call them. The count comes from the printer, so a machine with a different number of lanes
shows its own. Reload the page after changing the setting.

## FilaMan spool card

FilaMan is an NFC/RFID filament tracking system with its own Moonraker component, independent of
Spoolman. With the separate filaman Bespok3d plugin installed, a small card in the bottom right
corner shows the filament assigned to each extruder (name and colour, falling back to the bare
spool id if that lookup fails), a button to clear the assignment, and a **repush** button that
resends every assigned spool to the printer, the same thing that already happens once at boot.
Use it if a channel still shows as unknown after startup, instead of restarting Moonraker.

There is no spool picker on this card: assigning a spool to an extruder is FilaMan's own job (an
NFC tap, or its own app), not something this card does for you.

The card talks directly to the filaman component's own endpoints; it does not use Fluidd's
Spoolman panel. The repush button needs filaman 0.2.0 or newer; on an older filaman it silently
does nothing.

Nothing appears without that plugin installed: the card's first request fails and it never shows.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm nothing happens without it 😂

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hehe, yeah :D i implemented the manually repush shortly :D


## Configuration

- **Fluidd port** (default `80`): the port Fluidd is served on.
Expand Down
259 changes: 259 additions & 0 deletions fluidd/files/fluidd/b3d-filaman-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// A small floating card showing each extruder's assigned FilaMan spool, with a button to clear it
// and a button to manually repeat the startup repush.
//
// FilaMan is an NFC/RFID filament tracking system with its own Moonraker component (the filaman
// Bespok3d plugin), independent of Spoolman. It talks to Fluidd's built in Spoolman panel not at
// all, since that panel only lights up for a component actually named spoolman. This card is its
// own thing instead: it polls the filaman component's own REST endpoints directly and renders
// itself, without reading or writing anything in Fluidd's Vuex store. Deliberately no spool
// picker here: assigning a spool to an extruder is FilaMan's own job (an NFC tap, or its own
// app), this card only shows and clears what is already assigned.
//
// Inert on a printer without the filaman Moonraker component: the first status request 404s (or
// otherwise fails), the card is never created, and nothing polls again until the next page load.
//
// This file is copied into the vendored bundle by scripts/patch-fluidd.sh, which also adds the
// script tag that loads it. Re-vendoring re-applies both.

(function bespok3dFilamanCard() {
var STATUS_PATH = '/server/filaman/status'
var SPOOL_ID_PATH = '/server/filaman/spool_id'
var REPUSH_PATH = '/server/filaman/repush'
var PROXY_PATH = '/server/filaman/proxy'
var POLL_INTERVAL_MS = 5000
var CARD_ELEMENT_ID = 'b3d-filaman-card'

var spoolDetailsById = {}
var spoolDetailFetchesInFlight = {}

function requestJson(path, requestInit) {
return window.fetch(path, requestInit).then(function readBody(response) {
if (!response.ok) throw new Error('filaman request failed: ' + response.status)
return response.json()
})
}

function unwrapMoonrakerResult(body) {
return body.result || body
}

function fetchStatus() {
return requestJson(STATUS_PATH, { credentials: 'same-origin' }).then(unwrapMoonrakerResult)
}

function clearExtruderSpool(extruderName) {
var query = '?extruder=' + encodeURIComponent(extruderName)
return requestJson(SPOOL_ID_PATH + query, { method: 'POST', credentials: 'same-origin' })
}

function repushAssignedSpools() {
return requestJson(REPUSH_PATH, { method: 'POST', credentials: 'same-origin' })
}

// FilaMan's REST API nests a spool's consumption log under /api/v1/spools/{id}/consumptions,
// so the spool itself is expected at /api/v1/spools/{id}. Routed through the filaman
// component's own proxy endpoint, which already holds the credentials this card does not have.
function fetchSpoolDetail(spoolId) {
return requestJson(PROXY_PATH, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_method: 'GET',
path: '/api/v1/spools/' + spoolId,
use_v2_response: true
})
})
.then(unwrapMoonrakerResult)
.then(function readProxyResult(proxyResult) {
if (proxyResult.error) throw new Error(proxyResult.error.message || 'proxy error')
return spoolDetailFromApiSpool(proxyResult.response)
})
}

function normalizedColorHex(hex) {
return hex.charAt(0) === '#' ? hex : '#' + hex
}

function spoolDetailFromApiSpool(apiSpool) {
var filament = apiSpool.filament || {}
var colorEntry = (filament.colors || [])[0]
var initialTotalWeightG = filament.initial_total_weight_g != null
? filament.initial_total_weight_g
: apiSpool.initial_total_weight_g
var emptySpoolWeightG = apiSpool.empty_spool_weight_g
var initialWeightG = (initialTotalWeightG != null && emptySpoolWeightG != null)
? Math.max(initialTotalWeightG - emptySpoolWeightG, 0)
: filament.raw_material_weight_g

return {
name: filament.designation || null,
colorHex: colorEntry && colorEntry.color && colorEntry.color.hex_code
? normalizedColorHex(colorEntry.color.hex_code)
: null,
remainingWeightG: apiSpool.remaining_weight_g != null ? apiSpool.remaining_weight_g : null,
initialWeightG: initialWeightG != null ? initialWeightG : null
}
}

function spoolDetail(spoolId) {
var cached = spoolDetailsById[spoolId]
if (cached) return cached

if (!spoolDetailFetchesInFlight[spoolId]) {
spoolDetailFetchesInFlight[spoolId] = true
fetchSpoolDetail(spoolId).then(function cacheDetail(detail) {
delete spoolDetailFetchesInFlight[spoolId]
spoolDetailsById[spoolId] = detail
poll()
}, function keepShowingSpoolId() {
// Detail lookup failed (backend unreachable, or this FilaMan version has no per-spool
// GET). Not fatal: the row below falls back to the bare spool id. Retried next poll.
delete spoolDetailFetchesInFlight[spoolId]
})
}
return null
}

function cardElement() {
var existing = document.getElementById(CARD_ELEMENT_ID)
if (existing) return existing

var card = document.createElement('div')
card.id = CARD_ELEMENT_ID
card.style.cssText =
'position:fixed;right:12px;bottom:12px;z-index:9999;' +
'background:#1e1e20;color:#fff;border:1px solid #3a3a3d;border-radius:8px;' +
'padding:10px 12px;font:13px/1.4 sans-serif;min-width:200px;max-width:300px;' +
'box-shadow:0 2px 10px rgba(0,0,0,.5)'
document.body.appendChild(card)
return card
}

function removeCardElement() {
var existing = document.getElementById(CARD_ELEMENT_ID)
if (existing) existing.remove()
}

function iconButtonElement(label, onClick) {
var button = document.createElement('button')
button.type = 'button'
button.textContent = label
button.style.cssText =
'background:none;border:1px solid #555;border-radius:4px;' +
'color:#fff;cursor:pointer;font-size:11px;padding:1px 6px'
button.addEventListener('click', onClick)
return button
}

function headingElement() {
var heading = document.createElement('div')
heading.style.cssText =
'display:flex;align-items:center;justify-content:space-between;margin-bottom:4px'

var title = document.createElement('span')
title.style.cssText = 'font-weight:600'
title.textContent = 'FilaMan'
heading.appendChild(title)

heading.appendChild(iconButtonElement('repush', function onRepushClicked() {
repushAssignedSpools().then(poll, poll)
}))
return heading
}

function colorDotElement(colorHex) {
var dot = document.createElement('span')
dot.style.cssText =
'display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px;' +
'background:' + colorHex + ';border:1px solid rgba(255,255,255,.3)'
return dot
}

function weightFractionText(detail) {
if (detail.remainingWeightG == null || detail.initialWeightG == null) return null
return Math.round(detail.remainingWeightG) + 'g / ' + Math.round(detail.initialWeightG) + 'g'
}

function spoolRowElement(extruderName, spoolId) {
var row = document.createElement('div')
row.style.cssText = 'margin:4px 0'

var mainLine = document.createElement('div')
mainLine.style.cssText = 'display:flex;align-items:center;justify-content:space-between'

var nameCell = document.createElement('span')
nameCell.style.cssText = 'display:flex;align-items:center;overflow:hidden'

var extruderLabel = document.createElement('strong')
extruderLabel.style.cssText = 'margin-right:6px'
extruderLabel.textContent = extruderName
nameCell.appendChild(extruderLabel)

var detail = spoolId == null ? null : spoolDetail(spoolId)
var nameText = document.createElement('span')
nameText.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap'
if (spoolId == null) {
nameText.textContent = 'no spool'
nameText.style.opacity = '.7'
} else {
if (detail && detail.colorHex) nameCell.appendChild(colorDotElement(detail.colorHex))
nameText.textContent = (detail && detail.name) || ('#' + spoolId)
}
nameCell.appendChild(nameText)
mainLine.appendChild(nameCell)

if (spoolId != null) {
mainLine.appendChild(iconButtonElement('clear', function onClearClicked() {
clearExtruderSpool(extruderName).then(poll, poll)
}))
}
row.appendChild(mainLine)

var weightText = detail ? weightFractionText(detail) : null
if (weightText) {
var weightLine = document.createElement('div')
weightLine.style.cssText = 'opacity:.7;font-size:11px'
weightLine.textContent = weightText
row.appendChild(weightLine)
}

return row
}

function renderStatus(status) {
var extruderSpools = status.extruder_spools || {}
var extruderNames = Object.keys(extruderSpools)
var card = cardElement()
card.textContent = ''
card.appendChild(headingElement())

if (!extruderNames.length) {
var emptyState = document.createElement('div')
emptyState.style.cssText = 'opacity:.7'
emptyState.textContent = 'no extruders reported'
card.appendChild(emptyState)
return
}
extruderNames.forEach(function appendRow(extruderName) {
card.appendChild(spoolRowElement(extruderName, extruderSpools[extruderName]))
})
}

function poll() {
fetchStatus().then(renderStatus, removeCardElement)
}

function whenDocumentIsReady(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback)
return
}
callback()
}

whenDocumentIsReady(function start() {
poll()
window.setInterval(poll, POLL_INTERVAL_MS)
})
})()
2 changes: 1 addition & 1 deletion fluidd/files/fluidd/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<noscript>
<strong>We're sorry, but fluidd doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div><script src="./b3d-afc-toolmap.js"></script><script src="./b3d-tool-buttons.js"></script>
<div id="app"></div><script src="./b3d-afc-toolmap.js"></script><script src="./b3d-tool-buttons.js"></script><script src="./b3d-filaman-card.js"></script>
<!-- built files will be auto injected -->
</body>
</html>
2 changes: 1 addition & 1 deletion fluidd/files/fluidd/sw.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions fluidd/manifest.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "fluidd",
"title": "Fluidd",
"version": "0.1.5",
"version": "0.1.6",
"sw_version": "1.37.3",
"author": "bespoked",
"description": "Swaps SM's outdated Fluidd for the current upstream release. You get input-shaper graphs, integrated config/G-code editors, tool-colour preview, modern mesh viz. Uninstall restores the SM-shipped copy automatically; no nginx config touched.",
"description": "Swaps SM's outdated Fluidd for the current upstream release. You get input-shaper graphs, integrated config/G-code editors, tool-colour preview, modern mesh viz, and a FilaMan spool card when the filaman plugin is installed. Uninstall restores the SM-shipped copy automatically; no nginx config touched.",
"tagline": "Modern Fluidd v1.37.3: replaces the stock SM build.",
"category": "ui",
"channel": "stable",
"printer_specific": true,
"changelog": "doc/CHANGELOG.md",
"published_at": "2025-10-12",
"updated_at": "2026-08-01",
"updated_at": "2026-08-11",
"source": "https://github.com/Bespok3d/fluidd-plugin",
"homepage": "https://github.com/Bespok3d/fluidd-plugin",
"publisher": "PLACEHOLDER",
Expand Down
Loading
Loading