services_hub

Example of the component espressif/adf_examples v0.2.0
# Services Hub Example

This example exercises `esp_service` with a production-style multi-service setup on real hardware. It wires together `wifi_service`, `esp_ota_service`, `esp_cli_service`, and `esp_button_service` through a shared `adf_event_hub` and an `esp_service_manager`, and optionally exposes service tools over MCP.

## Services Included

| Service | Type | Description |
|---------|------|-------------|
| `wifi_service` | Real | ESP-IDF Wi-Fi station with connect/disconnect events |
| `esp_ota_service` | Real | HTTP OTA with version check; wires `esp_ota_service_source_http_create()` + `esp_ota_service_target_app_create()` + `esp_ota_service_checker_app_create()` (`require_higher_version = true`) and skips body verification (`.verifier = NULL`) |
| `esp_cli_service` | Real | ESP console CLI; auto-discovers registered services and their commands |
| `esp_button_service` | Real | GPIO + ADC button input; long-press triggers provisioning mode |

## Optional MCP Integration

`CONFIG_ESP_MCP_ENABLE=y` is on by default, so `wifi_service_mcp` and `esp_ota_service_mcp` register their tools with the service manager. The shipped `sdkconfig.defaults` enables only the **STDIO** transport (`CONFIG_ESP_MCP_TRANSPORT_STDIO=y`); enable any of `CONFIG_ESP_MCP_TRANSPORT_{HTTP,SSE,WS,UART,SDIO}` in `menuconfig` if you need the tools exposed elsewhere.

## Build and Flash

Board selection is owned by `esp_board_manager`. On first checkout, generate
the `components/gen_bmgr_codes/` component for your board, then build:

```bash
cd adf_examples/services_hub
idf.py bmgr -b esp32_s3_korvo2_v3   # selects target + generates board code
idf.py menuconfig                   # Wi-Fi SSID/password + OTA URL (see below)
idf.py build flash monitor
```

Replace `esp32_s3_korvo2_v3` with any board you have YAML for; the `bmgr`
assistant also picks the IDF target (do **not** run `idf.py set-target`).

## HTTP OTA Server

The example reuses the shared tooling under
`components/esp_ota_service/tools/`, so no per-example server script is needed.
Two scripts drive the flow:

| Script | Purpose |
|--------|---------|
| `build_firmware.py` | Writes `version.txt`, runs `idf.py build`, and copies `build/services_hub.bin` to `firmware_samples/services_hub_v<ver>.bin` while refreshing `manifest.json`. |
| `ota_http_serve.py` | Stands up a tiny HTTP server on `0.0.0.0:18070` that returns the newest `services_hub_v*.bin` for `GET /firmware.bin` and the JSON metadata for `GET /manifest.json`. |

### 1. Flash the baseline (v1.0.0)

The URL is composed at boot from `CONFIG_PROD_OTA_HTTP_HOST` +
`CONFIG_PROD_OTA_HTTP_PORT` + `CONFIG_PROD_OTA_HTTP_PATH`, so set the host IP
(and optionally port/path) in `menuconfig` **before** building. The host must
be reachable from the Wi-Fi SSID you configured.

```bash
cd components/esp_ota_service/tools
./build_firmware.py services_hub -v 1.0.0      # build + publish baseline

cd ../../../adf_examples/services_hub
idf.py -p /dev/ttyUSB0 flash                   # flash baseline only; do not flash again
```

### 2. Publish a newer image (v1.0.1) without touching the device

```bash
cd components/esp_ota_service/tools
./build_firmware.py services_hub -v 1.0.1      # rebuild + publish services_hub_v1.0.1.bin
ls firmware_samples/services_hub_v1.0.*.bin
cat firmware_samples/manifest.json
```

> Do not run `idf.py flash` after this step, otherwise the device also runs
> v1.0.1 and has nothing to upgrade to.

### 3. Start the HTTP server and observe the upgrade

```bash
./ota_http_serve.py --example services_hub --update-manifest-url
#   Serving services_hub_v1.0.1.bin (1392.3 KiB)
#   URL      http://<host_ip>:18070/firmware.bin
#   Manifest http://<host_ip>:18070/manifest.json
```

Open a second terminal with `idf.py -p /dev/ttyUSB0 monitor`, then press the
device **RST** button. A successful run prints:

```
Wi-Fi connected: <ip>
OTA_SRC_HTTP: HTTP opened: http://<host_ip>:18070/firmware.bin  Content-Length: 1425696
OTA_VER_UTIL: Current firmware: 1.0.0  Incoming firmware: 1.0.1
OTA_CHK_APP: upgrade_available=true
OTA_TGT_APP: Writing to partition: ota_1  offset 0x00210000
OTA_SERVICE: [0] progress: ... / 1425696 bytes
ESP_GMF_HTTP: No more data, ret: 0
esp_image: segment 0..5 (verified twice)
OTA_TGT_APP: Boot partition set to: ota_1
```

After the automatic reboot the banner shows `App version: 1.0.1`, loaded from
`offset 0x210000` (ota_1). Subsequent checks print
`Current firmware: 1.0.1  Incoming firmware: 1.0.1` and skip the upgrade.

### Useful flags

- `./ota_http_serve.py --port 8080` — change listening port.
- `./ota_http_serve.py --bin services_hub_v1.0.0.bin` — serve a specific bin
  instead of the newest for the example.
- `./build_firmware.py services_hub --skip-build` — only re-publish the bin
  already in `build/`, without rebuilding.

### Caveats

- `CONFIG_PROD_OTA_HTTP_{HOST,PORT,PATH}` are composed into the URL at boot
  and baked into the running firmware. Changing any of them requires another
  `idf.py flash`; they are not pushed via OTA itself.
- `CONFIG_PROD_OTA_TIMEOUT_MS` defaults to 120 s. For slow networks or when
  the in-app CLI/MCP smoke loop competes for CPU, bump it (up to 600 000).
- On Linux hosts with `ufw` enabled, allow the port:
  `sudo ufw allow 18070/tcp`.

## Configuration (menuconfig)

Under **Services Hub Example** (all consumed by `main/app_main.c`):

| Option | Default | Description |
|--------|---------|-------------|
| `PROD_WIFI_SSID` | `""` | Wi-Fi SSID the example connects to on boot; leave empty to skip Wi-Fi start-up. Set per-developer in `menuconfig`; never commit concrete credentials |
| `PROD_WIFI_PASSWORD` | `""` | Password for `PROD_WIFI_SSID`; empty = open network. Same guidance as SSID |
| `PROD_OTA_HTTP_HOST` | `0.0.0.0` | Host portion of the firmware URL; override with the IP printed by `ota_http_serve.py` |
| `PROD_OTA_HTTP_PORT` | 18070 | TCP port `ota_http_serve.py` listens on; pass the same value via `--port` if you change it |
| `PROD_OTA_HTTP_PATH` | `/firmware.bin` | URL path served by the script; change only if a reverse proxy remaps it |
| `PROD_OTA_TIMEOUT_MS` | 120000 | Upper bound `app_main` waits for the OTA upgrade to finish; raise for slow networks |

The three HTTP fields are composed at boot into `http://<host>:<port><path>` and
printed in the startup banner (`OTA URL: ...`). Changing any of them still
requires a reflash, since the URL is built from compile-time Kconfig values.

Button GPIO/ADC pins come from the board-manager YAML (`idf.py bmgr -b <board>`), not from Kconfig.

To create a project from this example, run:

idf.py create-project-from-example "espressif/adf_examples=0.2.0:services_hub"

or download archive (~14.00 KB)