"""
Hoverboard Calculator — Because Michael J. Fox Lied To Me (But It's Cool)
Built for the Build Small Hackathon 2026.
An AI-powered acoustic phase array designer for frictionless surfaces.
Not levitation. Friction cancellation. The distinction matters —
it requires orders of magnitude less force.
Tile a surface with equilateral triangles. Speaker at each vertex.
Three speakers converge on each centroid = acoustic pressure node.
2,300 nodes = a skateboard that glides like it's on ice.
"It's not a hoverboard. It's an acoustic air hockey table.
And the grid is triangles, not holes."
Inspired by Real Genius (1985), Weird Science (1985), and one guy
who drew helicopters 400 years too early.
"""
import gradio as gr
import math
import json
import os
from openai import OpenAI
# --- NVIDIA Nemotron ---
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")
NEMOTRON_MODEL = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
nvidia_client = None
if NVIDIA_API_KEY:
nvidia_client = OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=NVIDIA_API_KEY,
)
print("NVIDIA Nemotron connected.")
else:
print("No NVIDIA_API_KEY — running in fallback mode.")
# --- Physics Engine ---
SPEED_OF_SOUND = 343.0 # m/s at 20C
FREQUENCY = 40000 # 40kHz ultrasonic
WAVELENGTH = SPEED_OF_SOUND / FREQUENCY # ~8.575mm
FORCE_PER_NODE_GRAMS = 3.0 # conservative, from published research
def calculate_grid(width_m, height_m, triangle_side_mm):
"""Calculate the full triangular grid for a surface."""
side_m = triangle_side_mm / 1000.0
h = side_m * math.sqrt(3) / 2 # triangle height
cols = int(width_m / side_m) + 1
rows = int(height_m / h) + 1
# Generate vertex positions (speaker locations)
vertices = set()
triangles = []
for row in range(rows):
for col in range(cols):
x = col * side_m + (0.5 * side_m if row % 2 else 0)
y = row * h
if x <= width_m + 0.001 and y <= height_m + 0.001:
vertices.add((round(x, 6), round(y, 6)))
vertices = sorted(vertices)
# Count triangles (each upward + downward pair in the grid)
num_triangles_approx = int((width_m * height_m) / (0.5 * side_m * h))
# Centroids = pressure nodes
num_nodes = num_triangles_approx
return {
"vertices": len(vertices),
"triangles": num_triangles_approx,
"nodes": num_nodes,
"triangle_side_mm": triangle_side_mm,
"surface_area_m2": round(width_m * height_m, 2),
"wavelength_mm": round(WAVELENGTH * 1000, 1),
"rows": rows,
"cols": cols,
}
def calculate_bom(grid):
"""Generate the bill of materials with purchase links."""
n_transducers = grid["vertices"]
n_hv583 = math.ceil(n_transducers / 16)
n_esp32 = math.ceil(n_hv583 / 8)
n_driver_ics = math.ceil(n_transducers / 4)
n_pcb_tiles = math.ceil(n_transducers / 30)
n_psu = max(1, math.ceil(n_transducers / 600))
items = [
{"item": "Ultrasonic transducers (40kHz, 16mm)", "qty": n_transducers, "unit": 0.15,
"link": "https://www.amazon.com/s?k=40khz+ultrasonic+transducer+16mm",
"note": "Buy in bulk packs of 100+. TCT40-16T/R pairs."},
{"item": "HV583 shift registers (16-ch)", "qty": n_hv583, "unit": 7.00,
"link": "https://www.amazon.com/s?k=HV583+shift+register",
"note": "High-voltage 16-ch shift register. Alt: check Mouser/DigiKey if unavailable."},
{"item": "ESP32-S3 DevKit", "qty": n_esp32, "unit": 5.00,
"link": "https://www.amazon.com/s?k=ESP32-S3+development+board",
"note": "One ESP32 controls up to 8 shift registers (128 transducers)."},
{"item": "L293D motor driver IC", "qty": n_driver_ics, "unit": 0.05,
"link": "https://www.amazon.com/s?k=L293D+motor+driver+IC",
"note": "Quad half-H bridge. 4 transducers per chip. Buy bulk packs."},
{"item": "Prototype PCB boards (large)", "qty": n_pcb_tiles, "unit": 8.00,
"link": "https://www.amazon.com/s?k=prototype+PCB+board+large+double+sided",
"note": "~30 transducers per tile. Or order custom PCBs from JLCPCB ($2/board)."},
{"item": "48V DC power supply (10A)", "qty": n_psu, "unit": 60.00,
"link": "https://www.amazon.com/s?k=48V+10A+DC+power+supply+switching",
"note": "Switching PSU. One per ~600 transducers."},
{"item": "Table frame materials", "qty": 1, "unit": 150.00,
"link": "https://www.amazon.com/s?k=plywood+sheet+project+board",
"note": "3/4\" plywood base + aluminum angle brackets + acrylic top sheet."},
{"item": "Wiring & connectors kit", "qty": 1, "unit": 100.00,
"link": "https://www.amazon.com/s?k=22AWG+hookup+wire+kit+electronics+connectors",
"note": "22AWG solid core wire, JST connectors, standoffs, solder."},
]
for item in items:
item["total"] = round(item["qty"] * item["unit"], 2)
grand_total = sum(i["total"] for i in items)
return items, grand_total
def calculate_weight_capacity(grid):
"""How much can the surface support?"""
total_force_grams = grid["nodes"] * FORCE_PER_NODE_GRAMS
total_force_kg = total_force_grams / 1000
scenarios = [
{"object": "Skateboard deck", "weight_kg": 2.0},
{"object": "Small package", "weight_kg": 5.0},
{"object": "Heavy package", "weight_kg": 20.0},
{"object": "Small human (child)", "weight_kg": 30.0},
]
for s in scenarios:
force_per_node = (s["weight_kg"] * 1000) / grid["nodes"]
s["force_per_node_g"] = round(force_per_node, 2)
s["feasible"] = force_per_node <= FORCE_PER_NODE_GRAMS
return scenarios, total_force_kg
def calculate_phase_delay(node_x, node_y, speaker_x, speaker_y):
"""Calculate phase delay for a single speaker targeting a node."""
dist = math.sqrt((node_x - speaker_x)**2 + (node_y - speaker_y)**2)
phase = (dist % WAVELENGTH) / WAVELENGTH * 360
return round(phase, 1)
def generate_ai_insight(grid, bom_total, scenarios):
"""Let the AI add some flavor."""
if not nvidia_client:
return "AI insight requires NVIDIA_API_KEY."
feasible = [s for s in scenarios if s["feasible"]]
infeasible = [s for s in scenarios if not s["feasible"]]
prompt = f"""I just designed an acoustic frictionless surface. Here are the specs:
- Surface: {grid['surface_area_m2']} m2
- Triangles: {grid['triangles']} equilateral, {grid['triangle_side_mm']}mm sides
- Transducers: {grid['vertices']} at 40kHz ultrasonic
- Pressure nodes: {grid['nodes']}
- Total cost: ${bom_total:.0f}
- Can support: {', '.join(s['object'] for s in feasible) if feasible else 'nothing yet'}
- Cannot support: {', '.join(s['object'] for s in infeasible) if infeasible else 'everything works!'}
Give me a one-paragraph excited but technically grounded assessment.
Include one creative application idea nobody would think of.
Keep it under 100 words. Be fun."""
try:
response = nvidia_client.chat.completions.create(
model=NEMOTRON_MODEL,
messages=[
{"role": "system", "content": "You are an enthusiastic physics engineer who loves acoustic levitation. Be fun but accurate. Short responses."},
{"role": "user", "content": prompt}
],
max_tokens=200,
temperature=0.7,
)
return response.choices[0].message.content.strip()
except Exception as e:
return f"(AI thinking failed: {e})"
# --- Main Function ---
def design_hoverboard(width_ft, height_ft, triangle_side_mm):
"""The main calculation pipeline."""
width_m = width_ft * 0.3048
height_m = height_ft * 0.3048
grid = calculate_grid(width_m, height_m, triangle_side_mm)
bom, bom_total = calculate_bom(grid)
scenarios, total_force = calculate_weight_capacity(grid)
insight = generate_ai_insight(grid, bom_total, scenarios)
# Pull counts for build steps
n_pcb_tiles = math.ceil(grid["vertices"] / 30)
n_esp32 = math.ceil(math.ceil(grid["vertices"] / 16) / 8)
n_psu = max(1, math.ceil(grid["vertices"] / 600))
# Build the HTML output
html = f"""
{grid['vertices']:,}
Transducers
{grid['triangles']:,}
Triangles
{grid['nodes']:,}
Pressure Nodes
{grid['wavelength_mm']}mm
Wavelength
Weight Capacity
{''.join(f"""
{'✅' if s['feasible'] else '❌'}
{s['object']}
{s['weight_kg']}kg — {s['force_per_node_g']}g/node
""" for s in scenarios)}
Published research: ~3g force per 40kHz node. Friction cancellation requires less than full levitation.
Bill of Materials — Order Parts
| Component | Qty | Unit $ | Total | Buy |
{''.join(f'| {i["item"]} {i.get("note","")} | {i["qty"]:,} | ${i["unit"]:.2f} | ${i["total"]:.2f} | Amazon |
' for i in bom)}
| TOTAL | ${bom_total:,.2f} | |
Build Steps
1
Cut the base. Cut 3/4" plywood to {width_ft}' x {height_ft}'.
This is your table surface. Sand it flat.
2
Mark the triangle grid. Draw equilateral triangles with {grid['triangle_side_mm']}mm sides
across the entire surface. Each vertex = one transducer hole.
You need {grid['vertices']:,} holes. Use a template and a drill press with a 16mm bit.
3
Mount transducers. Press-fit or hot-glue one 40kHz transducer into each hole,
facing UP. All transducers should be flush with the surface.
4
Wire PCB tiles. Solder {grid['vertices']:,} transducers to {n_pcb_tiles} PCB tiles
(~30 per tile). Each tile connects to one L293D driver cluster and one HV583 shift register.
5
Connect controllers. Wire {n_esp32} ESP32(s) to the shift registers via SPI.
Each ESP32 handles up to 8 HV583 chips (128 transducers).
Flash with the phase array firmware (open source, link in references).
6
Power it up. Connect {n_psu} 48V PSU(s) to the driver boards.
Add a master power switch and a fuse (10A per PSU).
Warning: 48V can hurt you. Respect the voltage.
7
Top it off. Lay a thin acrylic sheet (1/8") over the transducer surface.
This is your friction-cancellation surface. The acoustic pressure nodes form
between the acrylic and anything sitting on it.
8
Test it. Power on. Place a lightweight object (playing card, small foam block)
on the surface. It should glide with near-zero friction. Adjust phase delays
in firmware to optimize node placement.
"""
return html
# --- CSS --- 80s Nintendo / Excitebike / LEGO primary colors
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap');
.hb-container {
background: #F0F0E8;
color: #222;
border-radius: 0;
border: 4px solid #222;
padding: 32px;
font-family: 'VT323', monospace;
max-width: 850px;
margin: 0 auto;
box-shadow: 8px 8px 0px #222;
}
.hb-header { text-align: center; margin-bottom: 28px; }
.hb-title { font-family: 'Press Start 2P', cursive; font-size: 1.4em; color: #E40000; letter-spacing: 0.05em; }
.hb-subtitle { font-size: 1.1em; color: #666; margin-top: 8px; font-family: 'VT323', monospace; }
.hb-stat-row { display: flex; justify-content: space-around; margin: 24px 0; gap: 12px; }
.hb-stat {
text-align: center; background: #FFD700; border: 4px solid #222;
padding: 24px 20px; box-shadow: 6px 6px 0px #222;
flex: 1; clip-path: polygon(50% 0%, 100% 25%, 100% 100%, 0% 100%, 0% 25%);
padding-top: 36px;
}
.hb-stat:nth-child(2) { background: #E40000; }
.hb-stat:nth-child(2) .hb-stat-num { color: #FFD700; }
.hb-stat:nth-child(2) .hb-stat-label { color: #fff; }
.hb-stat:nth-child(3) { background: #0050DD; }
.hb-stat:nth-child(3) .hb-stat-num { color: #FFD700; }
.hb-stat:nth-child(3) .hb-stat-label { color: #fff; }
.hb-stat:nth-child(4) { background: #00A800; }
.hb-stat:nth-child(4) .hb-stat-num { color: #fff; }
.hb-stat:nth-child(4) .hb-stat-label { color: #222; }
.hb-stat-num { font-family: 'Press Start 2P', cursive; font-size: 1.1em; color: #E40000; }
.hb-stat-label { font-size: 1.1em; color: #222; text-transform: uppercase; letter-spacing: 0.1em; font-family: 'VT323', monospace; margin-top: 6px; }
.hb-section { margin: 24px 0; }
.hb-section-title {
font-family: 'Press Start 2P', cursive; font-size: 0.7em; color: #0050DD;
text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 12px;
border-bottom: 3px solid #0050DD; padding-bottom: 6px;
}
.hb-weight-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.hb-weight-item { padding: 16px; border: 3px solid #222; background: #fff; box-shadow: 4px 4px 0px #222; }
.hb-feasible { border-left: 8px solid #00A800; background: #F0FFF0; }
.hb-infeasible { border-left: 8px solid #E40000; background: #FFF0F0; opacity: 0.8; }
.hb-weight-icon { font-size: 1.2em; }
.hb-weight-name { font-size: 1.1em; font-weight: bold; color: #222; font-family: 'VT323', monospace; }
.hb-weight-detail { font-size: 1em; color: #666; font-family: 'VT323', monospace; }
.hb-note { font-size: 0.9em; color: #888; margin-top: 8px; font-style: italic; }
.hb-bom { width: 100%; border-collapse: collapse; font-size: 1em; font-family: 'VT323', monospace; }
.hb-bom th { text-align: left; color: #fff; background: #0050DD; padding: 8px; font-size: 0.9em; }
.hb-bom td { padding: 6px 8px; border-bottom: 2px solid #ddd; }
.hb-bom tr:hover { background: #FFD700; }
.hb-bom-total { font-weight: bold; color: #00A800; }
.hb-bom-total td { border-top: 3px solid #E40000; padding-top: 8px; font-size: 1.2em; }
.hb-part-note { font-size: 0.85em; color: #888; margin-top: 2px; font-style: italic; }
.hb-buy-link {
color: #fff; text-decoration: none; font-weight: bold;
background: #E40000; border: 2px solid #222; padding: 3px 10px;
font-size: 0.9em; font-family: 'Press Start 2P', cursive; font-size: 0.6em;
box-shadow: 2px 2px 0px #222;
}
.hb-buy-link:hover { background: #00A800; }
.hb-build-steps { display: flex; flex-direction: column; gap: 16px; }
.hb-step {
display: flex; gap: 16px; align-items: flex-start;
background: #fff; border: 3px solid #222; padding: 18px;
box-shadow: 5px 5px 0px #222;
transform: translateX(0);
transition: transform 0.15s;
}
.hb-step:hover { transform: translateY(-4px); box-shadow: 5px 9px 0px #222; }
.hb-step:nth-child(odd) { border-left: 8px solid #0050DD; }
.hb-step:nth-child(even) { border-left: 8px solid #E40000; }
.hb-step-num {
background: #E40000; color: #fff; font-weight: bold;
font-family: 'Press Start 2P', cursive; font-size: 0.7em;
width: 40px; height: 40px; border: 3px solid #222;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
.hb-step:nth-child(even) .hb-step-num { background: #0050DD; }
.hb-step:nth-child(3) .hb-step-num { background: #FFD700; color: #222; }
.hb-step:nth-child(5) .hb-step-num { background: #00A800; }
.hb-step-text { font-size: 1.15em; line-height: 1.7; color: #333; font-family: 'VT323', monospace; }
.hb-step-text strong { color: #0050DD; }
.hb-step-text em { color: #E40000; }
.hb-ai { background: #FFD700; border: 3px solid #222; padding: 16px; box-shadow: 4px 4px 0px #222; }
.hb-insight { font-size: 1.1em; line-height: 1.6; color: #222; font-family: 'VT323', monospace; }
.hb-footer {
text-align: center; margin-top: 28px; padding-top: 16px;
border-top: 3px solid #222; font-size: 0.95em; color: #666;
font-family: 'VT323', monospace; line-height: 1.8;
}
.hb-dedication { color: #E40000; font-family: 'Press Start 2P', cursive; font-size: 0.6em; }
.gradio-container { background: #C8C8B8 !important; }
footer { display: none !important; }
button.primary {
background: #E40000 !important; color: #fff !important;
border: 3px solid #222 !important; font-family: 'Press Start 2P', cursive !important;
font-size: 0.8em !important; box-shadow: 4px 4px 0px #222 !important;
border-radius: 0 !important;
}
button.primary:hover { background: #00A800 !important; }
"""
# --- Gradio App ---
with gr.Blocks(css=CUSTOM_CSS, title="Hoverboard Calculator", theme=gr.themes.Base()) as app:
gr.HTML("""
HOVERBOARD CALCULATOR
Because Michael J. Fox Lied To Me (But It's Cool)
Not levitation. Friction cancellation. The distinction matters.
""")
with gr.Row():
width = gr.Slider(minimum=1, maximum=12, value=6, step=0.5, label="Width (feet)")
height = gr.Slider(minimum=1, maximum=12, value=6, step=0.5, label="Height (feet)")
tri_size = gr.Slider(minimum=25, maximum=150, value=75, step=5, label="Triangle side (mm)")
calc_btn = gr.Button("CALCULATE PHASE ARRAY", variant="primary", size="lg")
output = gr.HTML()
calc_btn.click(
fn=design_hoverboard,
inputs=[width, height, tri_size],
outputs=[output],
)
gr.HTML("""
Inspired by Real Genius (1985), Weird Science (1985), and one guy who drew helicopters 400 years too early.
Heuremen -- Build Small Hackathon 2026
""")
if __name__ == "__main__":
app.launch()