← Commit history

Rev 2 hero 3D: the UV laser engraving on the case, built from the engraving SVG

John Lauer ·908d53779d ·1d ago ·parent 72b8ecd
7 files changed +83−3
README.md+2−2
@@ -4,7 +4,7 @@ <!-- rev2-status:begin --> ## Rev 2 build status -Rev 2 is being built right now by three AI threads in Adom Hydrogen; this section follows their work. Last updated Nov 8, 2:37 PM Central.+Rev 2 is being built right now by three AI threads in Adom Hydrogen; this section follows their work. Last updated Nov 8, 2:57 PM Central.  | Stage | | What it covers | |---|---|---|@@ -438,7 +438,7 @@ Design notes, print settings and assembly: [case/README.md](case/README.md); eng  ## Firmware -Plain C for the ATtiny1616, updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge) (avr-gcc, about 13.1 KB of the 16 KB flash). The echo path is pure hardware:+Plain C for the ATtiny1616, updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge) (avr-gcc, about 13.1 KB of the 16 KB flash). The echo path is pure hardware: AC0, the event system and TCB0/TCB1 single-shots. The CPU runs the state machine, the learner, the lights and the safety checks, and a 1 s watchdog guards the main loop. Full source is in [firmware/](firmware/README.md), with a host test that runs the learner against simulated faucets
docs/3d/ghost-hand-hero-936f203c.glbadded
⋯ 1 unchanged line ⋯
docs/3d/size-hand/README.md+4
@@ -5,3 +5,7 @@ An average adult right hand (180 mm wrist to middle fingertip), palm down and fl Cut from the MakeHuman base mesh (`makehuman/data/3dobjs/base.obj`) using its default skeleton weights (`makehuman/data/rigs/default_weights.mhw`), both released as CC0 by Data Collection AB (https://github.com/makehumancommunity/makehuman). Steps: `extract.py` (hand plus distal forearm by skin weights), `pose.py` (palm down, fingers +Y, scaled to 180 mm), `level.py` (forearm trimmed 55 mm behind the wrist, levelled), `cap.py` (wrist cut capped, watertight).  Frame: metres, Z up, Z = 0 on the counter, wrist stub at Y = 0, fingers toward +Y, centred on X. 146 x 227 x 54 mm, 234 KB.++## Sink scene (`sink_scene.glb`, `sink_scene.py`)++The hand washing under a Moen 8551 (dimensions from Moen's spec sheet rev 3/17: column 53 x 57 x 166 mm, outlet 105 mm out and 142 mm up, sensor window 40 mm up), a 17 x 14 in basin in a 900 x 600 mm counter, and the water stream. Frame: metres, Z up, counter top Z = 0, origin = Ghost Hand case centre, +Y = where its IR window points; the faucet window lands at (0, 0.330, 0.040): 330 mm away, 49 deg off the faucet's axis, inside the aim study's working zone. Nodes: sink_counter, sink_basin, sink_faucet, sink_faucet_window, sink_water, size_reference_hand. 451 KB. Preview: sink_views.png (the green box stands in for Ghost Hand).
docs/3d/size-hand/sink_scene.glbadded
⋯ 1 unchanged line ⋯
docs/3d/size-hand/sink_scene.pyadded+76
@@ -0,0 +1,76 @@+"""Bathroom sink scene for the Ghost Hand hero: Moen 8551 (spec sheet rev 3/17 dimensions), a 17 x 14 in basin+in a counter, the water stream, and the size-reference hand washing under it. Output frame: metres, Z up,+counter top Z = 0, origin = Ghost Hand case centre, +Y = the way its IR window points (at the faucet window)."""+import numpy as np, trimesh, math+from shapely.geometry import Polygon, Point+from shapely import affinity+mm = 1e-3+def mat(name, rgba, metal=0.0, rough=0.6):+    return trimesh.visual.TextureVisuals(material=trimesh.visual.material.PBRMaterial(+        name=name, baseColorFactor=rgba, metallicFactor=metal, roughnessFactor=rough,+        alphaMode='BLEND' if rgba[3] < 255 else 'OPAQUE', doubleSided=True))+# ---- faucet frame F: x across, y forward over the basin, z up, window (column front face) at y = 0+W, D, H = 53, 57, 166           # column wide, deep, tall+REACH_FRONT, OUT_Z = 105, 142   # column front to outlet centre; outlet underside height+WIN_Z = 40                      # sensor window centre, pre-2017 unit (John's)+BA, BB, LIP = 432 / 2, 356 / 2, 25+BC = LIP + BB                   # basin centre y+parts = {}+col = trimesh.creation.extrude_polygon(Polygon([(-W/2, -D), (W/2, -D), (W/2, 0), (-W/2, 0)]).buffer(-8).buffer(8), H - 22)+parts['faucet'] = [col]+spout = trimesh.creation.extrude_polygon(Polygon([(-W/2, -D), (W/2, -D), (W/2, REACH_FRONT + 14), (-W/2, REACH_FRONT + 14)]).buffer(-6).buffer(6), 22)+spout.apply_translation([0, 0, H - 22]); parts['faucet'].append(spout)+win = trimesh.creation.extrude_polygon(Point(0, 0).buffer(1).simplify(0.01), 1.0)+win.apply_scale([7, 11, 1]); win.apply_transform(trimesh.transformations.rotation_matrix(-math.pi/2, [1, 0, 0]))+win.apply_translation([0, 0.6, WIN_Z]); parts['faucet_window'] = [win]+aer = trimesh.creation.cylinder(radius=10, height=3, sections=32); aer.apply_translation([0, REACH_FRONT, OUT_Z - 1.5])+parts['faucet'].append(aer)+# counter with the basin opening, 900 x 600, 30 thick+ctr = Polygon([(-450, -D - 60), (450, -D - 60), (450, 600 - D - 60), (-450, 600 - D - 60)])+hole = affinity.scale(Point(0, BC).buffer(1, 128), BA, BB)+counter = trimesh.creation.extrude_polygon(ctr.difference(hole), 30); counter.apply_translation([0, 0, -30])+parts['counter'] = [counter]+# basin: half ellipsoid bowl, 150 deep+bowl = trimesh.creation.uv_sphere(radius=1, count=[48, 96])+bowl.update_faces(bowl.triangles_center[:, 2] < 0.0); bowl.remove_unreferenced_vertices()+bowl.apply_scale([BA - 2, BB - 2, 150]); bowl.apply_translation([0, BC, -2])+parts['basin'] = [bowl]+drain = trimesh.creation.cylinder(radius=18, height=1, sections=32); drain.apply_translation([0, BC + 40, -150])+parts['basin'].append(drain)+# hand washing under the stream: palm down, fingers toward the faucet, tilted down 25 deg, the stream on the back of the hand+hand = trimesh.load('hand_ref.glb', force='mesh')          # wrist y = 0, fingers +Y, Z = 0 at its lowest point+Lh = hand.bounds[1][1]+hand.apply_translation([0, -Lh, 0])                         # fingertips at y = 0+hand.apply_transform(trimesh.transformations.rotation_matrix(math.pi, [0, 0, 1]))          # fingers toward -y (the faucet)+hand.apply_transform(trimesh.transformations.rotation_matrix(math.radians(25), [1, 0, 0]))  # fingertips down+hand.apply_scale(1000)                                      # hand_ref is in metres+hand.apply_translation([-10, REACH_FRONT - 55, 10])         # knuckles under the stream, fingertips over the bowl+parts['hand'] = [hand]+# water: from the outlet onto the back of the hand, then off the fingertips into the bowl+near = np.hypot(hand.vertices[:, 0], hand.vertices[:, 1] - REACH_FRONT) < 4+top = hand.vertices[near, 2].max() if near.any() else 0.0+w1 = trimesh.creation.cylinder(radius=3.5, height=OUT_Z - 3 - top, sections=24); w1.apply_translation([0, REACH_FRONT, (OUT_Z - 3 + top) / 2])+tips = hand.vertices[np.argmin(hand.vertices[:, 1])]          # fingertip nearest the faucet+bz = -150 * math.sqrt(max(0.0, 1 - ((tips[1] - BC) / BB) ** 2 - (tips[0] / BA) ** 2))+w2 = trimesh.creation.cylinder(radius=2.5, height=tips[2] - bz, sections=16); w2.apply_translation([tips[0], tips[1], (tips[2] + bz) / 2])+parts['water'] = [w1, w2]+print('stream lands on the hand at z', round(top, 1), 'mm; runs off at', tips.round(1))+# ---- place: Ghost Hand on the counter beside the basin, 330 mm from the window, 49 deg off the faucet axis+GX, GY = 250, 215+dist = math.hypot(GX, GY); yaw = math.atan2(-GX, -GY)       # direction from device to window in F+# transform F -> device frame: translate device to origin, rotate so the device->window direction is +Y+T = trimesh.transformations.translation_matrix([-GX, -GY, 0])+R = trimesh.transformations.rotation_matrix(-(math.atan2(0 - GY, 0 - GX) - math.pi/2), [0, 0, 1])+M = trimesh.transformations.scale_matrix(mm) @ R @ T+looks = {'faucet': ([214, 218, 222, 255], 1.0, 0.12), 'faucet_window': ([18, 18, 22, 255], 0.0, 0.05),+         'counter': ([226, 214, 190, 255], 0.0, 0.7), 'basin': ([246, 246, 244, 255], 0.0, 0.25),+         'water': ([200, 225, 245, 120], 0.0, 0.05), 'hand': ([224, 172, 140, 255], 0.0, 0.65)}+sc = trimesh.Scene()+for k, ms in parts.items():+    m = trimesh.util.concatenate(ms); m.apply_transform(M)+    if k == 'basin': m.update_faces(np.arange(len(m.faces)))+    rgba, metal, rough = looks[k]; m.visual = mat(k, rgba, metal, rough)+    sc.add_geometry(m, node_name='sink_' + k if k != 'hand' else 'size_reference_hand', geom_name=k)+w = trimesh.transformations.transform_points([[0, 0, WIN_Z]], M)[0]+print('window in device frame (mm):', (w * 1000).round(1), ' distance', round(dist), 'mm, off axis', round(math.degrees(math.atan2(GX, GY))), 'deg')+sc.export('sink_scene.glb'); import os; print(os.path.getsize('sink_scene.glb') // 1024, 'KB')
docs/3d/size-hand/sink_views.pngadded
⋯ 1 unchanged line ⋯
page.json+1−1
@@ -43,7 +43,7 @@     "subcategory": "",     "body_size": null,     "parts": {-      "model_3d": "docs/3d/ghost-hand-hero-47282fc0.glb"+      "model_3d": "docs/3d/ghost-hand-hero-936f203c.glb"     },     "distributor_links": {},     "viewer_3d": {