openscribe/case/openscribe_case.scad
Laurence 031074c9a9
Some checks failed
ci / firmware (push) Failing after 2s
ci / server (push) Failing after 45s
scaffold: OpenScribe open-source self-hosted AI voice recorder
Bootstrap of the project (M0). Sets up the monorepo, design docs, hardware BOM,
the open API contract, component skeletons, licensing and CI, following the
Default Workflow SOP.

What changed:
- CLAUDE.md + docs/: copied the Default Workflow so sessions load the SOP.
- state/: PROJECT, ARCHITECTURE, DECISIONS, TODO, NOTES filled in for OpenScribe.
  ARCHITECTURE captures the four-part design (firmware, server, app, case) and the
  three sync paths; DECISIONS records the hardware, AI-stack, storage, app and
  licensing choices; TODO lays out milestones M1-M9.
- hardware/BOM.md: two build options (compact XIAO ESP32-S3 Sense; dev ESP32-S3 +
  I2S mic + SD), wiring/pinout, indicative cost.
- api/openapi.yaml: the completely open API (device + server surfaces), including
  recording list/download/delete and exports (wav/ogg/txt/srt/vtt/md/json).
- firmware/: PlatformIO ESP32-S3 project, two board profiles, pin map, boot scaffold
  with module seams for M1-M4.
- server/: FastAPI skeleton mirroring the OpenAPI, config for self-hosted MinIO,
  faster-whisper and Ollama; stub routes browsable at /docs.
- app/, case/: Flutter app plan; parametric OpenSCAD enclosure.
- Licensing: GPL-3.0 (code), CERN-OHL-S-2.0 (hardware), CC-BY-SA-4.0 (case/docs),
  REUSE-style LICENSES/ with SPDX headers; LICENSING.md explains the split.
- CI: Forgejo Actions workflow builds firmware (both profiles) and lints/imports server.

Why:
- Everything self-hosted and openly licensed per the user's requirements: an open
  API, three sync paths (BLE control, WiFi transfer, independent WiFi upload on
  charge to generic cloud storage), and a full self-hosted transcription+summary stack.

Notes:
- No custom PCB in v1; off-the-shelf modules. Physical verification waits on parts.
- Component code is stubs at M0; features land milestone by milestone, each as its
  own branch/PR per the workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:21:37 +01:00

91 lines
No EOL
2.8 KiB
OpenSCAD

// SPDX-License-Identifier: CC-BY-SA-4.0
//
// OpenScribe parametric enclosure (OpenSCAD).
//
// M0 scaffold: a parametric two-part shell sized from the board + battery dimensions, with
// the openings a recorder needs (mic port, button, USB, LED). Milestone M8 measures the
// chosen build from hardware/BOM.md, tunes these parameters, and adds mount/clip features
// and print-in-place tolerances. Render each half and export STL from there.
//
// Usage: set `part = "bottom" | "top" | "both"` and F5/F6 to preview/render.
/* [Which part] */
part = "both"; // ["both","bottom","top"]
/* [Board] (default: XIAO ESP32-S3 Sense - Build A) */
board_l = 21.0; // board length (mm)
board_w = 17.5; // board width (mm)
board_h = 3.5; // board thickness incl. components (mm)
/* [Battery] (default: ~502030 LiPo) */
batt_l = 30.0;
batt_w = 20.0;
batt_h = 5.5;
/* [Enclosure] */
wall = 1.6; // wall thickness (mm)
gap = 0.8; // clearance around internals (mm)
corner_r = 3.0; // outer corner radius (mm)
lid_lip = 1.2; // overlap between top and bottom (mm)
/* [Openings] */
mic_port_d = 1.2; // acoustic port diameter (mm)
usb_w = 9.5; usb_h = 3.4; // USB-C cutout
button_d = 4.0; // button plunger hole
led_d = 2.5; // LED light-pipe hole
// ---- derived internal cavity ----
inner_l = max(board_l, batt_l) + 2*gap;
inner_w = board_w + batt_w + 3*gap; // board beside battery
inner_h = max(board_h, batt_h) + 2*gap;
outer_l = inner_l + 2*wall;
outer_w = inner_w + 2*wall;
outer_h = inner_h + 2*wall;
module rrect(l, w, h, r) {
hull() for (x=[r, l-r], y=[r, w-r])
translate([x, y, 0]) cylinder(h=h, r=r, $fn=48);
}
module shell(height) {
difference() {
rrect(outer_l, outer_w, height, corner_r);
translate([wall, wall, wall])
rrect(outer_l-2*wall, outer_w-2*wall, height, max(0.5, corner_r-wall));
}
}
module bottom() {
h = outer_h*0.55;
difference() {
shell(h);
// USB-C cutout on one short side
translate([-1, (outer_w-usb_w)/2, wall+1])
cube([wall+2, usb_w, usb_h]);
}
}
module top() {
h = outer_h*0.55;
difference() {
shell(h);
// mic acoustic port (top face)
translate([outer_l*0.5, outer_w*0.5, h-wall-1])
cylinder(h=wall+2, d=mic_port_d, $fn=24);
// record button hole
translate([outer_l*0.75, outer_w*0.5, h-wall-1])
cylinder(h=wall+2, d=button_d, $fn=24);
// LED light-pipe hole
translate([outer_l*0.25, outer_w*0.5, h-wall-1])
cylinder(h=wall+2, d=led_d, $fn=24);
}
}
if (part == "both") {
bottom();
translate([0, outer_w + 6, 0]) top();
} else if (part == "bottom") {
bottom();
} else {
top();
}