""" 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"""
Your Hoverboard Design
{width_ft}' x {height_ft}' surface — {grid['nodes']:,} pressure nodes
{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
{''.join(f'' for i in bom)}
ComponentQtyUnit $TotalBuy
{i["item"]}
{i.get("note","")}
{i["qty"]:,}${i["unit"]:.2f}${i["total"]:.2f}Amazon
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.
AI Assessment
{insight}
""" 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()