# Boards and firmware

The system is the same system on every board. Differences are data, not code: each board's `boards/<name>/cyd.py` declares an `OZELLIKLER` (features) dict, `runtime.ozellikler()` merges it with the measured core capabilities (`threads`, `max_jobs`, `platform`), `GET /api/caps` publishes it and **the panel hides unsupported buttons**.

```python
OZELLIKLER = {
    'display': True, 'touch': True, 'sd': True, 'rgb_led': True,
    'light': True, 'speaker': True, 'ble': True, 'tls': True,
    'wifi_config': True, 'docs': True, 'lite_ui': False,
}
```

| Feature | CYD | OLED | NodeMCU |
|---|---|---|---|
| display / touch / sd | ✅ ✅ ✅ | ✅ ❌ ❌ | ❌ ❌ ❌ |
| rgb_led / light / speaker | ✅ ✅ ✅ | ❌ ❌ ❌ | ✅ ✅ ❌ |
| ble / tls (in solo mode) | ✅ ✅ | ✅ ✅ | ❌ ❌ |
| wifi_config / docs | ✅ ✅ | ✅ ✅ | ❌ ❌ |
| lite_ui | ❌ | ❌ | ✅ |
| threads / max_jobs | ✅ 8 (threads 4) | ✅ 4 | ❌ 3 |

## CYD (ESP32-2432S028R)

The "Cheap Yellow Display": 320×240 ILI9341 display, XPT2046 touch, SD slot, RGB LED, LDR, speaker, CH340 USB-serial. **Custom firmware** (`micropython-esp32-cydstudio.bin`, a `CYD_STUDIO` board based on v1.28.0):

- 12 core modules frozen into flash: `runtime jobs sysinfo logstore settings netmgr store server` + `cyd ili9341 xpt2046 sdcard`. `boot.py`, `main.py`, `index.html` and the docs stay on the filesystem.
- **Fixed heap** of 96 KB, auto-growth off.
- RAM diet (`sdkconfig.free_ram`): TCP window 5760 → 3072, WiFi buffers 10/32/32 → 8/12/12, sockets 16 → 6.
- Result: 55 KB of internal DRAM idle (27.8 on stock), 7 normal + 2–3 thread projects.

The panel is natively landscape and RGB-ordered: the verified setting is `rotation=0`, `bgr=False`, 320×240. Display MISO is not wired, so nothing can be read back; orientation is set by eye (Settings → Display, live). SD only on VSPI (slot 3); slot 2 is the display's bus.

Rebuild (WSL, ESP-IDF v5.5.1): `python tools/frozen_esp32_build.py [--heap 96]`, ~3 minutes. Flash: `tools/flash.py COM3 firmware/micropython-esp32-cydstudio.bin --koru` (BOOT-held procedure). Deploy: `tools/deploy.py --board cyd --frozen --samples --reset`.

## NodeMCU (ESP8266)

On-board LED, one analog input (`cyd.A0`), 38 KB heap, no `_thread`. **Custom firmware** (`micropython-esp8266-frozen.bin`): core + lite server (`boards/nodemcu/server.py`) + `ssd1306` frozen, asyncio included.

| | Stock | Frozen |
|---|---|---|
| Panel mode, fresh boot | 4,784 B | **24,704 B** |
| After ~5 minutes | 2,512 B | **24,672 B** (no fragmentation) |

On stock even a 3 KB project hit `MemoryError` at the `compile()` line; freezing keeps the bytecode in flash. Pin labels `cyd.D0`…`cyd.D8`: `D1`/`D2` (GPIO5/4) are easy I/O and the standard I2C pair, `D4` (GPIO2) is the on-board blue LED (inverted), `D3`/`D8`/`D0` have boot duties, avoid them.

Lite panel (`boards/nodemcu/index.html`, ~31 KB): search, ▶/■/✕ per row, Console | Saved log, Settings, Docs, TR/EN, shortcuts. Build: `tools/frozen_esp8266_build.py`; flash `tools/flash_esp8266.py COM3 --koru` (no button); deploy `tools/deploy_mpy.py --board nodemcu --frozen --samples --reset`.

## ESP32 OLED

128×64 SSD1306 OLED, no touch or SD, CP2102 USB-serial. Stock MicroPython v1.28; the core goes to the filesystem (`deploy.py --board oled --samples`). The `Ekran` class offers the same API as the ILI9341 with colours collapsing to on/off; CYD projects run unchanged. I2C pins vary by model, `_oled_bul()` tries the candidate pairs `(5,4) (4,15) (21,22) (16,17) (13,14)` in turn. `cyd.touch()`, `cyd.light()`, `cyd.beep()` raise a clear error.

> **Warning:** powered from a PC USB port this board's 3.3 V rail collapses the moment WiFi starts (brownout loop). It cannot be fixed in software; bare MicroPython dies on the same `wlan.active(True)` line. Fix: a LiPo on the battery connector, a powered hub, or an external fixed 3.3 V supply. On external 3.3 V it ran fine at 240 MHz and full TX power.

## Adding a board

Put a `cyd.py` (an `OZELLIKLER` dict + re-exports of `should_stop`, `sleep`, `rota`, `dram`, `wifi`, `buton` from `runtime`) and a `config.example.json` under `boards/<name>/`; drivers if needed. The core does not need to change. When adding a feature: add the key to `runtime._VARSAYILAN`, enable it in the relevant boards' `OZELLIKLER`, check `caps.<key>` in the panel.

## Shadowing

`sys.path = ['', '.frozen', '/lib', '/projects']`: a same-named `.py` on the filesystem overrides the frozen one. `mpremote fs cp x.py :x.py` is fine for a quick single-file experiment, but every shadow costs ~10 KB of heap and measurements are misleading in that state. A core change means a firmware rebuild; clean up with `--frozen` afterwards.
