Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75dd63b56f | ||
|
|
64475dbf5e | ||
|
|
76b02b840c |
No files matched your search
@@ -1,76 +1,135 @@
|
|||||||
# Kidsbox
|
# Kidsbox
|
||||||
|
|
||||||
This is a battery-powered rfid-tag-controlled audio player meant to be used by kids.
|
A battery-powered, RFID/NFC-tag-controlled audio player for kids.
|
||||||
You can bundle audio-files to rfid tags and let your kids choose the music or audiobook of their favour by placing the corresponding tag atop of the box.
|
Place an NFC tag on the box — the mapped audio file or stream plays automatically.
|
||||||
|
Tags are programmed from a smartphone using any NFC writing app (e.g. NFC Tools).
|
||||||
|
The box streams from the URL written to the tag, caches it locally for offline play,
|
||||||
|
and exposes a web interface for playback control and storage management.
|
||||||
|
|
||||||
|Front|Side|
|
|Front|Side|
|
||||||
|-|-|
|
|-|-|
|
||||||
|||
|
|||
|
||||||
|
|
||||||
## Todo
|
---
|
||||||
|
|
||||||
- Volume buttons
|
## Quick Start
|
||||||
- Playlists
|
|
||||||
- Pausing
|
|
||||||
- ... I have a lot of ideas :)
|
|
||||||
|
|
||||||
## Setup
|
### 1. Install the development environment
|
||||||
|
|
||||||
1. Install ESP-ADF and ESP-IDF according to the [install guide](https://docs.espressif.com/projects/esp-adf/en/latest/get-started/index.html#installation-step-by-step).
|
```bash
|
||||||
2. Clone this repository and `cd` into the repository directory.
|
./install.sh
|
||||||
3. `git submodule init --recursive`
|
```
|
||||||
3. `cd device`
|
|
||||||
4. Make sure that you have `idf.py` available in your environment. This is described in the install guide.
|
This installs **ESP-IDF v5.2** and **ESP-ADF v2.7** into `~/esp/` and writes a `.env` file.
|
||||||
5. Execute `idf.py build` to compile, `idf.py build flash monitor` to flash it to your board.
|
Existing installations are detected automatically (via `$IDF_PATH` / `$ADF_PATH`).
|
||||||
|
|
||||||
|
### 2. Configure and build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .env && source $IDF_PATH/export.sh
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.sh` will:
|
||||||
|
- Prompt for your WiFi SSID and password
|
||||||
|
- Build the firmware (`idf.py build`)
|
||||||
|
- Optionally flash to the device
|
||||||
|
- Optionally prepare an SD card (create directories, copy web UI)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. Write an audio URL to an NFC tag with a smartphone app (e.g. **NFC Tools**).
|
||||||
|
2. Place the tag on the box → the box reads the URL, streams the audio, and caches it.
|
||||||
|
3. Subsequent plays of the same tag use the local cache (no internet needed).
|
||||||
|
4. Removing the tag pauses playback; replacing it resumes.
|
||||||
|
|
||||||
|
Without an NDEF URL on a tag, the box falls back to a local mapping file at
|
||||||
|
`/sdcard/internal/<tag-id>` (plain text, one URL or file path per file).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web Interface
|
||||||
|
|
||||||
|
Once connected to WiFi, open `http://<device-ip>` in a browser.
|
||||||
|
|
||||||
|
| Section | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Now Playing | Track URI, play/pause/stop, volume slider |
|
||||||
|
| Device Info | IP address, playback status, cache size, free heap |
|
||||||
|
| Storage | List and delete cached files |
|
||||||
|
| Bluetooth | Toggle BT speaker mode (stub — in progress) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enclosure
|
||||||
|
|
||||||
|
A parametric, fully 3D-printable enclosure lives in [`enclosure/`](enclosure/) —
|
||||||
|
a rectangular cabinet with two front speakers and a removable top where the NFC tag
|
||||||
|
rests, with **swappable speaker mounts** so you can print an adapter to fit your
|
||||||
|
drivers. See [`enclosure/README.md`](enclosure/README.md).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
### Parts
|
### Parts
|
||||||
|
|
||||||
_TBD_
|
| Part | Links |
|
||||||
|
|---|---|
|
||||||
|
| ESP32-LyraT 4.3 | [Mouser](https://www.mouser.de/ProductDetail/Espressif-Systems/ESP32-LyraT?qs=MLItCLRbWsxPzPCja546ZA%3D%3D) |
|
||||||
|
| RC522 RFID/NFC module | — |
|
||||||
|
| SD card (FAT32, any size) | — |
|
||||||
|
|
||||||
|Part|Links|
|
### DIP-Switch (LyraT 4.3)
|
||||||
|-|-|
|
|
||||||
|ESP32-LyraT 4.3|[Mouser](https://www.mouser.de/ProductDetail/Espressif-Systems/ESP32-LyraT?qs=MLItCLRbWsxPzPCja546ZA%3D%3D)|
|
|
||||||
|
|
||||||
|
| Switch | Position | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | OFF | Disconnects GPIO 12 from SD-D2 |
|
||||||
|
| 2 | OFF | Disconnects GPIO 13 from SD-D3 |
|
||||||
|
| 3 | OFF | Connects GPIO 15 to SD CMD |
|
||||||
|
| 4 | ON | Connects GPIO 13 to JTAG header |
|
||||||
|
| 5 | ON | Connects GPIO 12 to JTAG header |
|
||||||
|
| 6 | OFF | Connects GPIO 13 to SD CLK |
|
||||||
|
| 7 | OFF | Disconnects GPIO 12 from AUX-in detection |
|
||||||
|
| 8 | OFF | — |
|
||||||
|
|
||||||
### Hardware Setup
|
### RC522 Wiring
|
||||||
|
|
||||||
The peripherals must be configured for:
|
> MISO/MOSI pin names are **not** inverted between host and chip on this board.
|
||||||
|
|
||||||
- SD Card
|
| RC522 Pin | LyraT v4.3 Pin | GPIO |
|
||||||
- RFID-module
|
|---|---|---|
|
||||||
- Vol+/- buttons
|
| 3.3V | 3.3V | — |
|
||||||
|
| RST | 3.3V | — |
|
||||||
|
| GND | GND | — |
|
||||||
|
| SDA | I2C SDA | 18 |
|
||||||
|
| SCK | I2C SCL | 23 |
|
||||||
|
| MOSI | JTAG MTCK | 13 |
|
||||||
|
| MISO | JTAG MTDI | 12 |
|
||||||
|
|
||||||
The ESP32-LyraT board has only a few exposed GPIO pins. To save pins, the SD-Card reader runs only in 1-Wire mode. I have actually no idea what the limitations and differences are, but I didn't encounter any problems yet.
|
### SD Card Layout
|
||||||
|
|
||||||
#### DIP-Switch
|
```
|
||||||
|
/sdcard/
|
||||||
|
www/ ← web interface files (copied by setup.sh)
|
||||||
|
internal/ ← tag-id → URL/path mapping files (manual)
|
||||||
|
cache/ ← downloaded audio files (managed automatically)
|
||||||
|
```
|
||||||
|
|
||||||
The switches must be exatly in the positions outlined in the table.
|
---
|
||||||
|
|
||||||
|Number|Setting|Purpose|
|
## Roadmap
|
||||||
|-|-|-|
|
|
||||||
|1|OFF|Disconnects GPIO 12 from SD-D2|
|
|
||||||
|2|OFF|Disconnects GPIO 13 from SD-D3|
|
|
||||||
|3|OFF|Connects GPIO 15 to SD CMD|
|
|
||||||
|4|ON|Connects GPIO 13 to the JTAG header|
|
|
||||||
|5|ON|Connects GPIO 12 to the JTAG header|
|
|
||||||
|6|OFF|Connects GPIO 13 to SD CLK|
|
|
||||||
|7|OFF|Disconnects GPIO 12 from the AUX-in-detection|
|
|
||||||
|8|OFF| - |
|
|
||||||
|
|
||||||
#### RFID
|
- [x] RFID/NFC tag detection
|
||||||
|
- [x] NDEF URL reading from NFC tags
|
||||||
_The MISO/MOSI pins are not inverted on host/chip_
|
- [x] HTTP audio streaming
|
||||||
|
- [x] Local file cache
|
||||||
Connection Table:
|
- [x] WiFi (native ESP-IDF, no Arduino dependency)
|
||||||
|
- [x] Web interface — playback controls, storage, device info
|
||||||
|RC522 Pin|ESP32 Lyrat v4.3 Pin|GPIO Number|
|
- [ ] Volume buttons (hardware)
|
||||||
|-|-|-|
|
- [ ] Playlists
|
||||||
|3.3v|3.3v|-|
|
- [ ] Bluetooth speaker mode (A2DP sink)
|
||||||
|RST|3.3v|-|
|
|
||||||
|GND|GND|-|
|
|
||||||
|SDA|I2C SDA|18|
|
|
||||||
|SCK|I2C SCL|23|
|
|
||||||
|MOSI|JTAG MTCK|13|
|
|
||||||
|MISO|JTAG MTDI|12|
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Regenerate Kidsbox enclosure STLs and preview images from the OpenSCAD source.
|
||||||
|
#
|
||||||
|
# make stl export all printable STLs into stl/
|
||||||
|
# make img render preview PNGs into img/ (needs a display or xvfb)
|
||||||
|
# make all both
|
||||||
|
# make clean remove generated STLs and images
|
||||||
|
#
|
||||||
|
# Override the renderer if openscad isn't on PATH: make OPENSCAD=/path/to/openscad
|
||||||
|
|
||||||
|
OPENSCAD ?= openscad
|
||||||
|
SRC = kidsbox.scad params.scad lib.scad
|
||||||
|
PARTS = body lid speaker_adapter grill
|
||||||
|
|
||||||
|
STL_FILES = $(addprefix stl/,$(addsuffix .stl,$(PARTS)))
|
||||||
|
|
||||||
|
# Software GL so it works headless under xvfb on a server.
|
||||||
|
RENDER_ENV = LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe
|
||||||
|
XVFB = xvfb-run -a -s "-screen 0 1280x960x24"
|
||||||
|
IMGOPTS = --viewall --autocenter --imgsize=1280,960 --colorscheme=Tomorrow
|
||||||
|
|
||||||
|
.PHONY: all stl img clean
|
||||||
|
all: stl img
|
||||||
|
|
||||||
|
stl: $(STL_FILES)
|
||||||
|
|
||||||
|
stl/%.stl: $(SRC)
|
||||||
|
@mkdir -p stl
|
||||||
|
$(OPENSCAD) -D 'part="$*"' -o $@ kidsbox.scad
|
||||||
|
|
||||||
|
img: $(SRC)
|
||||||
|
@mkdir -p img
|
||||||
|
$(RENDER_ENV) $(XVFB) $(OPENSCAD) -D 'part="assembly"' -D 'explode=0' \
|
||||||
|
--camera=0,0,0,64,0,28,0 $(IMGOPTS) -o img/hero.png kidsbox.scad
|
||||||
|
$(RENDER_ENV) $(XVFB) $(OPENSCAD) -D 'part="assembly"' \
|
||||||
|
--camera=0,0,0,62,0,28,0 $(IMGOPTS) -o img/assembly.png kidsbox.scad
|
||||||
|
$(RENDER_ENV) $(XVFB) $(OPENSCAD) -D 'part="body"' \
|
||||||
|
--camera=0,0,0,60,0,25,0 $(IMGOPTS) -o img/body.png kidsbox.scad
|
||||||
|
$(RENDER_ENV) $(XVFB) $(OPENSCAD) -D 'part="lid"' \
|
||||||
|
--camera=0,0,0,60,0,200,0 $(IMGOPTS) -o img/lid_top.png kidsbox.scad
|
||||||
|
$(RENDER_ENV) $(XVFB) $(OPENSCAD) -D 'part="speaker_adapter"' \
|
||||||
|
--camera=0,0,0,60,0,20,0 $(IMGOPTS) -o img/speaker_adapter.png kidsbox.scad
|
||||||
|
$(RENDER_ENV) $(XVFB) $(OPENSCAD) -D 'part="grill"' \
|
||||||
|
--camera=0,0,0,55,0,20,0 $(IMGOPTS) -o img/grill.png kidsbox.scad
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f stl/*.stl img/*.png
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Kidsbox — 3D-printable enclosure
|
||||||
|
|
||||||
|
A parametric, fully 3D-printable reimagining of the original wooden Kidsbox.
|
||||||
|
The design keeps the look of the hand-built box — a rectangular cabinet with two
|
||||||
|
front-firing speakers and a removable top where the NFC tag rests — but makes the
|
||||||
|
**speaker mount swappable** so you can print an adapter to fit whatever drivers you
|
||||||
|
have on hand.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> Modelled in [OpenSCAD](https://openscad.org). Everything is driven by
|
||||||
|
> [`params.scad`](params.scad) — change a few numbers, re-export, and print.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's in the box
|
||||||
|
|
||||||
|
| Part | Qty | File | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Body / cabinet | 1 | `stl/body.stl` | Shell with all internal mounts |
|
||||||
|
| Lid | 1 | `stl/lid.stl` | Buttons + NFC reader mount underneath |
|
||||||
|
| Speaker adapter | 2 | `stl/speaker_adapter.stl` | **Swappable** — print to fit your driver |
|
||||||
|
| Speaker grill | 2 | `stl/grill.stl` | Snaps into the sound opening |
|
||||||
|
|
||||||
|
The body carries mounts for everything else:
|
||||||
|
|
||||||
|
- **2× 18650 battery holder** — open-top cradle with retaining lips (`batt_*` params)
|
||||||
|
- **ESP32-LyraT v4.3** — four floor standoffs (`lyrat_*` params)
|
||||||
|
- **RC522 NFC reader** — pocket on the lid underside, so a tag placed on top reads
|
||||||
|
through the thin (`lid_t = 2.4 mm`) PLA window
|
||||||
|
- **3 buttons** (vol−, play/pause, vol+) — 12 mm holes in a row near the lid's front edge
|
||||||
|
- **Speaker interface** — a fixed bolt pattern the swappable adapters mount to
|
||||||
|
- **Side ports** — USB/charging cutout and a power-switch hole
|
||||||
|
- **Vents** on the back wall
|
||||||
|
|
||||||
|
| Body | Lid (underside) |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Printing
|
||||||
|
|
||||||
|
| Setting | Recommendation |
|
||||||
|
|---|---|
|
||||||
|
| Material | PLA or PETG (PLA is fine; keep metal away from the NFC window) |
|
||||||
|
| Layer height | 0.2 mm |
|
||||||
|
| Walls / perimeters | 3+ |
|
||||||
|
| Infill | 15–20 % |
|
||||||
|
| Supports | **Body:** none (print open-side up). **Lid:** none. **Adapter/Grill:** none. |
|
||||||
|
| Orientation | Body open-top up; lid top-face down; adapter & grill flat |
|
||||||
|
|
||||||
|
The body is ~190 × 115 × 110 mm, so it fits a 220 × 220 bed (Ender-3 class) and larger.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assembly
|
||||||
|
|
||||||
|
1. **Speakers** → bolt each driver to a `speaker_adapter` using the adapter's inner
|
||||||
|
screw holes, then bolt the adapter to the four bosses inside the front wall (M3).
|
||||||
|
2. **Grills** → press a `grill` into each sound opening from the front.
|
||||||
|
3. **Electronics** → screw the ESP32-LyraT onto the floor standoffs; drop the 18650
|
||||||
|
holder into the cradle; route the USB/charge lead to the side cutout.
|
||||||
|
4. **NFC reader** → screw the RC522 to the four posts under the lid.
|
||||||
|
5. **Buttons** → fit the three 12 mm momentary switches into the lid holes.
|
||||||
|
6. **Close up** → lower the lid (the lip locates it) and drive four M3 screws into the
|
||||||
|
corner bosses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Swapping speakers (the whole point)
|
||||||
|
|
||||||
|
The front wall never changes — it has a **fixed interface**: a `spk_sound_d` opening
|
||||||
|
and four bosses on a `spk_bolt_d` circle. Every speaker bolts to the box through an
|
||||||
|
**adapter** that you print to match *your* driver. To make a new adapter, edit the
|
||||||
|
adapter block in [`params.scad`](params.scad):
|
||||||
|
|
||||||
|
```scad
|
||||||
|
// ---- swappable speaker ADAPTER (edit for YOUR speaker) ----
|
||||||
|
spk_frame_d = 66; // your speaker's outer frame diameter
|
||||||
|
spk_cutout_d = 58; // cone clearance (a bit less than the cone surround)
|
||||||
|
spk_screw_count = 4; // your speaker's mounting-hole count
|
||||||
|
spk_screw_bolt_d = 60; // your speaker's mounting-hole bolt circle
|
||||||
|
```
|
||||||
|
|
||||||
|
Then re-export just that part:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openscad -D 'part="speaker_adapter"' -o stl/speaker_adapter.stl kidsbox.scad
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave the `spk_sound_d` / `spk_bolt_d` (panel) values alone and your new adapter will
|
||||||
|
drop straight onto the same box.
|
||||||
|
|
||||||
|
| Speaker adapter (swappable) | Grill |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regenerating everything
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make stl # export all STLs into stl/
|
||||||
|
make img # render preview PNGs (needs a display, or xvfb on a server)
|
||||||
|
make all
|
||||||
|
```
|
||||||
|
|
||||||
|
Or one part at a time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openscad -D 'part="body"' -o stl/body.stl kidsbox.scad
|
||||||
|
openscad -D 'part="lid"' -o stl/lid.stl kidsbox.scad
|
||||||
|
openscad -D 'part="speaker_adapter"' -o stl/speaker_adapter.stl kidsbox.scad
|
||||||
|
openscad -D 'part="grill"' -o stl/grill.stl kidsbox.scad
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Verify before mass-printing
|
||||||
|
|
||||||
|
These dimensions are sensible defaults derived from the reference photos and common
|
||||||
|
parts, **not** from calipers on your exact hardware. Before printing the whole set,
|
||||||
|
print a quick test of the fit-critical features and confirm against your parts:
|
||||||
|
|
||||||
|
- ESP32-LyraT mounting-hole rectangle (`lyrat_hole_dx`, `lyrat_hole_dy`)
|
||||||
|
- RC522 mounting-hole rectangle (`bx`, `by` in the `rc522_mount()` module)
|
||||||
|
- Your 18650 holder footprint (`batt_holder_*`)
|
||||||
|
- Speaker frame/cone/bolt-circle (`spk_frame_d`, `spk_cutout_d`, `spk_screw_bolt_d`)
|
||||||
|
|
||||||
|
A cheap way to validate the speaker fit is to print one `speaker_adapter` first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
enclosure/
|
||||||
|
├── kidsbox.scad master model + part dispatcher
|
||||||
|
├── params.scad all tunable dimensions ← edit here
|
||||||
|
├── lib.scad helper modules
|
||||||
|
├── Makefile export/render targets
|
||||||
|
├── stl/ exported STLs
|
||||||
|
└── img/ rendered previews
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,275 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Kidsbox — parametric 3D-printable enclosure
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Open in OpenSCAD and set `part` (top-right) or export from the CLI:
|
||||||
|
//
|
||||||
|
// openscad -D 'part="body"' -o stl/body.stl kidsbox.scad
|
||||||
|
// openscad -D 'part="lid"' -o stl/lid.stl kidsbox.scad
|
||||||
|
// openscad -D 'part="speaker_adapter"' -o stl/speaker_adapter.stl kidsbox.scad
|
||||||
|
// openscad -D 'part="grill"' -o stl/grill.stl kidsbox.scad
|
||||||
|
//
|
||||||
|
// `part="assembly"` shows everything together (for preview only).
|
||||||
|
// All tunables live in params.scad.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
include <params.scad>
|
||||||
|
use <lib.scad>
|
||||||
|
|
||||||
|
part = "assembly"; // body | lid | speaker_adapter | grill | assembly
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Front-panel speaker interface (the FIXED "mount for the speaker mount")
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Subtractive: the sound opening + an outer recess for the grill flange.
|
||||||
|
module speaker_cut(cx, cz) {
|
||||||
|
translate([cx, 0, cz]) rotate([-90, 0, 0]) {
|
||||||
|
// sound opening straight through the front wall
|
||||||
|
translate([0, 0, -1]) cylinder(h = wall + 2, d = spk_sound_d);
|
||||||
|
// grill flange recess on the OUTSIDE face
|
||||||
|
translate([0, 0, -0.01]) cylinder(h = grill_t + 0.01, d = spk_sound_d + 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additive: 4 bosses on the INSIDE so a swappable adapter can bolt on.
|
||||||
|
module speaker_bosses(cx, cz) {
|
||||||
|
translate([cx, 0, cz]) rotate([-90, 0, 0])
|
||||||
|
translate([0, 0, wall - 0.5])
|
||||||
|
bolt_circle(spk_bolt_d, spk_bolt_count, 45)
|
||||||
|
boss(spk_boss_d, spk_boss_h + 0.5, 2.6); // self-tap for M3
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Button holes through the lid (row near the front edge)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
module button_cuts() {
|
||||||
|
for (i = [0 : btn_count - 1]) {
|
||||||
|
x = box_w/2 + (i - (btn_count - 1) / 2) * btn_pitch;
|
||||||
|
translate([x, btn_row_y, box_h - 1])
|
||||||
|
cylinder(h = lid_t + 2, d = btn_d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Side ports: USB/charge (right wall) and power switch (left wall)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
module side_ports() {
|
||||||
|
// USB on +X wall
|
||||||
|
translate([box_w - wall - 1, box_d * 0.30, usb_center_z])
|
||||||
|
cube([wall + 2, usb_w, usb_h], center = false);
|
||||||
|
// power switch on -X wall
|
||||||
|
translate([-1, box_d * 0.30, sw_center_z]) rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall + 2, d = sw_d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ventilation slots on the back wall (+Y)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
module vents() {
|
||||||
|
total = (vent_count - 1) * (vent_slot_w + 4);
|
||||||
|
for (i = [0 : vent_count - 1]) {
|
||||||
|
x = box_w/2 - total/2 + i * (vent_slot_w + 4);
|
||||||
|
translate([x, box_d - wall - 1, box_h * 0.55])
|
||||||
|
cube([vent_slot_w, wall + 2, vent_slot_len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal mounts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Corner posts the lid screws into (fused into the wall corners, top-anchored).
|
||||||
|
module lid_bosses() {
|
||||||
|
boss_len = lid_rim_h + 10;
|
||||||
|
for (sx = [lid_boss_inset, box_w - lid_boss_inset],
|
||||||
|
sy = [lid_boss_inset, box_d - lid_boss_inset])
|
||||||
|
translate([sx, sy, box_h - boss_len])
|
||||||
|
boss(lid_boss_d, boss_len, lid_screw_d - 0.6); // self-tap
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESP32-LyraT standoffs (board lies flat on the floor, toward the back).
|
||||||
|
lyrat_cx = box_w/2;
|
||||||
|
lyrat_cy = box_d - lyrat_d/2 - 10;
|
||||||
|
module lyrat_standoffs() {
|
||||||
|
for (sx = [-lyrat_hole_dx/2, lyrat_hole_dx/2],
|
||||||
|
sy = [-lyrat_hole_dy/2, lyrat_hole_dy/2])
|
||||||
|
translate([lyrat_cx + sx, lyrat_cy + sy, floor_t])
|
||||||
|
boss(lyrat_standoff_d, lyrat_standoff_h, lyrat_screw_d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2x 18650 battery cradle (open-top tray with end stops + clips), front-left.
|
||||||
|
batt_cx = box_w/2;
|
||||||
|
batt_cy = wall + batt_holder_d/2 + 4;
|
||||||
|
module battery_cradle() {
|
||||||
|
ow = batt_holder_w + 2*batt_wall;
|
||||||
|
od = batt_holder_d + 2*batt_wall;
|
||||||
|
translate([batt_cx - ow/2, batt_cy - od/2, floor_t])
|
||||||
|
difference() {
|
||||||
|
cube([ow, od, batt_holder_h]);
|
||||||
|
// hollow pocket, open top
|
||||||
|
translate([batt_wall, batt_wall, batt_wall])
|
||||||
|
cube([batt_holder_w, batt_holder_d, batt_holder_h]);
|
||||||
|
// cable notch in the front wall of the cradle
|
||||||
|
translate([ow/2 - 6, -1, batt_holder_h - 8])
|
||||||
|
cube([12, batt_wall + 2, 10]);
|
||||||
|
}
|
||||||
|
if (batt_clip) {
|
||||||
|
// two retaining lips along the long sides
|
||||||
|
for (sy = [batt_cy - od/2 + batt_wall/2, batt_cy + od/2 - batt_wall/2])
|
||||||
|
translate([batt_cx - batt_holder_w/3, sy, floor_t + batt_holder_h])
|
||||||
|
rotate([0, 0, 0])
|
||||||
|
cube([batt_holder_w*2/3, batt_wall, 2.5], center = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BODY
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
module body() {
|
||||||
|
spk_l = box_w/2 - spk_spacing/2;
|
||||||
|
spk_r = box_w/2 + spk_spacing/2;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
shell([box_w, box_d, box_h], corner_r, wall, floor_t);
|
||||||
|
speaker_bosses(spk_l, spk_center_z);
|
||||||
|
speaker_bosses(spk_r, spk_center_z);
|
||||||
|
lid_bosses();
|
||||||
|
lyrat_standoffs();
|
||||||
|
battery_cradle();
|
||||||
|
}
|
||||||
|
speaker_cut(spk_l, spk_center_z);
|
||||||
|
speaker_cut(spk_r, spk_center_z);
|
||||||
|
side_ports();
|
||||||
|
vents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// LID (NFC reader sits underneath; print thin-side up)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// RC522 pocket posts on the lid underside.
|
||||||
|
module rc522_mount() {
|
||||||
|
// board centred, posts hang DOWN from the lid underside (local -Z here)
|
||||||
|
bx = 54; by = 34; // RC522 mounting-hole rectangle (verify)
|
||||||
|
for (sx = [-bx/2, bx/2], sy = [-by/2, by/2])
|
||||||
|
translate([sx, sy, -8])
|
||||||
|
cylinder(h = 8, d = 6);
|
||||||
|
// pilot holes
|
||||||
|
for (sx = [-bx/2, bx/2], sy = [-by/2, by/2])
|
||||||
|
translate([sx, sy, -8.5])
|
||||||
|
cylinder(h = 9, d = 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
// top plate sitting on the wall tops
|
||||||
|
translate([box_w/2, box_d/2, box_h])
|
||||||
|
linear_extrude(lid_t)
|
||||||
|
rrect(box_w, box_d, corner_r);
|
||||||
|
// locating lip dropping into the interior
|
||||||
|
translate([box_w/2, box_d/2, box_h - lid_rim_h])
|
||||||
|
linear_extrude(lid_rim_h)
|
||||||
|
difference() {
|
||||||
|
rrect(inner_w - 2*fit, inner_d - 2*fit, max(0.1, corner_r - wall));
|
||||||
|
rrect(inner_w - 2*fit - 2*lid_rim_t,
|
||||||
|
inner_d - 2*fit - 2*lid_rim_t,
|
||||||
|
max(0.1, corner_r - wall - lid_rim_t));
|
||||||
|
}
|
||||||
|
// RC522 mount, centred under the lid
|
||||||
|
translate([box_w/2, box_d/2, box_h + lid_t]) rc522_mount();
|
||||||
|
}
|
||||||
|
// corner screw holes (countersunk) through the plate
|
||||||
|
for (sx = [lid_boss_inset, box_w - lid_boss_inset],
|
||||||
|
sy = [lid_boss_inset, box_d - lid_boss_inset]) {
|
||||||
|
translate([sx, sy, box_h - 0.5])
|
||||||
|
cylinder(h = lid_t + 1, d = lid_screw_d);
|
||||||
|
translate([sx, sy, box_h + lid_t - 1.4])
|
||||||
|
cylinder(h = 1.6, d1 = lid_screw_d, d2 = lid_screw_d + 3.5);
|
||||||
|
}
|
||||||
|
// button holes near the front edge
|
||||||
|
button_cuts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SWAPPABLE SPEAKER ADAPTER (print one per speaker model)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
module speaker_adapter() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = adapter_t, d = adapter_outer_d);
|
||||||
|
// frame seat ring raised on the back so the speaker locates
|
||||||
|
translate([0, 0, adapter_t])
|
||||||
|
ring(spk_frame_d + 6, spk_cutout_d, spk_frame_seat);
|
||||||
|
}
|
||||||
|
// cone clearance
|
||||||
|
translate([0, 0, -0.5]) cylinder(h = adapter_t + spk_frame_seat + 1, d = spk_cutout_d);
|
||||||
|
// speaker frame recess on the back
|
||||||
|
translate([0, 0, adapter_t - 0.01])
|
||||||
|
cylinder(h = spk_frame_seat + 0.02, d = spk_frame_d);
|
||||||
|
// mounting holes to the panel bosses (FIXED interface)
|
||||||
|
bolt_circle(spk_bolt_d, spk_bolt_count, 45)
|
||||||
|
translate([0, 0, -0.5]) cylinder(h = adapter_t + spk_frame_seat + 1, d = spk_bolt_hole_d);
|
||||||
|
// the speaker's OWN screw holes (swappable per model)
|
||||||
|
bolt_circle(spk_screw_bolt_d, spk_screw_count, 0)
|
||||||
|
translate([0, 0, -0.5]) cylinder(h = adapter_t + spk_frame_seat + 1, d = spk_screw_d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SPEAKER GRILL (snaps into the sound opening)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
module grill() {
|
||||||
|
r_out = spk_sound_d/2;
|
||||||
|
union() {
|
||||||
|
// outer flange that seats in the panel recess
|
||||||
|
ring(spk_sound_d + 8, spk_sound_d - 4, grill_t);
|
||||||
|
// friction lip into the hole
|
||||||
|
translate([0, 0, -grill_lip_h])
|
||||||
|
ring(spk_sound_d - 2*fit, spk_sound_d - 2*fit - 3, grill_lip_h + grill_t);
|
||||||
|
// concentric rings
|
||||||
|
for (i = [1 : grill_rings])
|
||||||
|
ring(2*r_out * i/(grill_rings+1) + 1.4,
|
||||||
|
2*r_out * i/(grill_rings+1) - 1.4, grill_t);
|
||||||
|
// central hub
|
||||||
|
cylinder(h = grill_t, d = 6);
|
||||||
|
// radial spokes
|
||||||
|
for (i = [0 : grill_spokes - 1])
|
||||||
|
rotate([0, 0, i * 360/grill_spokes])
|
||||||
|
translate([0, -1.0, 0]) cube([r_out, 2.0, grill_t]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ASSEMBLY PREVIEW
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
explode = 45; // lid lift for the exploded preview (0 = closed)
|
||||||
|
|
||||||
|
module assembly() {
|
||||||
|
spk_l = box_w/2 - spk_spacing/2;
|
||||||
|
spk_r = box_w/2 + spk_spacing/2;
|
||||||
|
color("WhiteSmoke") body();
|
||||||
|
color("Gainsboro") translate([0, 0, explode]) lid();
|
||||||
|
for (cx = [spk_l, spk_r]) {
|
||||||
|
// adapter just inside the front wall
|
||||||
|
color("SteelBlue")
|
||||||
|
translate([cx, wall + adapter_t, spk_center_z]) rotate([90, 0, 0])
|
||||||
|
speaker_adapter();
|
||||||
|
// grill in the opening
|
||||||
|
color("DimGray")
|
||||||
|
translate([cx, grill_t, spk_center_z]) rotate([-90, 0, 0])
|
||||||
|
grill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DISPATCH
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
if (part == "body") body();
|
||||||
|
else if (part == "lid") lid();
|
||||||
|
else if (part == "speaker_adapter") speaker_adapter();
|
||||||
|
else if (part == "grill") grill();
|
||||||
|
else assembly();
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Kidsbox enclosure — helper modules
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// A box with rounded VERTICAL edges only (flat top & bottom for clean FDM).
|
||||||
|
// size = [x, y, z], r = corner radius.
|
||||||
|
module rbox(size, r) {
|
||||||
|
x = size[0]; y = size[1]; z = size[2];
|
||||||
|
hull() {
|
||||||
|
for (sx = [r, x - r], sy = [r, y - r])
|
||||||
|
translate([sx, sy, 0]) cylinder(h = z, r = r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hollow shell: outer rbox minus inner rbox, open at the top (+Z).
|
||||||
|
// Floor thickness `ft`, wall `w`.
|
||||||
|
module shell(size, r, w, ft) {
|
||||||
|
x = size[0]; y = size[1]; z = size[2];
|
||||||
|
difference() {
|
||||||
|
rbox([x, y, z], r);
|
||||||
|
translate([w, w, ft])
|
||||||
|
rbox([x - 2*w, y - 2*w, z - ft + 1], max(0.1, r - w));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A screw boss (solid post with a pilot hole) standing in +Z.
|
||||||
|
module boss(od, h, hole_d) {
|
||||||
|
difference() {
|
||||||
|
cylinder(h = h, d = od);
|
||||||
|
translate([0, 0, -0.5]) cylinder(h = h + 1, d = hole_d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A flat ring.
|
||||||
|
module ring(od, id, h) {
|
||||||
|
difference() {
|
||||||
|
cylinder(h = h, d = od);
|
||||||
|
translate([0, 0, -0.5]) cylinder(h = h + 1, d = id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bolt circle: place children at `count` points on diameter `d`.
|
||||||
|
module bolt_circle(d, count, start = 0) {
|
||||||
|
for (i = [0 : count - 1])
|
||||||
|
rotate([0, 0, start + i * 360 / count])
|
||||||
|
translate([d/2, 0, 0]) children();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A rounded-rectangle plate (2D) for linear_extrude.
|
||||||
|
module rrect(x, y, r) {
|
||||||
|
hull() for (sx = [r, x - r], sy = [r, y - r])
|
||||||
|
translate([sx - x/2, sy - y/2]) circle(r = r);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Kidsbox enclosure — parameters
|
||||||
|
// All dimensions in millimetres. Edit here, then re-export the STLs.
|
||||||
|
// See README.md for the export commands.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ---- global print settings ------------------------------------------------
|
||||||
|
$fn = 64; // facet count for curves (raise for final render)
|
||||||
|
wall = 3.0; // shell wall thickness
|
||||||
|
floor_t = 3.0; // bottom thickness
|
||||||
|
corner_r = 6.0; // outer vertical-edge radius
|
||||||
|
fit = 0.30; // general clearance for mating parts (per side)
|
||||||
|
|
||||||
|
// ---- outer box dimensions (interior is derived) ---------------------------
|
||||||
|
box_w = 190; // width (X) — roughly one banana, per reference photo
|
||||||
|
box_h = 110; // height (Z)
|
||||||
|
box_d = 115; // depth (Y)
|
||||||
|
|
||||||
|
// derived interior
|
||||||
|
inner_w = box_w - 2*wall;
|
||||||
|
inner_d = box_d - 2*wall;
|
||||||
|
inner_h = box_h - floor_t; // open-top shell, lid closes it
|
||||||
|
|
||||||
|
// ---- lid -------------------------------------------------------------------
|
||||||
|
lid_t = 2.4; // thin enough for 13.56 MHz NFC to read through PLA
|
||||||
|
lid_rim_h = 6.0; // depth of the lip that drops into the box
|
||||||
|
lid_rim_t = 1.6; // thickness of that lip
|
||||||
|
lid_screw_d = 3.2; // M3 clearance
|
||||||
|
lid_boss_d = 9.0; // corner screw-boss outer diameter
|
||||||
|
lid_boss_inset = 9.0; // boss centre inset from interior corner
|
||||||
|
|
||||||
|
// ---- speaker panel interface (FIXED) --------------------------------------
|
||||||
|
// This is the standard "mount for the speaker mount". Every swappable
|
||||||
|
// speaker adapter bolts to THIS pattern, so the box never changes.
|
||||||
|
spk_sound_d = 66; // sound opening through the front panel
|
||||||
|
spk_bolt_d = 80; // bolt circle for the adapter screws
|
||||||
|
spk_bolt_count = 4; // number of adapter screws
|
||||||
|
spk_bolt_hole_d = 3.2; // M3 clearance through the panel
|
||||||
|
spk_boss_d = 8.0; // screw boss diameter on the inside of the panel
|
||||||
|
spk_boss_h = 5.0; // screw boss height (into the box)
|
||||||
|
spk_spacing = 94; // centre-to-centre of the two speakers (X)
|
||||||
|
spk_center_z = 48; // speaker centre height from the box floor (Z)
|
||||||
|
|
||||||
|
// ---- swappable speaker ADAPTER (edit for YOUR speaker) ---------------------
|
||||||
|
// Print one adapter per speaker model. Defaults suit a common 3" driver.
|
||||||
|
adapter_t = 3.0; // adapter plate thickness
|
||||||
|
adapter_outer_d = 88; // outer diameter of the adapter disc
|
||||||
|
spk_frame_d = 66; // speaker outer frame diameter (seat recess)
|
||||||
|
spk_frame_seat = 1.5; // depth the frame sinks into the adapter
|
||||||
|
spk_cutout_d = 58; // cone clearance hole through the adapter
|
||||||
|
spk_screw_count = 4; // the SPEAKER's own mounting holes
|
||||||
|
spk_screw_bolt_d = 60; // bolt circle of the speaker's mounting holes
|
||||||
|
spk_screw_d = 3.0; // speaker screw clearance
|
||||||
|
|
||||||
|
// ---- speaker grill (snaps into the sound opening) -------------------------
|
||||||
|
grill_t = 2.4; // grill face thickness
|
||||||
|
grill_rings = 3; // number of concentric rings
|
||||||
|
grill_spokes = 6; // number of radial spokes
|
||||||
|
grill_lip_h = 5.0; // snap lip depth into the sound hole
|
||||||
|
grill_standoff = 3.0; // gap the grill holds off the cone
|
||||||
|
|
||||||
|
// ---- buttons (top lid, row near the front edge) ---------------------------
|
||||||
|
// Kept off the front face (which stays clean like the reference box) and
|
||||||
|
// clear of the central NFC reader, which sits under the lid centre.
|
||||||
|
btn_d = 12.2; // panel hole for a 12 mm momentary switch
|
||||||
|
btn_count = 3; // vol-, play/pause, vol+
|
||||||
|
btn_pitch = 26; // spacing between buttons along X
|
||||||
|
btn_row_y = 18; // distance of the button row from the front wall
|
||||||
|
|
||||||
|
// ---- side ports ------------------------------------------------------------
|
||||||
|
usb_w = 12; // micro-USB / charge access cutout (W)
|
||||||
|
usb_h = 7; // (H)
|
||||||
|
usb_center_z = 22; // height of the port from the floor
|
||||||
|
sw_d = 7; // power slide-switch hole diameter
|
||||||
|
sw_center_z = 22;
|
||||||
|
|
||||||
|
// ---- ESP32-LyraT v4.3 mount (verify against your board) -------------------
|
||||||
|
lyrat_w = 99; // board long edge
|
||||||
|
lyrat_d = 57; // board short edge
|
||||||
|
lyrat_hole_dx = 92; // mounting-hole rectangle, long axis (VERIFY)
|
||||||
|
lyrat_hole_dy = 50; // mounting-hole rectangle, short axis (VERIFY)
|
||||||
|
lyrat_standoff_h = 6; // standoff height off the floor
|
||||||
|
lyrat_standoff_d = 6; // standoff outer diameter
|
||||||
|
lyrat_screw_d = 2.6; // self-tap for M2.5/M3
|
||||||
|
|
||||||
|
// ---- 2x 18650 battery holder cradle ---------------------------------------
|
||||||
|
batt_holder_w = 78; // typical 2-cell holder footprint (X)
|
||||||
|
batt_holder_d = 39; // (Y)
|
||||||
|
batt_holder_h = 21; // (Z)
|
||||||
|
batt_wall = 2.0; // cradle wall
|
||||||
|
batt_clip = true; // add retaining clips
|
||||||
|
|
||||||
|
// ---- ventilation -----------------------------------------------------------
|
||||||
|
vent_slot_w = 3;
|
||||||
|
vent_slot_len = 28;
|
||||||
|
vent_count = 5;
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# install.sh — set up the Kidsbox development environment
|
||||||
|
# Installs ESP-IDF v5.2 + ESP-ADF v2.7, or validates an existing installation.
|
||||||
|
# Supports: Linux (Debian/Ubuntu/Arch) and macOS (with Homebrew)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[install]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||||
|
die() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
|
||||||
|
heading() { echo -e "\n${BOLD}$*${NC}"; }
|
||||||
|
|
||||||
|
IDF_VERSION="v5.2.3"
|
||||||
|
ADF_VERSION="v2.7"
|
||||||
|
IDF_DEFAULT="$HOME/esp/esp-idf"
|
||||||
|
ADF_DEFAULT="$HOME/esp/esp-adf"
|
||||||
|
|
||||||
|
# ── detect OS ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
OS="$(uname -s)"
|
||||||
|
if [[ "$OS" == "Darwin" ]]; then
|
||||||
|
info "macOS detected"
|
||||||
|
MISSING_HINT=' Install missing tools with Homebrew: brew install git python3 cmake ninja'
|
||||||
|
elif [[ "$OS" == "Linux" ]]; then
|
||||||
|
info "Linux detected"
|
||||||
|
MISSING_HINT=' Install missing tools: sudo apt-get install git python3 python3-pip cmake ninja-build'
|
||||||
|
else
|
||||||
|
die "Unsupported OS: $OS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── prerequisites ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "Checking prerequisites"
|
||||||
|
|
||||||
|
MISSING=()
|
||||||
|
for cmd in git python3 cmake ninja; do
|
||||||
|
if command -v "$cmd" &>/dev/null; then
|
||||||
|
info "$cmd: $(command -v $cmd)"
|
||||||
|
else
|
||||||
|
MISSING+=("$cmd")
|
||||||
|
warn "$cmd: NOT FOUND"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#MISSING[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${MISSING_HINT}"
|
||||||
|
if [[ "$OS" == "Darwin" ]]; then
|
||||||
|
echo " If Homebrew is not installed: https://brew.sh"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
die "Missing prerequisites: ${MISSING[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pip3 may not exist as a standalone command on newer Python installs; try both
|
||||||
|
if command -v pip3 &>/dev/null; then
|
||||||
|
info "pip3: $(command -v pip3)"
|
||||||
|
elif python3 -m pip --version &>/dev/null 2>&1; then
|
||||||
|
info "pip: available via python3 -m pip"
|
||||||
|
else
|
||||||
|
echo -e "${MISSING_HINT}"
|
||||||
|
die "pip not found (tried pip3 and python3 -m pip)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||||
|
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,8) else 1)' \
|
||||||
|
|| die "Python >= 3.8 required (found $PYTHON_VERSION)"
|
||||||
|
info "Python $PYTHON_VERSION: OK"
|
||||||
|
|
||||||
|
# macOS: Xcode Command Line Tools are required by ESP-IDF's install script
|
||||||
|
if [[ "$OS" == "Darwin" ]]; then
|
||||||
|
if ! xcode-select -p &>/dev/null; then
|
||||||
|
die "Xcode Command Line Tools not found.\n Run: xcode-select --install\n Then re-run this script."
|
||||||
|
fi
|
||||||
|
info "Xcode CLT: $(xcode-select -p)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── ESP-IDF ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "ESP-IDF ($IDF_VERSION)"
|
||||||
|
|
||||||
|
if [[ -n "${IDF_PATH:-}" && -f "$IDF_PATH/export.sh" ]]; then
|
||||||
|
INSTALLED_TAG=$(git -C "$IDF_PATH" describe --tags 2>/dev/null || echo "unknown")
|
||||||
|
info "Found existing ESP-IDF at \$IDF_PATH=$IDF_PATH (tag: $INSTALLED_TAG)"
|
||||||
|
warn "Skipping IDF install. To use a fresh install, unset \$IDF_PATH and re-run."
|
||||||
|
else
|
||||||
|
IDF_PATH="${IDF_DEFAULT}"
|
||||||
|
info "Installing ESP-IDF $IDF_VERSION to $IDF_PATH"
|
||||||
|
mkdir -p "$(dirname "$IDF_PATH")"
|
||||||
|
if [[ -d "$IDF_PATH/.git" ]]; then
|
||||||
|
git -C "$IDF_PATH" fetch --tags
|
||||||
|
git -C "$IDF_PATH" checkout "$IDF_VERSION"
|
||||||
|
else
|
||||||
|
git clone --depth 1 --branch "$IDF_VERSION" \
|
||||||
|
https://github.com/espressif/esp-idf.git "$IDF_PATH"
|
||||||
|
fi
|
||||||
|
git -C "$IDF_PATH" submodule update --init --recursive
|
||||||
|
info "Running ESP-IDF install script (downloads toolchain — may take a while)"
|
||||||
|
"$IDF_PATH/install.sh" esp32
|
||||||
|
info "ESP-IDF installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── ESP-ADF ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "ESP-ADF ($ADF_VERSION)"
|
||||||
|
|
||||||
|
if [[ -n "${ADF_PATH:-}" && -d "$ADF_PATH" ]]; then
|
||||||
|
INSTALLED_TAG=$(git -C "$ADF_PATH" describe --tags 2>/dev/null || echo "unknown")
|
||||||
|
info "Found existing ESP-ADF at \$ADF_PATH=$ADF_PATH (tag: $INSTALLED_TAG)"
|
||||||
|
warn "Skipping ADF install. To use a fresh install, unset \$ADF_PATH and re-run."
|
||||||
|
else
|
||||||
|
ADF_PATH="${ADF_DEFAULT}"
|
||||||
|
info "Installing ESP-ADF $ADF_VERSION to $ADF_PATH"
|
||||||
|
mkdir -p "$(dirname "$ADF_PATH")"
|
||||||
|
if [[ -d "$ADF_PATH/.git" ]]; then
|
||||||
|
git -C "$ADF_PATH" fetch --tags
|
||||||
|
git -C "$ADF_PATH" checkout "$ADF_VERSION"
|
||||||
|
else
|
||||||
|
git clone --depth 1 --branch "$ADF_VERSION" \
|
||||||
|
https://github.com/espressif/esp-adf.git "$ADF_PATH"
|
||||||
|
fi
|
||||||
|
git -C "$ADF_PATH" submodule update --init --recursive
|
||||||
|
info "ESP-ADF installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── write .env ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "Writing .env"
|
||||||
|
|
||||||
|
ENV_FILE="$SCRIPT_DIR/.env"
|
||||||
|
cat > "$ENV_FILE" <<EOF
|
||||||
|
# Generated by install.sh — source this before building:
|
||||||
|
# source .env && source \$IDF_PATH/export.sh
|
||||||
|
export IDF_PATH="${IDF_PATH:-$IDF_DEFAULT}"
|
||||||
|
export ADF_PATH="${ADF_PATH:-$ADF_DEFAULT}"
|
||||||
|
EOF
|
||||||
|
info "Wrote $ENV_FILE"
|
||||||
|
|
||||||
|
# ── done ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "Done!"
|
||||||
|
echo ""
|
||||||
|
echo " Next steps:"
|
||||||
|
echo " 1. Source the environment:"
|
||||||
|
echo " source .env && source \$IDF_PATH/export.sh"
|
||||||
|
echo " 2. Configure WiFi and build:"
|
||||||
|
echo " ./setup.sh"
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# setup.sh — configure, build, and optionally flash/deploy Kidsbox
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
DEVICE_DIR="$SCRIPT_DIR/device"
|
||||||
|
WWW_DIR="$SCRIPT_DIR/www"
|
||||||
|
|
||||||
|
BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[setup]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||||
|
die() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
|
||||||
|
heading() { echo -e "\n${BOLD}$*${NC}"; }
|
||||||
|
prompt() { echo -e "${YELLOW}$*${NC}"; }
|
||||||
|
|
||||||
|
# ── check environment ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "Checking environment"
|
||||||
|
|
||||||
|
[[ -f "$SCRIPT_DIR/.env" ]] && source "$SCRIPT_DIR/.env"
|
||||||
|
|
||||||
|
[[ -n "${IDF_PATH:-}" ]] || die "IDF_PATH not set. Run: source .env && source \$IDF_PATH/export.sh"
|
||||||
|
[[ -n "${ADF_PATH:-}" ]] || die "ADF_PATH not set. Run: source .env && source \$IDF_PATH/export.sh"
|
||||||
|
command -v idf.py &>/dev/null || die "idf.py not found. Run: source \$IDF_PATH/export.sh"
|
||||||
|
|
||||||
|
info "IDF_PATH: $IDF_PATH"
|
||||||
|
info "ADF_PATH: $ADF_PATH"
|
||||||
|
|
||||||
|
# ── WiFi credentials ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "WiFi configuration"
|
||||||
|
|
||||||
|
SDKCONFIG_DEFAULTS="$DEVICE_DIR/sdkconfig.defaults"
|
||||||
|
|
||||||
|
if [[ -f "$SDKCONFIG_DEFAULTS" ]] \
|
||||||
|
&& grep -q "CONFIG_ESP_WIFI_SSID" "$SDKCONFIG_DEFAULTS" 2>/dev/null; then
|
||||||
|
CURRENT_SSID=$(grep 'CONFIG_ESP_WIFI_SSID' "$SDKCONFIG_DEFAULTS" | cut -d'"' -f2)
|
||||||
|
info "WiFi already configured (SSID: $CURRENT_SSID)"
|
||||||
|
prompt "Re-enter credentials? [y/N] "
|
||||||
|
read -r RECONFIGURE
|
||||||
|
[[ "$RECONFIGURE" =~ ^[Yy]$ ]] || SKIP_WIFI=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${SKIP_WIFI:-}" ]]; then
|
||||||
|
prompt "WiFi SSID: "
|
||||||
|
read -r WIFI_SSID
|
||||||
|
prompt "WiFi Password: "
|
||||||
|
read -rs WIFI_PASSWORD
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
[[ -n "$WIFI_SSID" ]] || die "SSID cannot be empty"
|
||||||
|
[[ -n "$WIFI_PASSWORD" ]] || die "Password cannot be empty"
|
||||||
|
|
||||||
|
cat > "$SDKCONFIG_DEFAULTS" <<EOF
|
||||||
|
CONFIG_ESP_WIFI_SSID="${WIFI_SSID}"
|
||||||
|
CONFIG_ESP_WIFI_PASSWORD="${WIFI_PASSWORD}"
|
||||||
|
CONFIG_ESP_MAXIMUM_RETRY=5
|
||||||
|
CONFIG_ESP_ADF_BOARD_LYRAT_V4_3=y
|
||||||
|
CONFIG_IDF_TARGET="esp32"
|
||||||
|
EOF
|
||||||
|
info "Wrote $SDKCONFIG_DEFAULTS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── set target & build ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "Building firmware"
|
||||||
|
|
||||||
|
cd "$DEVICE_DIR"
|
||||||
|
|
||||||
|
idf.py set-target esp32
|
||||||
|
|
||||||
|
info "Running idf.py build (this takes a few minutes on first run)"
|
||||||
|
idf.py build
|
||||||
|
|
||||||
|
info "Build complete."
|
||||||
|
|
||||||
|
# ── flash ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "Flash to device"
|
||||||
|
|
||||||
|
prompt "Flash firmware to device now? [y/N] "
|
||||||
|
read -r DO_FLASH
|
||||||
|
|
||||||
|
if [[ "$DO_FLASH" =~ ^[Yy]$ ]]; then
|
||||||
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
|
PORT_HINT="/dev/cu.usbserial-* or /dev/cu.SLAB_USBtoUART"
|
||||||
|
else
|
||||||
|
PORT_HINT="/dev/ttyUSB0 or /dev/ttyACM0"
|
||||||
|
fi
|
||||||
|
prompt "Serial port (e.g. $PORT_HINT, leave blank to auto-detect): "
|
||||||
|
read -r SERIAL_PORT
|
||||||
|
|
||||||
|
if [[ -n "$SERIAL_PORT" ]]; then
|
||||||
|
idf.py -p "$SERIAL_PORT" flash monitor
|
||||||
|
else
|
||||||
|
idf.py flash monitor
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── SD card setup ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "SD card setup"
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
|
SD_HINT="/Volumes/KIDSBOX"
|
||||||
|
else
|
||||||
|
SD_HINT="/media/\$USER/KIDSBOX"
|
||||||
|
fi
|
||||||
|
prompt "Mount path of SD card (e.g. $SD_HINT, leave blank to skip): "
|
||||||
|
read -r SD_MOUNT
|
||||||
|
|
||||||
|
if [[ -n "$SD_MOUNT" ]]; then
|
||||||
|
[[ -d "$SD_MOUNT" ]] || die "Mount path not found: $SD_MOUNT"
|
||||||
|
|
||||||
|
info "Creating directory structure on SD card"
|
||||||
|
mkdir -p "$SD_MOUNT/www"
|
||||||
|
mkdir -p "$SD_MOUNT/internal"
|
||||||
|
mkdir -p "$SD_MOUNT/cache"
|
||||||
|
|
||||||
|
info "Copying web interface"
|
||||||
|
cp -r "$WWW_DIR/." "$SD_MOUNT/www/"
|
||||||
|
|
||||||
|
info "SD card contents:"
|
||||||
|
ls "$SD_MOUNT/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " SD card is ready."
|
||||||
|
echo " To configure a tag: create a file at $SD_MOUNT/internal/<tag-id>"
|
||||||
|
echo " containing either:"
|
||||||
|
echo " - An http:// URL (will be streamed and cached)"
|
||||||
|
echo " - A local path (e.g. /sdcard/music/song.mp3)"
|
||||||
|
echo " Tag IDs are shown in the serial monitor when a tag is scanned."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── done ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
heading "Done!"
|
||||||
|
echo ""
|
||||||
|
echo " Once running, open http://<device-ip> in your browser."
|
||||||
|
echo ""
|
||||||