# Run modes and memory

Every project row has an **N / T / S** chip. Click to cycle; the choice is saved per file (`config.json` `modlar`). Default **N**. The selected file's bar: **Save · N|T|S · Run/Stop (one button) · Delete**; it turns into Stop while running and "Start solo" when S is selected.

## The three modes

| Mode | How it runs | Heap | Internal DRAM | For |
|---|---|---|---|---|
| **N Normal** | asyncio task on one shared thread | 2–15 KB | **0** (shared worker 17 KB once) | LEDs, sensors, small pages: most projects |
| **T Thread** | Its own FreeRTOS thread, sliced by the GIL (ESP32 only) | same | 10–15 KB | Display drawing, tight loops without sleep, heavy compute |
| **S Solo** | Panel off, board reboots, single project | all | all | BLE, HTTPS |

**How normal mode works:** the project file is wrapped line by line into an `async def`, top-level names are declared `global`, `cyd.sleep(` → `await`, `time.sleep(` → `await`. The result is streamed to a temp file and compiled from the file with `execfile`; a 12 KB source never sits in the heap as a string. If a sleep call is inside a `def` body the transform does not apply (MicroPython silently turns a plain `def` containing `await` into a generator and the body never runs); a pre-scan catches that and the project **opens as T by itself**, the log says why. If you write `async def` yourself it is left alone and the `main()` coroutine is run.

**Solo** is one-shot: `main.py` deletes the flag first thing; pressing RESET brings the panel back. Panel: select the file → S → Run → WiFi on/off question. API: `POST /api/solo?name=<file>&wifi=0|1`.

## Two memories

The ESP32 has two separate pockets, and the second one is the critical one:

| | |
|---|---|
| MicroPython heap | Project data, `gc.mem_free()` |
| **Internal DRAM** | WiFi, sockets, thread stacks, BLE, TLS. `cyd.dram()` |

`gc.mem_free()` can look generous while a thread cannot be created or BLE/TLS returns `ENOMEM`. Measure with the panel's system box or `cyd.dram()`.

| CYD | Stock v1.28.0 | **CYD Studio firmware** |
|---|---|---|
| Internal DRAM free, panel up | 27.8 KB | **55 KB** |
| Python heap free | 80 / 137 (auto-grows, swallows DRAM) | 75 / 94 (**fixed 96 KB**) |
| Concurrent projects | 1 (2nd: `can't create thread`) | 7 normal + 2–3 threads |

What the firmware changed: 12 core modules frozen into flash; the heap is fixed (auto-doubling dropped DRAM from 76 to 12 KB with two small projects); smaller lwIP/WiFi buffers; live log ring 250 → 100 lines (250 lines held 20.7 KB and fragmented the heap); server socket timeout 5 seconds.

Thread stacks come out of internal DRAM: 10–15 KB per project. A project drawing on the display takes 25–33 KB of heap (returned on stop). No leak: same project started/stopped 4× → 65.7 / 65.7 / 65.9 / 65.8 KB.

## Measured: projects side by side (CYD)

| Started in order | From heap | From DRAM | Running |
|---|---|---|---|
| yuk_kontrol (12 KB source + `/yuk` routes) | 12.2 KB | **17.3 KB** (shared worker, once) | 1 |
| led_yakip_sondur | 2.2 | 0 | 2 |
| gece_lambasi | 8.2 | 0.3 | 3 |
| ornek_led | 4.7 | 0 | 4 |
| isik_olcum | 7.7 | 0 | 5 |
| harici_led | 7.1 | 0 | 7 |
| ornek_isik_sensoru (draws on the display) | **33 KB** → heap 1 KB | 0 | 8 |

Seven small projects side by side with internal RAM flat at 37 KB; the limit is now the heap. If you need a bigger heap: `frozen_esp32_build.py --heap 112` (DRAM 55 → 39 KB, still room for 2 threads).

## Truly side by side?

A "button poller" project looping every 10 ms; longest gap between turns over 15 seconds:

| Scenario | average | max | gaps >100 ms |
|---|---|---|---|
| Alone, normal | 12.9 ms | 21 ms | 0 |
| + 4 normal projects (LED/LDR) | 15.9 | 352 | 5 |
| + a display-drawing project (normal) | 25.5 | **973** | 33 |
| + a thread doing 90% CPU math | 211 | **3405** | 5 |
| Same load, poller in **T** | 12.0 | **37** | 0 |

Reading: next to LED/sensor neighbours normal mode really alternates. **A display-drawing project is a bad neighbour in normal mode** (an SPI fill holds the single thread for ~1 s): make it T. **A thread that never yields the CPU** starves the normal worker: put `cyd.sleep(0.001)` in the heavy loop or make the input project T. For buttons the real answer is `cyd.buton` (interrupt), not polling.

## Stopping and the watchdog

Threads do not isolate: MicroPython has a GIL, one thread runs bytecode at a time. A project that never yields the CPU starves the panel. The protection:

1. The server writes a heartbeat every turn; the listening socket has a timeout so `accept()` cannot stall it.
2. When the first job starts a watchdog thread checks the heartbeat every 2 seconds.
3. If the panel misses heartbeats for 20 seconds, running projects get the stop flag.
4. If a project does not stop within 4 seconds, `machine.reset()`. The panel always comes back; verified: a choking project was stopped at second 28 and the panel returned to 302 ms.

## On the ESP8266

There is no `_thread`. N = a task in the server's asyncio loop (the T chip is hidden), S is available. Measured: 3 normal projects side by side, heap 19 → 9 KB (2.5–5 KB each), panel max 0.69 s. Practical limit 3 projects. For a project that never calls `cyd.sleep()` a timer pre-emptor checks every 100 ms and, if the pump has not run for 400 ms, slips a server turn between two bytecodes; with `while True: x += 1` running, 6/6 status queries were answered. If a stop request is ignored for 4.5 seconds the log gets `!!! yanit vermiyor` and the board resets (the panel is back in ~10 s).
