// CieloTrack receiver enclosure — CARRIER-BOARD, INDOOR SILL-STAND variant. // // For the unit that SITS on a window sill rather than being mounted. It is the // external-dipole box reclined onto an integrated wedge foot: the two antennas stand // near-vertical (~62 deg elevation), which is what a dipole wants for omnidirectional // coverage of the low-to-mid elevations where drones actually are; the USB-C ports face // down-and-forward so a power/flash cable drops straight to the sill; and a wide flat // foot keeps a top-heavy pair of antennas from tipping it. // // It DROPS the outdoor hardware — no mounting ears, no drain, no power gland — because a // sill unit is neither mounted nor rained on. Power comes in through the XIAO USB-C // (accessible ports, so you can also re-flash without opening it). The wall-mountable, // weatherproof unit is cielotrack-carrier-case-wallmount.scad; use that one outdoors. // // openscad -D 'part="base"' -o base.stl cielotrack-carrier-case-indoor.scad // openscad -D 'part="lid"' -o lid.stl cielotrack-carrier-case-indoor.scad // // Board fit VERIFIED against the Flux 3D preview on 2026-09-03: 60 x 30 mm carrier, four M3 // holes at (+/-26.5, +/-11.5) mm, USB-C at |X| = 13 mm. The XIAOs are SOCKETED on headers // (not soldered flat as the render shows), so the USB-C sits ~header_h above the carrier — // the USB slots are placed at that height, not at board level. Set header_h to your stack. // Print PETG or PLA — indoors there is no sun load — but never carbon-filled filament: it // is conductive and detunes the antennas. // // REMEMBER: only ever energise ONE XIAO USB-C at a time. The 5 V rails are linked, so the // powered module feeds the other; two cables at once back-feeds a rail. /* [Board — verified against the Flux 3D preview, 2026-09-03] */ board_l = 60.0; // X - CONFIRMED 60 mm from the Flux board outline board_w = 30.0; // Y - CONFIRMED 30 mm from the Flux board outline board_t = 1.6; // PCB thickness (Standard 2-layer) // The two XIAOs are SOCKETED on pin/female headers here (not soldered flat as the Flux // render shows), so the whole module rides up on the headers. header_h drives both the box // height AND where the USB-C ports land on the wall, so measure it and set it. header_h = 8.5; // XIAO standoff on its headers, top of carrier -> bottom of XIAO. MEASURE. xiao_t = 3.5; // XIAO module thickness incl. RF shield comp_h = header_h + xiao_t; // tallest thing above the carrier (~12 mm socketed) /* [Board mounting — MATCH the four M3 holes in Flux] */ hole_inset_x = 3.5; hole_inset_y = 3.5; boss_h = 4.0; // standoff under the board boss_d = 6.0; boss_pilot = 2.6; // pilot for an M3 self-tapper (open to 4.0 for a heat-set insert) /* [Fit] */ clearance = 1.0; wall = 2.4; floor_t = 2.4; lid_t = 2.4; lip_h = 3.0; // lid skirt depth (the locking lip) lip_gap = 0.25; // printed fit; loosen to 0.35 if your printer runs tight /* [Which edge faces out] */ // Per the Flux board, USB-C and u.FL sit on OPPOSITE long edges of each module. Orient the // carrier so the two USB-C face the FRONT (-Y, the wall with the slots); the u.FL then land // on the BACK (+Y) edge next to the SMA bulkheads, giving the shortest pigtail run. Both // USB-C are at |X| = 13 mm (measured off the Flux 3D). front = -1 is the -Y wall. front = -1; /* [Openings] */ // Accessible USB-C ports, one per module, in the front wall. On the sill unit these are // the power path (and let you re-flash without opening the lid), so they are ON by default. usb_slots = true; usb_offset = 13.0; // |X| of each module's USB port from board centre — CONFIRMED ±13 (Flux 3D) usb_w = 11.0; // USB-C plug body plus a cable boot usb_slot_h = 6.0; // just tall enough for a USB-C boot; kept 1 mm clear of the lid seam usb_c_rise = 1.5; // USB-C centre above the XIAO's own PCB vent_d = 3.0; // a few small vents, high on the short walls (exposed after recline) /* [External dipole antennas] */ // Two SMA bulkheads on the BACK (+Y) wall, bores straight out the wall (sma_tilt=0). The // pod's recline then sets the antenna elevation: at sma_tilt=0 the world elevation equals // `rake`. Raise sma_tilt to aim them lower/further out, lower it (or reduce rake) toward // horizontal. Spaced apart so the two 2.4 GHz radios do not couple. sma_d = 6.6; // bulkhead nut bore (typical 6.5 mm) - check yours sma_tilt = 0; // bore straight out the back wall; the recline below sets the elevation sma_boss_d = 13.0; // flat seat for the nut sma_boss_h = 4.5; sma_sep = 40.0; // centre-to-centre spacing of the two antennas /* [Sill stand] */ rake = 62; // recline of the whole pod, deg. At sma_tilt=0 this is the antenna elevation (~62 deg, near-vertical). foot_t = 3.0; // thickness of the flat foot pad on the sill foot_pad_l = 6.0; // how far the foot is wider than the pod, each side in X foot_pad_y = 15.0;// how far the foot reaches past the pod footprint, front and back in Y /* [Render] */ part = "both"; // "base" | "lid" | "both" $fn = 48; // ---- derived ------------------------------------------------------------------ inner_l = board_l + 2*clearance; inner_w = board_w + 2*clearance; inner_h = boss_h + board_t + comp_h + 2; // 2 mm headroom under the lid outer_l = inner_l + 2*wall; outer_w = inner_w + 2*wall; outer_h = inner_h + floor_t; hx = board_l/2 - hole_inset_x; hy = board_w/2 - hole_inset_y; holes = [[hx,hy],[hx,-hy],[-hx,hy],[-hx,-hy]]; drop = outer_w/2 * sin(rake); // lift so the reclined pod's lowest edge lands on z=0 foot_l = outer_l + 2*foot_pad_l; foot_w = outer_w*cos(rake) + 2*foot_pad_y; module rrect(l,w,h,r=2.5){ hull() for(x=[r-l/2,l/2-r], y=[r-w/2,w/2-r]) translate([x,y,0]) cylinder(r=r,h=h); } // The pod is built flat, then this places it reclined on the foot. module reclined(){ translate([0,0,drop]) rotate([rake,0,0]) children(); } // Solid outer shell + the antenna bosses (no holes yet). module pod_solid(){ rrect(outer_l, outer_w, outer_h); for(sx=[-1,1]) translate([sx*sma_sep/2, outer_w/2 - 1, floor_t + inner_h/2]) rotate([90 - sma_tilt, 0, 0]) cylinder(d=sma_boss_d, h=sma_boss_h*2, center=true); } // Everything removed from the shell. module pod_cuts(){ // cavity translate([0,0,floor_t]) rrect(inner_l, inner_w, inner_h + 1, r=2.0); // accessible USB-C ports in the front wall // Socketed build: the USB-C rides UP with the XIAO on its headers, so the slot centres // on (carrier top + header_h + usb_c_rise), NOT at carrier level. This scales with // header_h and always stays ~1 mm below the lid seam. if(usb_slots) for(sx=[-1,1]) translate([sx*usb_offset, front*outer_w/2, floor_t + boss_h + board_t + header_h + usb_c_rise]) cube([usb_w, wall*3, usb_slot_h], center=true); // vents, HIGH on both short walls (exposed and facing outward once reclined) for(s=[-1,1]) for(i=[-1,0,1]) translate([s*outer_l/2, i*8, floor_t + inner_h - 4]) rotate([0,90,0]) cylinder(d=vent_d, h=wall*3, center=true); // SMA bores through the angled bosses for(sx=[-1,1]) translate([sx*sma_sep/2, outer_w/2 - 1, floor_t + inner_h/2]) rotate([90 - sma_tilt, 0, 0]) cylinder(d=sma_d, h=40, center=true); } module board_bosses(){ // Start 1 mm below the floor top so each boss fuses into the floor (one manifold solid), // not merely touches it on a coincident plane. for(h=holes) translate([h[0], h[1], floor_t - 1]) difference(){ cylinder(d=boss_d, h=boss_h + 1); translate([0,0,-1]) cylinder(d=boss_pilot, h=boss_h + 3); } } // Wedge foot: hull from a flat pad on the sill up to the reclined pod's underside. module wedge(){ hull(){ // Sink the slab 1 mm into the pod so the wedge fuses with the shell, not just abuts it. reclined() translate([0,0,-1]) rrect(outer_l, outer_w, 2.0); rrect(foot_l, foot_w, foot_t); // flat pad on the sill } } module base(){ difference(){ union(){ reclined() pod_solid(); wedge(); } // Cut the openings through the whole assembly, so the wedge never seals a port. reclined() pod_cuts(); } // board-support bosses, added after the difference so their pilots survive reclined() board_bosses(); } module lid(){ difference(){ union(){ rrect(outer_l, outer_w, lid_t); translate([0,0,-lip_h]) rrect(inner_l - lip_gap*2, inner_w - lip_gap*2, lip_h, r=2.0); } translate([0,0,-lip_h-0.5]) rrect(inner_l - lip_gap*2 - 2*1.6, inner_w - lip_gap*2 - 2*1.6, lip_h+1, r=1.2); } } if (part == "base") base(); else if (part == "lid") lid(); else { base(); translate([0, foot_w/2 + outer_l/2 + 6, 0]) lid(); }