Dent Lattice — 7-Node Hardware Demonstrator

Observability-first build · undeniable physics test · M1 milestone
9 May 2026 · v1 build doc · sister to preprint and simulator

0. Goal

Build seven physical nodes. Connect them. Kill one. Watch the survivors reroute, redistribute load, and adapt their coupling phase. That is the entire goal of this build. No origami. No metabolism. No cognition. One primitive: distributed adaptive coupling under physical stress. If the seven nodes pass, the substrate is materially credible. If they fail, §11 of the preprint tells us where the redesign starts.

Do not attempt to make this build elegant. The first prototype must be ugly and instrumented.
Do oversize every observable surface — traces, LEDs, test pads, exposed coils, loose wiring. Ugly-but-measurable beats elegant-but-opaque every time.

1. Layout — controlled asymmetry

A symmetric 6-around-1 hex would hide the dynamics we need to see. The first build is deliberately asymmetric:

N1 ━━━━━━ N2 │ │ │ │ N6 ━━ N0 ━━━━━ N3 │ ·· │ · ← intentionally weak (thin coil, fewer turns) │ · N5 ━━━━ N4

One centre node (N0). Six surrounding nodes (N1–N6). All six edges from N0 to N1–N5 are full strength. The edge N0–N4 is intentionally weak — half the coil turns, twice the resistance. When load is applied between N4 and N0, the substrate must reroute around the weak edge. This single asymmetry produces visible decision-making during stress.

Each node is a 30 mm × 30 mm 2-layer PCB. Edges are not soldered — they are pluggable jumper coils mounted on header pins. Edges can be physically removed mid-experiment to simulate edge failure independently from node failure.

2. Per-node design — observability first

2.1 Required components

RefPartWhy
U1ESP32-C3-MINI-1RISC-V MCU, ESP-NOW radio, 4 GPIO available for instrumentation
U2TI DRV8837 H-bridgePolarisation flip on coupling coil
U3TI TPS61023 boost converterSupercap → 3.3 V rail
U4Allegro A1324 Hall sensorRead magnetic field at coil edge
C15 F · 2.7 V supercapacitorEnergy store, mounted vertically off-board for swap
L1–L66× detachable coils on header pinsOne per neighbour edge; pluggable; some intentionally weak
D1–D66× LEDs (one per coil)Phase status display: brightness ∝ phase
D_PHASERGB indicator LEDPhase 0=off, 1=blue, 2=green, 3=yellow, 4=orange, 5=red
D_HBWhite heartbeat LEDBlinks on every transmitted heartbeat
R_SHUNT0.1 Ω current shuntCoil current measurement; 4-pin Kelvin connection
TP1–TP10Test padsV_supercap, V_3v3, GND, INA, INB, HALL_OUT, current shunt ±, UART_TX, UART_RX
J_UART4-pin debug UART headerLive state log over serial; no firmware update without it
J_PWR2-pin external supply headerBench-power injection bypassing supercap during debug
NTC10 kΩ thermistorCoil bobbin temperature; ADC-readable

2.2 What you must NOT optimise away

No tinted indicator LEDs. Use bright, exposed, viewable-from-3-metres LEDs. Camera footage of the demonstrator must be legible.
No SMT supercaps. Use through-hole, off-board mounted on a header. They WILL need swapping. Plan for it.
No fancy enclosure. Keep boards open. A clear acrylic backplate with PCBs zip-tied or through-bolted is sufficient.
No combined coil/sensor footprint. Coil and Hall sensor must be at least 8 mm apart with a copper pour between them. Otherwise switching transients corrupt the sensor reading (see preprint §7.2).

3. PCB notes for JLCPCB

4. Coil construction — six per node

Each edge between two nodes is one coil. Two header pins on each PCB hold the coil. Coils are removable.

Coil typeWireTurnsCoreUse
STANDARD (×5)1.0 mm enamelled copper306 mm OD ferrite, 5 mm longFive strong edges from N0
WEAK (×1)0.5 mm enamelled copper156 mm OD ferrite, 5 mm longThe deliberately weak edge N0–N4
OUTER (×6)1.0 mm enamelled copper306 mm OD ferrite, 5 mm longEdges N1–N2, N2–N3, etc. completing the ring

Wind each by hand on a small lathe-or-jig. Mark them with coloured heat-shrink: red = STANDARD, white = WEAK, blue = OUTER. So during the experiment a researcher can see at a glance which edge is weak.

5. Firmware skeleton

The Python decision rule from the preprint §3.1 ports verbatim to ESP-IDF C. Each node runs the same firmware; nodes differ only in their assigned ID (set by 4 DIP switches reading the lower nibble at boot).

// dent_node.c — minimal ESP-IDF firmware skeleton

#include <esp_now.h>
#include <esp_wifi.h>

#define HEARTBEAT_PERIOD_MS 16
#define HEARTBEAT_TIMEOUT_MS 150
#define MAX_NEIGHBOURS 6

typedef struct {
    uint8_t id;
    uint8_t phase;          // 0..5
    float energy_J;
    float load_N;
    float damage;
    bool alive;
    uint64_t last_heartbeat[MAX_NEIGHBOURS];
    uint8_t neighbour_ids[MAX_NEIGHBOURS];
    uint8_t routes[256];    // dest -> next-hop id, 0xFF = unknown
} node_state_t;

static node_state_t self;

void on_hb_recv(const uint8_t *mac, const uint8_t *data, int len) {
    uint8_t nb_id = data[0];
    self.last_heartbeat[find_neighbour_idx(nb_id)] = esp_timer_get_time() / 1000;
    // import routes from this neighbour
    for (int i = 1; i + 1 < len; i += 2) {
        uint8_t dest = data[i];
        if (dest != self.id && self.routes[dest] == 0xFF) {
            self.routes[dest] = nb_id;
        }
    }
}

uint8_t decide_phase(node_state_t *n) {
    if (n->energy_J < 0.9f)              return 0;   // browned out (5% E_max)
    if (n->damage > 0.5f)                return 1;   // injured
    if (n->load_N > 8.0f)                return 5;   // fused
    if (n->load_N > 4.0f)                return 4;
    if (n->load_N > 1.5f)                return 3;
    if (n->load_N > 0.2f)                return 2;
    return 1;                                            // baseline sense
}

void heartbeat_task(void *_) {
    while (1) {
        uint8_t pkt[64] = {self.id};
        int n = 1;
        for (int d = 0; d < 256; d++) {
            if (self.routes[d] != 0xFF) { pkt[n++] = d; pkt[n++] = self.routes[d]; }
        }
        esp_now_send(BROADCAST_MAC, pkt, n);
        gpio_set_level(LED_HB, 1); vTaskDelay(2 / portTICK_PERIOD_MS); gpio_set_level(LED_HB, 0);
        vTaskDelay(HEARTBEAT_PERIOD_MS / portTICK_PERIOD_MS);
    }
}

void detect_dead_neighbours(void) {
    uint64_t now_ms = esp_timer_get_time() / 1000;
    for (int i = 0; i < MAX_NEIGHBOURS; i++) {
        if (self.last_heartbeat[i] && (now_ms - self.last_heartbeat[i]) > HEARTBEAT_TIMEOUT_MS) {
            uint8_t dead_id = self.neighbour_ids[i];
            for (int d = 0; d < 256; d++) {
                if (self.routes[d] == dead_id) self.routes[d] = 0xFF;
            }
            self.last_heartbeat[i] = 0;
            ESP_LOGI("dent", "neighbour %u declared dead, routes purged", dead_id);
        }
    }
}

void apply_phase(uint8_t phase, gpio_num_t ina, gpio_num_t inb) {
    int duty = phase * 51;   // 0..255 PWM duty per phase
    ledc_set_duty(LEDC_LOW_SPEED_MODE, ina, duty);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, ina);
}

Each node runs three FreeRTOS tasks: heartbeat (16 ms), control (62 Hz — phase decision and apply), and telemetry (10 Hz UART log).

6. Test procedures (M1 measurements)

Each procedure produces a numbered data file. Don't run a procedure without writing one. Each file gets pushed to git after the run; failed runs go in the same git history with the failure mode noted.

6.1 P1 — Reroute latency on node death

  1. Power up all 7 nodes from external bench supply (skip supercap during this test).
  2. Attach UART to a hub; log all nodes simultaneously.
  3. At t = 5 s, physically pull the supercap header on N3.
  4. Record from each surviving UART log: time of last heartbeat received from N3, time when N3 marked dead, time when all routes through N3 were purged.
  5. Repeat 50× across all interior nodes.
  6. Plot histogram of (route_purge_time − N3_power_off_time). Expected mean: 50–80 ms.

6.2 P2 — Stiffness modulation under load

  1. Mount node panel vertically, clamp N1–N6 to a force gauge (Imada DS2-200N).
  2. Apply increasing load to N4 (the weak edge): 0.5 N → 1 N → 2 N → 4 N → 8 N → 12 N.
  3. For each load step, record from each node's UART log: phase, supercap V, coil current at shunt.
  4. Plot phase vs applied load on N4 (expected: matches §3.1 of preprint to within ±0.5 N at each threshold).
  5. Plot apparent stiffness (force / displacement) vs phase. Expected: monotonically increasing.

6.3 P3 — Supercap recharge cycle (no shard delivery yet)

  1. Disconnect bench supply. Run on supercap only.
  2. Apply 4 N continuously to N4. Allow nodes to brown out.
  3. Inject a 12 V pulse for 50 ms onto J_PWR of one node at a time, mimicking a shard delivery.
  4. Record UART log: time from injection to that node returning to phase 1 sensing.
  5. Plot recovery latency. Expected: under 200 ms per node.

6.4 P4 — Asymmetry visibility

  1. Apply 4 N at N4 (along the WEAK edge to N0).
  2. Capture: phase distribution across all 7 nodes.
  3. Expected behaviour: the substrate routes load THROUGH the strong edges (e.g. via N3 or N5) instead of through the weak N0–N4 edge. N3 and N5 should be observed in higher phase than N0 itself.
  4. This is the visual proof that local rules produce non-trivial structural decisions.

7. Failure analysis (what to look for)

From the preprint §7, we expect to see one or more of these problems on first power-up. Predicting them in advance prevents panic.

SymptomLikely causeFirst fix
Heartbeats time out spuriouslyESP-NOW radio congestion at all-broadcastSwitch to unicast pairs; rate-limit at 16 ms
Hall sensor reads junk during phase changeCoil switching transient cross-couplingSoftware gate: ignore HALL for 100 µs after every flip
Coil overheats at phase 5Continuous 1.4 A through 1 Ω = 2 W spotLimit phase 5 to 5 s burst, decay to phase 4
Routes never convergeHeartbeat payload missing routesVerify the route blob is actually being transmitted; UART dump it
Nodes reboot under loadSupercap brown-out during boost transitionAdd 100 µF tantalum local to MCU rail

8. Total cost (single 7-node build)

7 PCBs (JLCPCB qty 10)£18 incl. shipping
SMT assembly (5 unique parts × 7 boards)£32 (JLCPCB Q100 service)
Through-hole + headers + supercaps + coils + LEDs£36 (LCSC + Aliexpress mixed)
Imada DS2-200N force gauge (loan from a uni lab if possible; new: £450)£0–450
USB-UART hub for parallel logging£18
Acrylic backplate + zip-ties + bench supply£25
Lab build total (with force gauge)£579
Lab build total (gauge borrowed)£129

9. Time budget

PCB design (KiCad, 7 days incl. peer review)1 week
JLCPCB lead time + SMT assembly2 weeks
Coil winding + through-hole assembly3 days
Firmware port (Python rule → ESP-IDF C)1 week
Bring-up + first heartbeat over the wire2 days
P1–P4 measurement runs1 week
Total to first published M1 dataset≈ 6 weeks

10. Definition of M1 done

M1 is complete and publishable when all four conditions hold:

  1. P1 mean reroute latency under 100 ms with σ < 30 ms across 50 trials
  2. P2 phase thresholds match preprint §3.1 within ±0.5 N at every transition, across all 7 nodes
  3. P3 supercap recovery returns nodes to phase 1 in under 250 ms after pulse injection
  4. P4 video shows visible re-routing of load away from the weak edge with no firmware adjustment between runs

If any single condition fails, write the failure mode into a follow-up commit and iterate. Do not proceed to M2 until all four pass.

11. What this build is NOT trying to prove

This build proves one mechanism: distributed adaptive coupling with self-repair, under physical load, on commodity hardware. That is enough for M1. Higher-order claims wait for higher-numbered milestones.


v1 build doc · 9 May 2026 · ShortFactory Research · sister documents: preprint · live simulator