| """ |
| 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_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.") |
|
|
|
|
| |
|
|
| SPEED_OF_SOUND = 343.0 |
| FREQUENCY = 40000 |
| WAVELENGTH = SPEED_OF_SOUND / FREQUENCY |
| FORCE_PER_NODE_GRAMS = 3.0 |
|
|
|
|
| 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 |
|
|
| cols = int(width_m / side_m) + 1 |
| rows = int(height_m / h) + 1 |
|
|
| |
| 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) |
|
|
| |
| num_triangles_approx = int((width_m * height_m) / (0.5 * side_m * h)) |
|
|
| |
| 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})" |
|
|
|
|
| |
| 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) |
|
|
| |
| 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)) |
|
|
| |
| html = f""" |
| <div class="hb-container"> |
| <div class="hb-header"> |
| <div class="hb-title">Your Hoverboard Design</div> |
| <div class="hb-subtitle">{width_ft}' x {height_ft}' surface — {grid['nodes']:,} pressure nodes</div> |
| </div> |
| |
| <div class="hb-grid-viz"> |
| <div class="hb-stat-row"> |
| <div class="hb-stat"> |
| <div class="hb-stat-num">{grid['vertices']:,}</div> |
| <div class="hb-stat-label">Transducers</div> |
| </div> |
| <div class="hb-stat"> |
| <div class="hb-stat-num">{grid['triangles']:,}</div> |
| <div class="hb-stat-label">Triangles</div> |
| </div> |
| <div class="hb-stat"> |
| <div class="hb-stat-num">{grid['nodes']:,}</div> |
| <div class="hb-stat-label">Pressure Nodes</div> |
| </div> |
| <div class="hb-stat"> |
| <div class="hb-stat-num">{grid['wavelength_mm']}mm</div> |
| <div class="hb-stat-label">Wavelength</div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="hb-section"> |
| <div class="hb-section-title">Weight Capacity</div> |
| <div class="hb-weight-grid"> |
| {''.join(f""" |
| <div class="hb-weight-item {'hb-feasible' if s['feasible'] else 'hb-infeasible'}"> |
| <div class="hb-weight-icon">{'✅' if s['feasible'] else '❌'}</div> |
| <div class="hb-weight-name">{s['object']}</div> |
| <div class="hb-weight-detail">{s['weight_kg']}kg — {s['force_per_node_g']}g/node</div> |
| </div> |
| """ for s in scenarios)} |
| </div> |
| <div class="hb-note">Published research: ~3g force per 40kHz node. Friction cancellation requires less than full levitation.</div> |
| </div> |
| |
| <div class="hb-section"> |
| <div class="hb-section-title">Bill of Materials — Order Parts</div> |
| <table class="hb-bom"> |
| <tr><th>Component</th><th>Qty</th><th>Unit $</th><th>Total</th><th>Buy</th></tr> |
| {''.join(f'<tr><td>{i["item"]}<div class="hb-part-note">{i.get("note","")}</div></td><td>{i["qty"]:,}</td><td>${i["unit"]:.2f}</td><td>${i["total"]:.2f}</td><td><a href="{i.get("link","#")}" target="_blank" class="hb-buy-link">Amazon</a></td></tr>' for i in bom)} |
| <tr class="hb-bom-total"><td colspan="3">TOTAL</td><td>${bom_total:,.2f}</td><td></td></tr> |
| </table> |
| </div> |
| |
| <div class="hb-section"> |
| <div class="hb-section-title">Build Steps</div> |
| <div class="hb-build-steps"> |
| <div class="hb-step"> |
| <div class="hb-step-num">1</div> |
| <div class="hb-step-text"> |
| <strong>Cut the base.</strong> Cut 3/4" plywood to {width_ft}' x {height_ft}'. |
| This is your table surface. Sand it flat. |
| </div> |
| </div> |
| <div class="hb-step"> |
| <div class="hb-step-num">2</div> |
| <div class="hb-step-text"> |
| <strong>Mark the triangle grid.</strong> 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. |
| </div> |
| </div> |
| <div class="hb-step"> |
| <div class="hb-step-num">3</div> |
| <div class="hb-step-text"> |
| <strong>Mount transducers.</strong> Press-fit or hot-glue one 40kHz transducer into each hole, |
| facing UP. All transducers should be flush with the surface. |
| </div> |
| </div> |
| <div class="hb-step"> |
| <div class="hb-step-num">4</div> |
| <div class="hb-step-text"> |
| <strong>Wire PCB tiles.</strong> 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. |
| </div> |
| </div> |
| <div class="hb-step"> |
| <div class="hb-step-num">5</div> |
| <div class="hb-step-text"> |
| <strong>Connect controllers.</strong> 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). |
| </div> |
| </div> |
| <div class="hb-step"> |
| <div class="hb-step-num">6</div> |
| <div class="hb-step-text"> |
| <strong>Power it up.</strong> Connect {n_psu} 48V PSU(s) to the driver boards. |
| Add a master power switch and a fuse (10A per PSU). |
| <em>Warning: 48V can hurt you. Respect the voltage.</em> |
| </div> |
| </div> |
| <div class="hb-step"> |
| <div class="hb-step-num">7</div> |
| <div class="hb-step-text"> |
| <strong>Top it off.</strong> 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. |
| </div> |
| </div> |
| <div class="hb-step"> |
| <div class="hb-step-num">8</div> |
| <div class="hb-step-text"> |
| <strong>Test it.</strong> 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. |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="hb-section hb-ai"> |
| <div class="hb-section-title">AI Assessment</div> |
| <div class="hb-insight">{insight}</div> |
| </div> |
| |
| <div class="hb-footer"> |
| "It's not a hoverboard. It's an acoustic air hockey table. And the grid is triangles, not holes."<br> |
| <span class="hb-dedication">If this ever makes money, the first check goes to the Michael J. Fox Foundation.</span> |
| </div> |
| </div> |
| """ |
|
|
| return html |
|
|
|
|
| |
| 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; } |
| """ |
|
|
| |
| with gr.Blocks(css=CUSTOM_CSS, title="Hoverboard Calculator", theme=gr.themes.Base()) as app: |
|
|
| gr.HTML(""" |
| <div style="text-align:center; padding: 20px 0;"> |
| <h1 style="color: #E40000; font-family: 'Press Start 2P', cursive; font-size: 1.6em; letter-spacing: 0.05em; text-shadow: 3px 3px 0px #222;"> |
| HOVERBOARD CALCULATOR |
| </h1> |
| <p style="color: #0050DD; font-family: 'VT323', monospace; font-size: 1.4em; margin-top: 8px;"> |
| Because Michael J. Fox Lied To Me (But It's Cool) |
| </p> |
| <p style="color: #666; font-family: 'VT323', monospace; font-size: 1.1em; margin-top: 4px;"> |
| Not levitation. Friction cancellation. The distinction matters. |
| </p> |
| </div> |
| """) |
|
|
| 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(""" |
| <div style="text-align:center; padding: 16px 0; color: #888; font-size: 1em; font-family: 'VT323', monospace;"> |
| Inspired by Real Genius (1985), Weird Science (1985), and one guy who drew helicopters 400 years too early.<br> |
| <span style="color: #E40000; font-family: 'Press Start 2P', cursive; font-size: 0.6em;">Heuremen -- Build Small Hackathon 2026</span> |
| </div> |
| """) |
|
|
| if __name__ == "__main__": |
| app.launch() |
|
|