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
27 changes: 27 additions & 0 deletions src/lib/components/GlueScaler.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
// GlueScaler.svelte — main UV-reactive bed glue (the "specialized" recipe).
// Data only; the engine lives in WeightScaler.svelte. 1× ≈ 2 oz (smallest
// worthwhile batch); default 2× ≈ 4 oz (recommended minimum).
import WeightScaler from './WeightScaler.svelte';

const ingredients = [
{ item: 'PVP-K90 powder', grams: 4.0, role: 'Primary film former — K90 (MW ~1.3M) is the strength upgrade over Frank’s K30' },
{ item: 'PVA, 88% hydrolyzed (cold-water grade)', grams: 1.2, role: 'Secondary film former / toughness — pins a minimum water fraction' },
{ item: 'PEG-400', grams: 1.0, role: 'Tackifier / plasticizer — the highest-leverage bond-strength additive' },
{ item: '1% boric-acid stock solution', grams: 0.85, role: 'Delivers a trace PVA crosslink (~0.7% of PVA) — dosed via stock so it’s weighable' },
{ item: 'Coated SrAl₂O₄:Eu,Dy phosphor, 35–50 µm', grams: 2.0, role: 'UV-reactive coverage indicator — MUST be waterproof/encapsulated grade' },
{ item: 'Ethanol (≥95%, denatured ok)', grams: 26.5, role: 'Co-solvent for PVP/PEG; flashes off on the heated bed' },
{ item: 'Distilled water (free water)', grams: 20.86, role: 'Dissolves the PVA; carrier lands at ~55:45 ethanol:water' }
];

const presets = [
{ factor: 1, label: '×1 · ~2 oz' },
{ factor: 2, label: '×2 · ~4 oz', recommended: true },
{ factor: 4, label: '×4 · ~8 oz' }
];

const footnote =
'Solids are ~14.6% by weight (PVP:PVA ≈ 77:23). 1× is the smallest worthwhile batch; 2× (~4 oz) is the recommended minimum. A 0.01 g scale is assumed — the boric acid is dosed as a 1% stock because the neat mass (≈8 mg) is below that resolution.';
</script>

<WeightScaler title="Glue batch scaler (by weight)" {ingredients} {presets} defaultFactor={2} {footnote} />
35 changes: 35 additions & 0 deletions src/lib/components/GlueScalerPinch.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
// GlueScalerPinch.svelte — "in a pinch" simplified derivation from on-hand
// chemicals (PVP-40, PVA powder, 91% IPA, distilled water, heavy PEG/PEO,
// plain strontium aluminate). 1 oz base; presets are absolute mini sizes.
// Still stronger than Frank's via the PEG/PEO tack lever + slightly higher
// adhesive solids — no K90, no boric acid, no coated phosphor required.
import WeightScaler from './WeightScaler.svelte';

// 1× = 1 oz ≈ 28.35 g finished glue.
const ingredients = [
{ item: 'PVP-40 powder (K-30 class, MW ~40k)', grams: 2.8, role: 'Film former — same PVP grade as Frank’s; the backbone' },
{ item: 'PVA lab powder', grams: 1.1, role: 'Toughness (PVP:PVA ≈ 72:28, echoes Frank’s 70:30)' },
{ item: 'PEG/PEO powder (heavy MW)', grams: 0.5, role: 'Tackifier — the strength edge over Frank’s; potent & stringy, start low' },
{ item: 'Strontium aluminate (plain, uncoated)', grams: 1.0, role: 'UV coverage indicator — uncoated, so mix small & use fresh' },
{ item: '91% isopropyl alcohol', grams: 16.0, role: 'Carrier; flashes off. Kept IPA-heavy to slow phosphor hydrolysis' },
{ item: 'Distilled water', grams: 6.95, role: 'Just enough to dissolve the PVA (carrier ≈ 63:37 IPA:water)' }
];

const presets = [
{ factor: 1, label: '1 oz' },
{ factor: 2, label: '2 oz' },
{ factor: 4, label: '4 oz' }
];

const footnote =
'Adhesive solids ~15.5% (PVP+PVA+PEO), a touch above Frank’s — the PEG/PEO is the tack lever. Uncoated phosphor hydrolyzes in water over time, so mix mini batches and use fresh; shake before each use. Heavy PEO strings — if your applicator clogs or cobwebs, cut the PEO. A 0.01 g scale is assumed.';
</script>

<WeightScaler
title="In-a-pinch batch scaler (on-hand chemicals)"
{ingredients}
{presets}
defaultFactor={1}
{footnote}
/>
112 changes: 112 additions & 0 deletions src/lib/components/WeightScaler.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script lang="ts">
// WeightScaler.svelte — generic by-weight batch scaler engine.
//
// Recipe data lives in thin wrapper components (GlueScaler, GlueScalerPinch)
// so the wrappers can be dropped into the mdsvex blog without tripping its
// brace-escaper. Everything is grams for a 0.01 g scale, with a g <-> oz
// toggle. `factor` multiplies the base-batch grams.

type Ingredient = { item: string; grams: number; role: string };
type Preset = { factor: number; label: string; recommended?: boolean };

let {
title = 'Batch scaler (by weight)',
ingredients = [],
presets = [{ factor: 1, label: '×1' }],
defaultFactor = presets[0]?.factor ?? 1,
totalLabel = 'Finished glue',
footnote = ''
}: {
title?: string;
ingredients?: Ingredient[];
presets?: Preset[];
defaultFactor?: number;
totalLabel?: string;
footnote?: string;
} = $props();

const G_PER_OZ = 28.349523125;

// null = "follow the recipe's default"; a click pins an explicit factor.
let factorOverride = $state<number | null>(null);
const factor = $derived(factorOverride ?? defaultFactor);
let unit = $state<'g' | 'oz'>('g');

const rows = $derived(
ingredients.map((ing) => {
const grams = ing.grams * factor;
const amount = unit === 'oz' ? (grams / G_PER_OZ).toFixed(3) : grams.toFixed(2);
return { item: ing.item, role: ing.role, amount };
})
);

const totalGrams = $derived(ingredients.reduce((s, i) => s + i.grams, 0) * factor);
const totalOz = $derived(totalGrams / G_PER_OZ);
</script>

<div class="card p-4 preset-outlined-surface-500 not-prose my-6">
<div class="mb-3 flex flex-wrap items-center justify-between gap-3">
<h3 class="m-0 text-lg font-semibold">{title}</h3>
<div class="flex items-center gap-2">
<div class="flex gap-1" role="group" aria-label="Scale the batch size">
{#each presets as p (p.factor)}
<button
type="button"
class="badge {factor === p.factor
? 'preset-filled-primary-500'
: 'preset-outlined-surface-500'}"
aria-pressed={factor === p.factor}
aria-label={`Batch ${p.label}${p.recommended ? ', recommended minimum' : ''}`}
onclick={() => (factorOverride = p.factor)}
>
{p.label}{p.recommended ? ' ✓' : ''}
</button>
{/each}
</div>
<button
type="button"
class="badge preset-outlined-surface-500"
aria-pressed={unit === 'oz'}
aria-label="Toggle grams or ounces"
onclick={() => (unit = unit === 'g' ? 'oz' : 'g')}
>
{unit === 'g' ? 'grams' : 'ounces'}
</button>
</div>
</div>

<table class="w-full text-sm">
<caption class="sr-only">
{title}, ~{totalOz.toFixed(1)} oz, shown in {unit === 'g' ? 'grams' : 'ounces'}
</caption>
<thead>
<tr class="text-left text-surface-600 dark:text-surface-400">
<th scope="col" class="py-1 pr-3 font-medium">Weight</th>
<th scope="col" class="py-1 font-medium">Ingredient</th>
</tr>
</thead>
<tbody>
{#each rows as row (row.item)}
<tr class="border-t border-surface-300 dark:border-surface-700">
<td class="whitespace-nowrap py-1 pr-3 font-mono">{row.amount} {unit}</td>
<td class="py-1">
<span class="font-medium">{row.item}</span>
<span class="block text-xs text-surface-500">{row.role}</span>
</td>
</tr>
{/each}
<tr class="border-t-2 border-surface-400 dark:border-surface-600 font-semibold">
<td class="whitespace-nowrap py-1 pr-3 font-mono">
{unit === 'oz' ? totalOz.toFixed(3) : totalGrams.toFixed(2)}
{unit}
</td>
<td class="py-1">{totalLabel} (~{totalOz.toFixed(1)} oz)</td>
</tr>
</tbody>
</table>

{#if footnote}
<p class="mt-3 text-xs text-surface-500">{footnote}</p>
{/if}
<p class="sr-only" aria-live="polite">Showing ~{totalOz.toFixed(1)} oz in {unit === 'g' ? 'grams' : 'ounces'}.</p>
</div>
83 changes: 83 additions & 0 deletions src/posts/2026-06-06-uv-reactive-bed-glue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: "Bed glue you can see in UV (and a Klipper gate that won't print without it)"
date: "2026-06-06"
description: "A 3D-printer bed adhesive stronger on paper than Frank's Instagoo, infused with strontium-aluminate phosphor so a 365 nm flash shows coverage — with a by-weight batch scaler, an off-the-shelf UV sensing BOM, and a Klipper pre-print gate."
tags: ["3d-printing", "klipper", "chemistry", "adhesives", "strontium-aluminate", "uv", "phosphor", "diy", "maker", "recipe"]
category: "tutorial"
published: false
slug: "uv-reactive-bed-glue"
excerpt: "Stronger-on-paper PVP/PVA bed glue dosed with strontium-aluminate phosphor, a by-weight batch scaler, a Mouser UV-sensing BOM, and a Klipper macro that aborts on a bare bed."
---

<script>
import GlueScaler from '$lib/components/GlueScaler.svelte';
import GlueScalerPinch from '$lib/components/GlueScalerPinch.svelte';
</script>

[Frank's "Instagoo"](https://goo.by.frank.af/) is a tidy 3D-printer bed glue: 21 g PVP-K30 + 9 g PVA in 200 ml of ~50% isopropyl alcohol — a 70:30 PVP:PVA solution at roughly 15% solids. It's the same chemistry family as the [DIY "Super Goop"](https://hackaday.com/2022/12/06/homebrew-3d-printer-goop-promises-better-bed-adhesion/) lineage and the commercial brush-ons: [Vision Miner Nano](https://visionminer.com/products/nano-polymer-adhesive)'s SDS discloses only an IPA carrier and a trade-secret polymer, and [Luke's Laboratory](https://www.lukeslabonline.com/products/bed-adhesive) sells Standard/Double/Triple "strength," which is just the same liquid at different solids. None of them publishes a real bond number.

I wanted two things on top: glue that is **stronger**, and glue I can **see** 👀. The seeing part is the fun bit — dose it with strontium-aluminate phosphor and a coat lights up green under UV, so a sensor (and a Klipper macro) can check the bed is actually covered before a print starts.

Fair warning up front: the "stronger" claim here is mechanistic, not yet measured. I haven't put a force gauge on a peel test — that's the next post. What follows is three changes each backed by a named source, which is the most I'll assert until I have numbers.

The full build — BOM, wiring, the Klipper config and host script — lives at **[transscendsurvival.org/tinyland-goo](https://transscendsurvival.org/tinyland-goo/)**. This post is the short version.

## The recipe

Everything by weight, for a 0.01 g scale. 1× (~2 oz) is the smallest worthwhile batch; **2× (~4 oz) is the recommended minimum**, so that's the default below.

<GlueScaler />

## Three honest knobs

Each of these is a documented lever, not a hunch:

| Knob | Change | Why it should help | The number to watch |
| --- | --- | --- | --- |
| Film former | PVP-K30 → [PVP-K90](https://ulipolymer.com/difference-between-pvp-k30-and-pvp-k90/) | ~30× the molecular weight (~1.3M vs ~40k) → more viscosity, film cohesion, tack | it's a drop-in swap |
| Tackifier | add PEG-400 | pressure-sensitive tack in [PVP/PEG blends peaks near 36 wt% PEG](https://www.tandfonline.com/doi/abs/10.1080/00218460213491) | start at ~20% so the film still releases |
| Crosslink | trace boric acid | [boric acid bonds PVA](https://iopscience.iop.org/article/10.1088/2631-8695/ad4cb4) for cohesion while staying water-redispersible | keep it ≤1% of the PVA — past that it stops releasing |

That last row is the one number worth getting right: enough crosslink to stiffen the film, not enough to weld your print to the bed.

## The catch: strontium aluminate hates water

`SrAl₂O₄:Eu,Dy` excites across UV-A (peak ~365 nm) and [emits green at ~520 nm](https://en.wikipedia.org/wiki/Strontium_aluminate) — that's the whole trick. But bare phosphor [hydrolyzes in water](https://www.sciencedirect.com/science/article/abs/pii/S0254058407003719) to non-luminescent hydroxides and quietly stops glowing. You can't dodge it by going anhydrous, because the PVA needs water to dissolve. The fix is a silica/fluoride-coated ("waterproof") grade plus a modest water fraction. Coated powder is mandatory here, not a nicety.

It's non-toxic with no GHS hazard class, but it's a hard mineral dust — mask and gloves when weighing.

## In a pinch

The recipe above wants PVP-K90, an encapsulated phosphor, and a boric-acid crosslink — specialized stock that's still in the mail. So here's a derivation from what's already on the shelf: PVP-40, PVA lab powder, 91% IPA, distilled water, a heavy PEG/PEO powder, and plain strontium aluminate. It's for an automated applicator on **less mission-critical** printers, in 1/2/4 oz mini batches.

It still beats Frank's — same PVP/PVA backbone, a hair more solids, and the PEG/PEO tackifier doing the heavy lifting instead of K90 + crosslink. Two honest trade-offs: the uncoated phosphor hydrolyzes over time (mix small, use fresh), and heavy PEO is stringy (keep it low or the applicator cobwebs). Both glues read the same under UV.

<GlueScalerPinch />

## Seeing coverage

Flood the bed with 365 nm UV and read the green that comes back. An [AMS AS7341 spectral sensor](https://www.adafruit.com/product/4698) has channels at 515 nm and 555 nm straddling the phosphor peak, and its on-chip interference filters reject the 365 nm excitation — so a single-point read often needs no separate glass filter. Baseline the bare bed once; coverage is the green rise above it. The whole sensing BOM (OSRAM 365 nm LED, MEAN WELL driver, the sensor, a Pi) is on the [project page](https://transscendsurvival.org/tinyland-goo/).

## The Klipper gate

`PRINT_START` runs a host script (via the `gcode_shell_command` extension) that reads the sensor and writes the result back through Moonraker's `SAVE_VARIABLE`. A second macro reads that value and aborts — before any heating or motion — if coverage is too low:

```toml
[gcode_macro _COVERAGE_GATE]
gcode:
{% set min_cov = params.MIN_COVERAGE|default(70)|float %}
{% set cov = printer.save_variables.variables.coverage_pct|default(-1)|float %}
{% if cov < min_cov %}
{ action_raise_error("COVERAGE GATE: %.1f%% < %.1f%%. Re-glue and restart." % (cov, min_cov)) }
{% endif %}
```

Two-point calibration (bare bed → 0%, fully glued → 100%) is required. The complete `coverage_gate.cfg` and `coverage_gate.py` are in the [tinyland-goo repo](https://github.com/Jesssullivan/tinyland-goo).

## Safety, briefly

Flammable alcohol carrier (mix away from flame; let it flash off before the bed heats) · phosphor dust (N95/P2 + eye protection when weighing) · boric acid (reproductive hazard if ingested — gloves, away from food) · 365 nm UV-A (eye/skin hazard at power; enclose it and pulse only during the read).

Next time, hopefully, with a force gauge and actual peel numbers. Until then it glows, which is most of the fun anyway.

Cheers, -Jess
Loading