# Writing projects

Projects live on the board under `/projects` (`/sd/projects` when an SD card is present). Each project is a single `.py` file that reaches the board's hardware and runtime through `import cyd`. The same file runs on all three boards; hardware the board does not have raises a clear error instead of silently misbehaving.

## Skeleton

```python
import cyd
from machine import Pin

led = Pin(getattr(cyd, 'D1', 22), Pin.OUT, value=0)   # NodeMCU: D1, CYD: GPIO22

try:
    while not cyd.should_stop():      # listen to the panel's Stop button
        led.value(1)
        if not cyd.sleep(0.5): break  # interruptible wait (seconds)
        led.value(0)
        if not cyd.sleep(0.5): break
finally:
    led.value(0)                      # clean up however the project ends
```

## The stop contract

- `while not cyd.should_stop()`, never `while True`. `should_stop()` is also injected as a global.
- `cyd.sleep(seconds)`, never `time.sleep`: it listens for Stop while waiting, returns `False` when interrupted, and yields to neighbouring projects. In normal mode it is rewritten to `await` automatically.
- A project that listens exits cleanly within 3 seconds. A normal task that ignores Stop is cancelled (`finally` runs). A project that never waits locks the shared worker; after 5 seconds the board resets (the panel keeps answering meanwhile). In a thread: reset if it is the only one running, otherwise flagged "stuck" with a force button.
- `print()` streams to the panel and to `/logs/<project>.log`; it survives solo mode too.

## The `cyd` module

| Call | What it does |
|---|---|
| `cyd.should_stop()` · `cyd.sleep(s)` | The stop contract |
| `cyd.display(rot=None, bgr=None)` | Display (created lazily; `fill`, `fill_rect`, `text(..., scale)`, `circle`). Same API on the OLED, colours collapse to on/off |
| `cyd.touch()` | `.touch()` → `(x, y)` or `None`, `.pressed()`, `.raw()` |
| `cyd.set_display(rot, bgr)` · `cyd.test_pattern()` | Orientation |
| `cyd.led(r, g, b)` · `cyd.led_off()` | On-board RGB LED (inverted logic) |
| `cyd.light()` | LDR, 0–4095 (higher = darker) |
| `cyd.beep(freq, ms)` | Speaker (GPIO 26) |
| `cyd.buton(pin, fn=None, aktif=0, sekme_ms=40)` | Interrupt-driven button: `.basildi()` (consumes), `.sayac` (count), `.basili()` (held); the IRQ is released when the project ends |
| `cyd.rota('/path', fn)` · `cyd.rota_sil()` · `cyd.rotalar()` | The project's web page on the main server |
| `cyd.dram()` | Free internal DRAM in bytes; check before BLE/TLS |
| `cyd.wifi()` | The connected STA interface (use it, never disable it) |
| `cyd.mount_sd()` | Mounts the SD card at `/sd`, returns the path or `None` |
| `cyd.claim_display()` | `(True, None)` or `(False, 'owner project')` |
| `cyd.job_name()` | This project's name |
| `cyd.D0` … `cyd.D8`, `cyd.A0` | NodeMCU pin labels |

## The project's own web page

Do not open a second HTTP server. For every request outside `/api` the main server first checks the routes registered by projects: one socket, one loop, same port.

```python
import cyd

count = 0

def page(req):                    # req: {'yontem', 'yol', 'sorgu', 'govde'}
    global count
    count += 1
    return '<h1>hello %d</h1>' % count   # str -> html, dict/list -> json,
                                         # bytes -> raw, (status, type, body) -> custom
cyd.rota('/hello', page)

while not cyd.should_stop():
    cyd.sleep(1)
```

The handler runs on the server's thread: keep it short, never `cyd.sleep` inside it, keep shared state in plain variables. Routes disappear when the project stops. They show up as links in the panel's **Apps** row. Sample: `yuk_kontrol.py` (`http://<ip>/yuk`).

## Buttons: interrupts, not polling

```python
import cyd

b = cyd.buton(22)                 # GPIO22 <-> GND
while not cyd.should_stop():
    if b.basildi():
        print('pressed', b.sayac)
    cyd.sleep(0.05)
```

Measured (50 simulated presses next to a display project and a heavy thread): 10 ms polling caught 5/50, the interrupt **50/50**.

## Don'ts

- **No second HTTP server.** A second listening socket drained internal DRAM through lwIP buffers and froze the panel (measured). Use `cyd.rota`.
- **Never call `wlan.active(False)` / `wlan.disconnect()`.** It takes the panel down too.
- **Never call `machine.reset()`.** `jobs` already manages that.
- **No endless loops without sleep.** In normal mode they stall the neighbours; in a thread Stop resets the board.
- **Do not draw on the display from two projects.** Data is not corrupted (there is an SPI lock) but they draw over each other; `cyd.claim_display()` declares ownership.

## Network

| Method | Normal mode | Solo mode |
|---|---|---|
| `requests.get('http://...')` | ✅ | ✅ |
| `requests.get('https://...')` | ❌ ENOMEM | ✅ |
| Raw `socket` | ✅ | ✅ |
| `ntptime.settime()` | ✅ | ✅ |
| BLE (`bluetooth`) | ❌ EIO | ✅ |

`requests` buffers the body in RAM; send large files in chunks over a raw socket (`ornek_sunucuya_yukle.py`, 9690 bytes verified with 22 KB of DRAM). Not available: camera module, WireGuard, Node.js / C#.

## On the NodeMCU

RAM is tight (~24 KB with the panel up). Avoid long `print` strings; ship big libraries as precompiled `.mpy` (`mpy-cross bme280.py`), because importing the `.py` source needs a large temporary block for parsing and raises `MemoryError`.
