// CieloTrack receiver enclosure — a stacked pair of Seeed XIAO ESP32-S3 boards, // outdoors, with the antenna on an SMA bulkhead. // // EVERY DIMENSION BELOW IS A DEFAULT, NOT A MEASUREMENT. // The one that matters most is stack_h: the soldered header pins and the JST housing // dominate the internal height, and nobody has measured this build. Measure the real // stack with calipers and set it before printing anything. // // openscad -D part=\"base\" -o base.stl cielotrack-case.scad // openscad -D part=\"lid\" -o lid.stl cielotrack-case.scad // // Print in PETG or ASA. Not PLA: an enclosed box in Texas sun passes PLA's softening // point and sags. Never carbon-filled filament — it is conductive and will detune or // shield the antenna, and the board will look healthy while hearing less. /* [What to measure] */ board_l = 21.0; // XIAO ESP32-S3, published. Verify. board_w = 17.5; // ditto stack_h = 16.0; // MEASURE THIS: two boards, header pins, JST housing pigtail_slack= 18.0; // room for the u.FL lead to loop without a tight bend /* [Fit] */ clearance = 1.2; // around the stack, per side wall = 2.4; // 3 perimeters at 0.4mm nozzle; weatherproof rather than minimal floor_t = 2.4; lid_t = 2.4; lip_h = 3.0; // lid skirt depth lip_gap = 0.25; // printed fit; loosen to 0.35 if your printer runs tight /* [Openings] */ sma_d = 6.6; // SMA bulkhead nut, typical 6.5mm — check yours // The bulkhead sits on an angled boss rather than square to the wall. Signal from this // site peaks around 30-40 degrees of elevation and falls off both sides, so an antenna // forced vertical is pointed above where the aircraft actually are. Most rubber-duck // antennas hinge at the base, so this only has to get the mount into the right range and // let the hinge do the rest. // // Read as "do not force vertical", not as a measured optimum: 330 rows over six bins, // and RSSI mixes distance with angle, since a closer aircraft is both louder and higher. sma_tilt = 35; // degrees up from horizontal boss_d = 13.0; // flat pad for the bulkhead nut to seat against boss_h = 4.5; // One opening, not two. The boards are stacked, so their USB-C connectors sit one above // the other at the same end — separated in Z by the stack pitch, not side by side. A // single slot spanning both cannot misalign with either, and two 10mm ports side by side // did not fit the 28mm end face anyway. usb_w = 11.0; // USB-C plug body plus a cable boot usb_slot_h = 13.0; // MEASURE: centre of lower connector to centre of upper, plus ~7 vent_d = 3.0; drain_d = 4.0; /* [Mounting] */ ear_t = 3.0; ear_r = 7.0; screw_d = 4.4; // M4 clearance /* [Render] */ part = "both"; // "base" | "lid" | "both" $fn = 64; // ---- derived ------------------------------------------------------------------ inner_l = board_l + 2*clearance; inner_w = board_w + 2*clearance + pigtail_slack; inner_h = stack_h + 2; // 2mm headroom over the stack outer_l = inner_l + 2*wall; outer_w = inner_w + 2*wall; outer_h = inner_h + floor_t; 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); } module base() { difference() { union() { rrect(outer_l, outer_w, outer_h); // mounting ears, outside the sealed volume so no screw enters the box for (s=[-1,1]) translate([s*(outer_l/2 + ear_r - 1.5), 0, 0]) cylinder(r=ear_r, h=ear_t); // Angled boss: gives the bulkhead nut a flat face to seat against at the // tilt, instead of biting into a curved or sloping wall. translate([0, outer_w/2 - 1, floor_t + inner_h/2]) rotate([90 - sma_tilt, 0, 0]) cylinder(d=boss_d, h=boss_h*2, center=true); } // cavity translate([0,0,floor_t]) rrect(inner_l, inner_w, inner_h + 1, r=2.0); // SMA bore through the angled boss. Tilted up, never straight up: a hole in a // horizontal face is where water gets in, and it is also the orientation the // signal data argues against. translate([0, outer_w/2 - 1, floor_t + inner_h/2]) rotate([90 - sma_tilt, 0, 0]) cylinder(d=sma_d, h=40, center=true); // One tall slot reaching both connectors. They are flashed one at a time and, // with UUSB tied between the boards, must never both be plugged in at once — so // a single opening is a feature, not a compromise. translate([0, -outer_w/2, floor_t + usb_slot_h/2 + 1.5]) cube([usb_w, wall*3, usb_slot_h], center=true); // vents low on both long sides; nothing open facing up for (s=[-1,1]) for (i=[-1,0,1]) translate([s*outer_l/2, i*7, floor_t + 3]) rotate([0,90,0]) cylinder(d=vent_d, h=wall*3, center=true); // drain at the lowest point translate([0,0,-1]) cylinder(d=drain_d, h=floor_t+2); // mounting holes for (s=[-1,1]) translate([s*(outer_l/2 + ear_r - 1.5), 0, -1]) cylinder(d=screw_d, h=ear_t+2); } } module lid() { difference() { union() { rrect(outer_l, outer_w, lid_t); // skirt that drops into the cavity translate([0,0,-lip_h]) rrect(inner_l - lip_gap*2, inner_w - lip_gap*2, lip_h, r=2.0); } // hollow the skirt so it is a lip, not a plug 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, outer_w + 6, 0]) lid(); }