Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
780 changes: 780 additions & 0 deletions design/mechanical/MECHANICAL_DESIGN.md

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions design/mechanical/cad/SECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Mechanical CAD (Phase D) — parametric geometry

The repository shipped **zero usable CAD**: every `.step` reference in the build package is marked *Pending*. Phase D replaces that void with **text-based, parametric geometry driven entirely by the computed design** in `design/mechanical/calc/params.py`. Nothing is hand-dimensioned that already exists as a parameter — change a number in `params.py` and the models (and the preview) move with it.

### The immediate visual

![NIGHTWATCH GEM meridian elevation](cad/preview_assembly.svg)

`cad/preview_assembly.svg` is a to-scale meridian-plane side elevation of the mount in its park pose — pier, RA housing on the polar axis inclined at the **38.9° site latitude**, DEC head, MN78 OTA pointing at the pole, and the counterweight shaft hanging on the down side. It is produced by a **pure-stdlib** Python script (no OpenSCAD, no third-party libraries) that imports `params.py` and writes the SVG with the real numbers annotated. This is the deliverable you can look at right now; the OpenSCAD files below are the manufacturable source.

### The parts

| File | Part | Headline dimensions (all traced to `params.py`) |
|---|---|---|
| `cad/ra_housing.scad` | RA (polar) axis housing | 203.2 × 203.2 × 76.2 mm block (`RA_HOUSING` 8″×8″×3″), 8 mm wall; stepped axial bore — 6008 seat (Ø68/Ø40, `BRG_RA_6008`) opening to the CSF‑32 Ø80 hollow‑drive register (`RA_DRIVE.bore_m`); flange bolt circle ≈ Ø104 |
| `cad/dec_housing.scad` | DEC axis housing + **Losmandy‑D saddle** | 152.4 × 152.4 × 63.5 mm (`DEC_HOUSING` 6″×6″×2.5″); 6006 seat (Ø55/Ø30, `BRG_DEC_6006`) → CSF‑25 Ø64 register (`DEC_DRIVE.bore_m`); dovetail saddle 76.2 mm / 15° with side clamp |
| `cad/pier_adapter.scad` | Pier adapter plate | 254 × 254 × 19.05 mm (10″×10″×0.75″) bridging the SOURCED 304.8 × 9.525 mm (12″×0.375″) `PIER` top plate to the RA housing corner pattern |
| `cad/counterweight_shaft.scad` | Counterweight shaft + weights | Ø31.75 mm × 457.2 mm 303‑SS (`COUNTERWEIGHTS` 1.25″×18″), stud + safety knob, 5+5+2.5 kg discs (12.5 kg available) |
| `cad/assembly.scad` | Full GEM assembly | Composes all four parts + the MN78 OTA tube (Ø210 × 1400 mm) on the pier, posed at latitude `SITE.latitude_deg` |
| `cad/svg_preview.py` | Zero‑dep previewer | Imports `params`/`units`, emits `preview_assembly.svg` |

### Parametric approach

Each `.scad` opens with a **params block** whose every entry carries a `// = P.<...>` comment tracing it to `params.py` (SI metres in the calculator → millimetres in OpenSCAD, the CAD convention). Parts are pure `module`s; `assembly.scad` pulls them in with `use <...>` and places them with explicit transforms (`rotate([0, 90-lat, 0])` puts local *+z* on the celestial pole; a nested `rotate([90,0,0])` sets the DEC axis perpendicular to RA). The SVG previewer is the strictest link in the chain — it reads `params.py` **at runtime**, so it can never disagree with the proofs, and a test pins the committed SVG byte‑for‑byte to the renderer output.

### How to render

OpenSCAD is **not installed** in the design sandbox, so each part header documents the command to run locally:

```
openscad -o ra_housing.stl design/mechanical/cad/ra_housing.scad
openscad -o assembly.stl design/mechanical/cad/assembly.scad
openscad -o assembly.png --imgsize=1600,1200 design/mechanical/cad/assembly.scad
```

The SVG needs nothing but Python: `python3 design/mechanical/cad/svg_preview.py` regenerates `preview_assembly.svg` (re‑run it whenever `params.py` changes).

### Honest assumptions (tagged ASSUMED/DERIVED in‑file and in `svg_preview`)

- **Pier adapter (10×10×0.75″)** is an *added* part — the repo's `PIER` only specifies a 12×12×0.375″ top plate, so the adapter geometry is **ASSUMED**.
- **Losmandy‑D dovetail** 76.2 mm / 15° is the **ASSUMED** industry standard; the repo never dimensions the saddle.
- **Bearing widths** (6008 = 15 mm, 6006 = 13 mm) and **counterweight disc OD/thickness** are **ASSUMED** — `params` gives bore/OD and weight mass only.
- **CSF flange bolt circle** is taken as **DERIVED** ≈ 1.3 × drive bore (`RA_DRIVE.bore_m` / `DEC_DRIVE.bore_m`), since datasheet PCDs are not in the repo.

### Verification

`design/mechanical/tests/test_cad.py` — **13 tests, all green** (and the full mechanical suite stays at 118 passed): the polar‑axis vector is a unit vector at the site latitude with the perpendiculars orthogonal to it; drawn dimensions equal `params.py` exactly; swapping MN76↔MN78 changes the geometry (proof it is parametric); the head sits above the pier top, the OTA clears the DEC head toward the pole, and the counterweight hangs on the opposite side of the inclined axis; the SVG is valid, dimensioned with the real numbers, and actually writes; and every `.scad` carries its render command and `params.py` traceability.
83 changes: 83 additions & 0 deletions design/mechanical/cad/assembly.scad
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// ============================================================================
// NIGHTWATCH GEM — full mount assembly
// Composes the parametric parts into a German Equatorial Mount on its pier,
// posed in the classic "counterweight-down / OTA-to-pole" park.
// Parametric OpenSCAD model. Units: MILLIMETRES.
//
// Render (OpenSCAD not installed here):
// openscad -o assembly.stl assembly.scad
// openscad -o assembly.png --imgsize=1600,1200 assembly.scad
//
// The immediate, dependency-free visual is design/mechanical/cad/preview_assembly.svg
// (generated by svg_preview.py, which reads the SAME params.py numbers).
//
// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm).
// ============================================================================

use <ra_housing.scad>
use <dec_housing.scad>
use <pier_adapter.scad>
use <counterweight_shaft.scad>

$fn = 64;

// ---- parameters (traced to params.py) --------------------------------------
lat = 38.9; // P.SITE.latitude_deg -> polar-axis altitude
pier_h = 914.4; // P.PIER.height_above_m = u.inch(36)
pier_d = 304.8; // P.PIER.diameter_m = u.inch(12)
tp_side = 304.8; // P.PIER.top_plate_side_m = u.inch(12)
tp_thk = 9.525; // P.PIER.top_plate_thk_m = u.inch(0.375)
adapter_thk = 19.05; // pier_adapter.scad (0.75 in), ASSUMED added part

ra_depth = 76.2; // P.RA_HOUSING.depth_m = u.inch(3)
dec_face = 152.4; // P.DEC_HOUSING.outer_x_m = u.inch(6)
dec_depth = 63.5; // P.DEC_HOUSING.depth_m = u.inch(2.5)

// Default OTA = MN78 (the params default across the torque/balance/pier proofs).
ota_od = 210.0; // P.MN78.tube_od_m = u.mm(210)
ota_len = 1400.0; // P.MN78.tube_length_m = u.mm(1400)
saddle_stack = 50.0; // stiffness.SADDLE_LEVER_M (assumed saddle offset)

// Derived placements
adapter_top = pier_h + tp_thk + adapter_thk;
L_ra = ra_depth + dec_face/2 + 40; // DEC-head seat along the polar axis (DERIVED)
ota_offset = dec_face/2 + saddle_stack + ota_od/2; // saddle-side radial offset

// ---- assembly --------------------------------------------------------------
module gem_assembly() {
// Concrete pier (exposed height only)
color([0.62, 0.62, 0.60]) cylinder(d = pier_d, h = pier_h);

// Steel top plate (SOURCED, P.PIER)
color([0.30, 0.30, 0.32])
translate([-tp_side/2, -tp_side/2, pier_h]) cube([tp_side, tp_side, tp_thk]);

// Pier adapter plate (10x10x0.75 in, ASSUMED added part)
color([0.80, 0.67, 0.24])
translate([0, 0, pier_h + tp_thk]) pier_adapter();

// ---- mount head, mounted on the polar (RA) axis ----
translate([0, 0, adapter_top])
rotate([0, 90 - lat, 0]) { // local +z -> celestial pole (alt = lat)

color([0.75, 0.78, 0.80]) ra_housing();

// DEC head at the far end of the RA axis; DEC axis _|_ RA axis
translate([0, 0, L_ra]) {

rotate([90, 0, 0]) color([0.70, 0.73, 0.76]) dec_housing();

// Counterweight shaft on the "down" side (classical GEM option;
// balance.py recommends deleting it — drawn for completeness).
translate([0, dec_face/2, 0])
rotate([-90, 0, 0]) color([0.20, 0.20, 0.22]) counterweight_shaft();

// OTA tube, parallel to the polar axis, on the saddle side.
// Saddle sits ~1/3 up the tube (matches OTA.cg_from_saddle model).
translate([0, -ota_offset, -ota_len/3])
color([0.10, 0.10, 0.12]) cylinder(d = ota_od, h = ota_len);
}
}
}

gem_assembly();
56 changes: 56 additions & 0 deletions design/mechanical/cad/counterweight_shaft.scad
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// ============================================================================
// NIGHTWATCH GEM — counterweight shaft + weights
// Parametric OpenSCAD model. Units: MILLIMETRES.
//
// Render (OpenSCAD not installed here):
// openscad -o counterweight_shaft.stl counterweight_shaft.scad
// openscad -o counterweight_shaft.png --imgsize=1200,900 counterweight_shaft.scad
//
// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm).
// NOTE: the counterweight-FREE study (balance.py) recommends DELETING this shaft;
// it is modelled so the classical GEM option remains fully drawable.
// ============================================================================

$fn = 96;

// ---- parameters (traced to params.py) --------------------------------------
shaft_d = 31.75; // P.COUNTERWEIGHTS.shaft_dia_m = u.inch(1.25) -> 0.03175 m (303-SS)
shaft_l = 457.2; // P.COUNTERWEIGHTS.shaft_len_m = u.inch(18.0) -> 0.4572 m

// Weights available = 12.5 kg (2 x 5 kg + 1 x 2.5 kg) = P.COUNTERWEIGHTS.weights_available_kg
// Cast-iron disc geometry is ASSUMED (params gives mass only, not OD/thickness).
disc_bore = 33.0; // slip fit over the shaft — ASSUMED
disc5_od = 127.0; // 5.0 kg disc OD — ASSUMED
disc5_t = 40.0; // 5.0 kg disc thickness — ASSUMED
disc25_od = 102.0; // 2.5 kg disc OD — ASSUMED
disc25_t = 33.0; // 2.5 kg disc thickness — ASSUMED

// End fittings — ASSUMED.
stud_d = 20.0; stud_l = 26.0; // threaded stud into the DEC block
knob_d = 46.0; knob_h = 16.0; // safety stop knob at the far end

module _disc(od, t) {
difference() {
cylinder(d = od, h = t);
translate([0, 0, -1]) cylinder(d = disc_bore, h = t + 2);
// knurl-substitute relief so the disc reads as a weight, not a plain ring
translate([0, 0, t/2]) rotate_extrude() translate([od/2 - 3, 0, 0]) square([3, t], center=true);
}
}

module counterweight_shaft() {
// threaded stud (mount end) at z<0
translate([0, 0, -stud_l]) cylinder(d = stud_d, h = stud_l);
// main 303-SS shaft
cylinder(d = shaft_d, h = shaft_l);
// safety stop knob at the far (top) end
translate([0, 0, shaft_l]) cylinder(d = knob_d, h = knob_h);

// Stacked weights near the far end (parked position): 5 + 5 + 2.5 kg.
z0 = shaft_l - 40;
translate([0, 0, z0 - disc5_t]) _disc(disc5_od, disc5_t);
translate([0, 0, z0 - disc5_t - disc5_t]) _disc(disc5_od, disc5_t);
translate([0, 0, z0 - disc5_t - disc5_t - disc25_t]) _disc(disc25_od, disc25_t);
}

counterweight_shaft(); // standalone; ignored by `use <>` in assembly
90 changes: 90 additions & 0 deletions design/mechanical/cad/dec_housing.scad
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// ============================================================================
// NIGHTWATCH GEM — DEC axis housing + Losmandy-D saddle
// Parametric OpenSCAD model. Units: MILLIMETRES.
//
// Render (OpenSCAD not installed here):
// openscad -o dec_housing.stl dec_housing.scad
// openscad -o dec_housing.png --imgsize=1200,900 dec_housing.scad
//
// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm).
// ============================================================================

$fn = 96;

// ---- parameters (traced to params.py) --------------------------------------
dec_outer_x = 152.4; // P.DEC_HOUSING.outer_x_m = u.inch(6) -> 0.1524 m
dec_outer_y = 152.4; // P.DEC_HOUSING.outer_y_m = u.inch(6)
dec_depth = 63.5; // P.DEC_HOUSING.depth_m = u.inch(2.5) -> bearing separation
dec_wall = 8.0; // P.DEC_HOUSING.wall_m = u.mm(8)

brg_od = 55.0; // P.BRG_DEC_6006.od_m = u.mm(55) (6006 output bearing)
brg_bore = 30.0; // P.BRG_DEC_6006.bore_m = u.mm(30)
brg_w = 13.0; // 6006 nominal width — ASSUMED (not in params.py)

drive_bore = 64.0; // P.DEC_DRIVE.bore_m = u.mm(64) (CSF-25-80 hollow bore)
flange_bc = 83.0; // ~ P.DEC_DRIVE.bore_m ; CSF-25 flange PCD (DERIVED ~1.3*bore)
flange_nbolt = 8; // ASSUMED
flange_bolt_d = 4.5; // M4 clearance — ASSUMED

// Losmandy-D dovetail (industry standard) — ASSUMED (repo does not dimension it).
losmandy_plate_w = 76.2; // 3.00 in nominal Losmandy-D plate width — ASSUMED std
dovetail_angle = 15.0; // deg, standard dovetail flank — ASSUMED std
saddle_height = 24.0; // jaw block height — ASSUMED
saddle_slot_depth = 14.0; // dovetail engagement depth — ASSUMED
clamp_bolt_d = 8.0; // side clamp knob thread — ASSUMED

// ---- dovetail channel (trapezoid: wide at base, narrow mouth) ---------------
module _dovetail_channel(chlen, plate_w, ang, depth) {
mouth = plate_w - 2 * depth * tan(ang); // narrower opening at the top
translate([0, chlen / 2, 0])
rotate([90, 0, 0]) // extrude +Z -> run along -Y
linear_extrude(height = chlen)
polygon(points = [
[-plate_w / 2, 0], // base (inside), wide
[ plate_w / 2, 0],
[ mouth / 2, depth], // mouth (top), narrow -> jaws grip
[-mouth / 2, depth]
]);
}

// ---- DEC bearing/drive housing ---------------------------------------------
module dec_core() {
difference() {
translate([-dec_outer_x/2, -dec_outer_y/2, 0])
cube([dec_outer_x, dec_outer_y, dec_depth]);

// Stepped axial bore along +z (the DEC axis):
translate([0, 0, -1]) cylinder(d = brg_od, h = brg_w + 1);
translate([0, 0, brg_w]) cylinder(d = drive_bore, h = dec_depth - brg_w + 1);
translate([0, 0, -1]) cylinder(d = brg_bore - 6, h = dec_depth + 2);

// CSF-25 flange bolt circle from the rear face.
for (i = [0 : flange_nbolt - 1])
rotate([0, 0, i * 360 / flange_nbolt])
translate([flange_bc / 2, 0, dec_depth - 14])
cylinder(d = flange_bolt_d, h = 18);
}
}

// ---- Losmandy-D saddle jaw block (sits on the +Y face of the DEC output) -----
module losmandy_saddle() {
blk_w = losmandy_plate_w + 44; // jaw width straddling the plate — ASSUMED
blk_l = dec_outer_y; // saddle length = DEC housing face
difference() {
translate([-blk_w/2, -blk_l/2, 0]) cube([blk_w, blk_l, saddle_height]);
// dovetail channel, mouth flush with the top surface
translate([0, 0, saddle_height - saddle_slot_depth])
_dovetail_channel(blk_l + 2, losmandy_plate_w, dovetail_angle, saddle_slot_depth + 2);
// side clamp knob cross-hole
translate([blk_w/2 - 8, 0, saddle_height/2])
rotate([0, 90, 0]) cylinder(d = clamp_bolt_d, h = 24, center = true);
}
}

module dec_housing() {
dec_core();
// Mount the saddle on the DEC output face (+z), lifted clear of the bore boss.
translate([0, 0, dec_depth]) losmandy_saddle();
}

dec_housing(); // executed standalone; ignored by `use <>` in assembly
59 changes: 59 additions & 0 deletions design/mechanical/cad/pier_adapter.scad
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// ============================================================================
// NIGHTWATCH GEM — pier adapter plate
// Bridges the concrete pier's steel top plate (12" x 0.375") to the RA housing.
// Parametric OpenSCAD model. Units: MILLIMETRES.
//
// Render (OpenSCAD not installed here):
// openscad -o pier_adapter.stl pier_adapter.scad
// openscad -o pier_adapter.png --imgsize=1200,900 pier_adapter.scad
//
// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm).
// ============================================================================

$fn = 96;

// ---- parameters ------------------------------------------------------------
// The adapter plate itself is 10" x 10" x 0.75" — an ADDED part (repo is silent
// on an adapter; tagged ASSUMED). It seats on the SOURCED pier top plate below.
adapter_side = 254.0; // 10 in = u.inch(10) -> ASSUMED (adapter geometry added)
adapter_thk = 19.05; // 0.75 in = u.inch(0.75) -> ASSUMED

// Below: the pier's steel top plate (SOURCED, params.py P.PIER).
pier_plate_side = 304.8; // P.PIER.top_plate_side_m = u.inch(12)
pier_plate_thk = 9.525; // P.PIER.top_plate_thk_m = u.inch(0.375) (reference)
pier_dia = 304.8; // P.PIER.diameter_m = u.inch(12) (cable-hole guide)

// Above: the RA housing corner bolt pattern it must accept.
ra_outer = 203.2; // P.RA_HOUSING.outer_x_m = u.inch(8)
ra_corner_inset = 18.0; // must match ra_housing.scad corner_inset — DERIVED

// Fastener geometry — ASSUMED.
down_bolt_d = 13.5; // M12 anchor pattern into the pier top plate
down_bc = 228.6; // bolt circle to the pier plate (9.0 in square) — ASSUMED
up_bolt_d = 7.0; // M6 tapped-clearance for the RA housing corners
cable_bore = 60.0; // central cable / drawbar pass-through — ASSUMED

module pier_adapter() {
ra_bc = ra_outer - 2 * ra_corner_inset; // RA housing corner square (mm)
difference() {
// 10" square plate with lightly chamfered footprint (via minkowski-free
// bevel: a plain square keeps the model unambiguous).
translate([-adapter_side/2, -adapter_side/2, 0])
cube([adapter_side, adapter_side, adapter_thk]);

// central cable / drawbar bore
translate([0, 0, -1]) cylinder(d = cable_bore, h = adapter_thk + 2);

// DOWN pattern: 4 bolts on a square to the pier top plate
for (sx = [-1, 1], sy = [-1, 1])
translate([sx * down_bc/2, sy * down_bc/2, -1])
cylinder(d = down_bolt_d, h = adapter_thk + 2);

// UP pattern: 4 tapped holes matching the RA housing corner bolts
for (sx = [-1, 1], sy = [-1, 1])
translate([sx * ra_bc/2, sy * ra_bc/2, -1])
cylinder(d = up_bolt_d, h = adapter_thk + 2);
}
}

pier_adapter(); // standalone; ignored by `use <>` in assembly
Loading
Loading