Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions DEVICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ parameters, certificate renewal.
| Parameters | ✅ | ✅ | ✅ | ✅ | ✅ |
| Certificate renewal | ✅ | ✅ | ✅ | ✅ | ✅ |
| Shell command | ✅ | ✅ | ✅ | ❌ | ❌ |
| Identify from the cloud (`tedge identify`, needs the shell command) | ✅ | ✅ | ✅ | ❌ | ❌ |
| Identify from the button (2 presses → `zephyr_Identify` event) | ✅ | ✅ | ✅ | ✅ (IO0 on the CAM-MB base) | ✅ |
| Log upload | ✅ | ✅ | ✅ | ✅ | ❌ |
| Crash dumps | ✅ | ✅ | ✅ | ❌ | ❌ |
| **Remote access** | ✅ | ✅ | ✅ | ❌ | ❌ |
Expand Down Expand Up @@ -238,8 +240,8 @@ Telemetry was published throughout, but the test tenant had no mapping for

`lib/common/tedge-boards/extras/shell-diagnostics.conf` adds the cloud shell
command (allow-list: `kernel uptime`, `kernel version`, `net iface`,
`net conn`, `wifi status`, `tedge params list`, `tedge diag`; `help` lists
them, one per line). It costs about 16 KB of internal RAM.
`net conn`, `wifi status`, `tedge params list`, `tedge diag`, and since
2026-10-06 `tedge identify`; `help` lists them, one per line). It costs about 16 KB of internal RAM.

| Board | Build | RAM | Result |
|---|---|---|---|
Expand All @@ -261,6 +263,70 @@ Test note: a TCP probe of a local `c8y remoteaccess server` port opens a
tunnel of its own, which holds the device's single session for a moment; an
SSH connection straight after it can find the slot busy.

### Identify measured (2026-10-06)

`device-identify` adds `tedge identify [seconds]` (cloud shell) and the sw0
double press (`zephyr_Identify` event) to every `tedge-*` image, and moves the
sw0 watcher out of the provisioning hand-off. Measured against `main` built
the same way, it costs **+463 B image / +32 B dram0** on the S3-DevKitC
modbus `tedge-full` + shell build and **+336 B / +24 B** on the WROOM agent
`tedge-ota` build. The table below is the new `release/size-baseline.json`
against the one before it, which also takes in #10 (shell results keep their
lines) and the 0.7.x version strings; the standalone images, which only get
the button refactor, moved by under 35 B.

Runs (eu-latest, all app-only flashes keeping Wi-Fi and identity):

| Board | Build | `tedge identify` (c8y_Command) | sw0 double press |
|---|---|---|---|
| S3-DevKitC | modbus full + shell | ✅ default 30 s, `10`, `3600` → 300 s; `0`/`soon` fail with the reason | ✅ event in ~2 s, LED ack |
| QT Py S3 | modbus full + shell | ✅ same cases | ✅ event, LED ack |
| ESP32-C6 | modbus full (no shell) | not advertised; sent anyway it fails "no handler (511)" | ✅ event, LED ack |
| ESP32-CAM | snmp ota (no shell) | not advertised | ✅ event (IO0 on the CAM-MB base), GPIO33 LED ack |

The shell-diagnostics builds above still pass their size gate with the
subcommand on the allow-list (S3/QT Py modbus and agent full: +465 to
+480 B, +176 B dram0, 56–65 KB arena left).

| Build | Δ image | Δ RAM (fullest region) | Arena after |
|---|---|---|---|
| `modbus-server-standalone-esp32-devkitc` | +32 | +8 (dram0_0_seg) | 99,440 |
| `modbus-server-standalone-esp32c6-devkitc` | +16 | +8 (sram0_0_seg) | 303,648 |
| `modbus-server-standalone-esp32s3-devkitc` | +17 | +8 (dram0_0_seg) | 202,980 |
| `modbus-server-standalone-qtpy-esp32s3` | +15 | +8 (dram0_0_seg) | 205,580 |
| `modbus-server-tedge-full-esp32c6-devkitc` | +225 | +176 (sram0_0_seg) | 76,896 |
| `modbus-server-tedge-full-esp32s3-devkitc` | +480 | +176 (dram0_0_seg) | 56,076 |
| `modbus-server-tedge-full-qtpy-esp32s3` | +465 | +176 (dram0_0_seg) | 59,684 |
| `modbus-server-tedge-ota-esp32-cam` | +401 | +168 (dram0_0_seg) | 51,560 |
| `modbus-server-tedge-ota-esp32-devkitc` | +433 | +24 (dram0_0_seg) | 15,096 |
| `modbus-server-tedge-ota-esp32c6-devkitc` | +209 | +32 (sram0_0_seg) | 110,656 |
| `modbus-server-tedge-ota-esp32s3-devkitc` | +209 | +24 (dram0_0_seg) | 105,260 |
| `modbus-server-tedge-ota-qtpy-esp32s3` | +208 | +24 (dram0_0_seg) | 108,868 |
| `opcua-server-standalone-esp32-devkitc` | +34 | +8 (dram0_0_seg) | 95,104 |
| `opcua-server-standalone-esp32c6-devkitc` | +15 | +8 (sram0_0_seg) | 279,584 |
| `opcua-server-standalone-esp32s3-devkitc` | +16 | +8 (dram0_0_seg) | 179,620 |
| `opcua-server-standalone-qtpy-esp32s3` | +17 | +8 (dram0_0_seg) | 182,220 |
| `opcua-server-tedge-ota-qtpy-esp32s3` | +207 | +24 (dram0_0_seg) | 91,988 |
| `snmp-agent-standalone-esp32-devkitc` | +31 | +8 (dram0_0_seg) | 90,992 |
| `snmp-agent-standalone-esp32c6-devkitc` | +16 | +8 (sram0_0_seg) | 297,088 |
| `snmp-agent-standalone-esp32s3-devkitc` | +16 | +8 (dram0_0_seg) | 196,388 |
| `snmp-agent-standalone-qtpy-esp32s3` | +0 | +8 (dram0_0_seg) | 198,988 |
| `snmp-agent-tedge-full-esp32c6-devkitc` | +225 | +176 (sram0_0_seg) | 64,608 |
| `snmp-agent-tedge-full-esp32s3-devkitc` | +208 | +168 (dram0_0_seg) | 59,148 |
| `snmp-agent-tedge-full-qtpy-esp32s3` | +209 | +168 (dram0_0_seg) | 62,756 |
| `snmp-agent-tedge-ota-esp32-cam` | +416 | +168 (dram0_0_seg) | 43,520 |
| `snmp-agent-tedge-ota-esp32c6-devkitc` | +224 | +32 (sram0_0_seg) | 98,352 |
| `snmp-agent-tedge-ota-esp32s3-devkitc` | +208 | +24 (dram0_0_seg) | 93,092 |
| `snmp-agent-tedge-ota-qtpy-esp32s3` | +208 | +32 (dram0_0_seg) | 96,692 |
| `tedge-agent-tedge-full-esp32c6-devkitc` | +223 | +176 (sram0_0_seg) | 81,568 |
| `tedge-agent-tedge-full-esp32s3-devkitc` | +480 | +176 (dram0_0_seg) | 61,244 |
| `tedge-agent-tedge-full-qtpy-esp32s3` | +480 | +176 (dram0_0_seg) | 64,852 |
| `tedge-agent-tedge-ota-esp32-cam` | +416 | +168 (dram0_0_seg) | 53,064 |
| `tedge-agent-tedge-ota-esp32-devkitc` | +432 | +168 (dram0_0_seg) | 16,600 |
| `tedge-agent-tedge-ota-esp32c6-devkitc` | +208 | +32 (sram0_0_seg) | 115,328 |
| `tedge-agent-tedge-ota-esp32s3-devkitc` | +224 | +24 (dram0_0_seg) | 109,788 |
| `tedge-agent-tedge-ota-qtpy-esp32s3` | +211 | +24 (dram0_0_seg) | 113,396 |

### Static sizes after the footprint work, link only (2026-09-22)

Every `release/devices.yml` build, rebuilt from the `reduce-memory-footprint`
Expand Down
48 changes: 42 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,8 @@ it through reset enters the ROM serial bootloader instead):
|---|---|
| **3 quick presses** within 2 s | Reboot into the provisioner. The stored network is kept until new credentials are verified, so an abandoned attempt returns to it when the window expires. |
| **Hold ≥ 10 s**, then release | Erase the stored credentials and reboot into the provisioner. The LED flickers once the erase is armed, so you know before letting go. |
| anything else | Nothing (1, 2 or 4+ presses, presses too slow, a shorter hold). |
| **2 quick presses** (`tedge-*` images) | Identify: a `zephyr_Identify` event goes to the cloud and the LED fast-blinks for 1 s. The device keeps serving. See [Identifying a device](#identifying-a-device). |
| anything else | Nothing (1 or 4+ presses, presses too slow, a shorter hold). |
| a press in the provisioner | Restarts an expired window; authorizes provisioning when `CONFIG_APP_WIFI_PROV_REQUIRE_AUTH=y`. |

**Provisioning window:** the provisioner advertises for
Expand All @@ -556,19 +557,22 @@ S3-DevKitC-1 and the QT Py S3, which also shows the colour):
| steady on | green | connected and serving |
| even blink, 250 ms on / 250 ms off | amber | not connected (associating, reconnecting) |
| **two short blinks, then a pause** (100 ms on, 150 off, 100 on, ~1.5 s off) | blue | provisioner, waiting for credentials |
| fast 5 Hz blink | white | identify request from a client, or the button pattern was recognized (just before the reboot) |
| fast 5 Hz blink | white | identify request (from the cloud with `tedge identify`, or from an Improv client), or a button pattern was recognized |
| very fast flicker | red | erase hold armed — release to erase |
| off | — | provisioning window expired; press the button to advertise again |

The application also logs every
button press and release with its length (`app_prov: sw0 released after
button press and release with its length (`app_button: sw0 released after
140 ms`), then what it made of the sequence ("not a gesture, ignored", "Button
pattern: …", "Erase armed: …"), so you can check a gesture on the console.
pattern: …", "Erase armed: …", "Identify pattern: …"), so you can check a
gesture on the console.

**Options.** Application (`lib/common/Kconfig`): `APP_PROV_HANDOFF` (on by
default in an MCUboot build with the provisioner layout),
`APP_WIFI_PROV_PRESS_COUNT` (3), `APP_WIFI_PROV_PRESS_WINDOW_MS` (2000),
`APP_WIFI_PROV_ERASE_HOLD_S` (10). Provisioner (`apps/wifi-provisioner/Kconfig`):
`APP_WIFI_PROV_ERASE_HOLD_S` (10), and for identify `APP_IDENTIFY` (on in
`tedge-*` images), `APP_IDENTIFY_PRESS_COUNT` (2, must differ from the
provisioning count), `APP_IDENTIFY_DURATION_S` (30). Provisioner (`apps/wifi-provisioner/Kconfig`):
`APP_WIFI_PROV_WINDOW_S` (900), `APP_WIFI_PROV_CONNECT_TIMEOUT_S` (30),
`APP_WIFI_PROV_REQUIRE_AUTH` (off); set them for the provisioner image with a
sysbuild image prefix, e.g. `-Dwifi-provisioner_CONFIG_APP_WIFI_PROV_REQUIRE_AUTH=y`.
Expand Down Expand Up @@ -1261,7 +1265,7 @@ health, on thin-edge.io topics over the Cumulocity MQTT Service. Cumulocity
only turns them into measurements where a **Smart Function** maps them; a
tenant without one shows the device connected, with firmware, restart and
remote access working, and no measurements. [`cumulocity/`](cumulocity/README.md)
has the Smart Functions (measurements, twin state, firmware progress), the
has the Smart Functions (measurements, twin state, firmware progress, events), the
DTM definitions behind the Parameters tab, and the shell diagnostics — what a
tenant needs, ready to deploy.

Expand Down Expand Up @@ -1339,6 +1343,38 @@ through the app's parameters, and on the Modbus app also the type
(`measurement_type`): a mapping keyed on the topic's last segment keeps
working when they do.

## Identifying a device

With several identical boards on a bench, the cloud's device list does not
say which board is which. Every `tedge-*` image (`CONFIG_APP_IDENTIFY`) can be
matched both ways.

**From the cloud: which board is this device?** Run `tedge identify` as a
shell command (Device Management, the device's **Shell** tab). The status LED
fast-blinks (white on an RGB LED) for 30 s, or for `tedge identify <seconds>`
(at most 300), then goes back to its normal pattern. The operation succeeds as
soon as the blinking starts. From the command line:

```sh
c8y operations create --device <device> \
--template "{c8y_Command:{text:'tedge identify 60'}}"
```

This needs an image with the cloud shell command, which today means the
images built with `lib/common/tedge-boards/extras/shell-diagnostics.conf`
(the S3-DevKitC and QT Py modbus-server and tedge-agent full builds; see
DEVICES.md). That file puts `tedge identify` on the allow-list. On other
images the cloud has no shell, so only the button works. The same command
works on a local console where the image has one.

**From the board: which device is this board?** Press `sw0` (the BOOT button)
**twice** quickly. The LED fast-blinks for 1 s and the device sends a
`zephyr_Identify` event ("Identify button pressed on <hostname>"), which shows
up on its own device in Cumulocity. Pressing again within 5 s sends no second
event. The device keeps serving throughout. On the MQTT Service the event
reaches the tenant only through the `tedge-zephyr-events` Smart Function
([`cumulocity/`](cumulocity/README.md)); deploy it once per tenant.

## Point libraries (for a tedge-dot collector)

Each firmware ships the point list a collector needs to read it, as a **point
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"getaddrinfo",
"getnameinfo",
"glyph",
"gpios",
"HASCOMPONENT",
"Heisenbug",
"HELF",
Expand Down
9 changes: 9 additions & 0 deletions cumulocity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ parameters, shell) need no mapping.
| `smart-functions/tedge-zephyr-measurements/` | `te/device/*///m/*` → measurements | any measurement, including `tedge_health` |
| `smart-functions/tedge-zephyr-twin/` | `te/device/*///twin/*` → managed-object fragments | `remoteAccess`, `tedge_Agent`, and the current parameter values the Parameters tab shows |
| `smart-functions/tedge-zephyr-firmware-progress/` | `te/device/*///progress/firmware` → `c8y_FirmwareDownload` events | download progress while a firmware update runs (optional) |
| `smart-functions/tedge-zephyr-events/` | `te/device/*///e/*` → events | any event, including `zephyr_Identify` from the sw0 identify pattern |
| `dtm/*.json` | Digital Twin Manager property definitions | the Parameters tab |

## Smart Functions
Expand Down Expand Up @@ -59,6 +60,14 @@ under a fragment named `<name>`, except `tedge_RemoteAccess`, which becomes
reads them. It replaces a rule that mapped only `tedge_RemoteAccess`; do not
deploy both.

**Events.** Every `te/device/<id>///e/<type>` message becomes an event of
type `<type>` on that device, with the payload's `text` (the type when there
is none), the device's `time` when it sent one, and any other payload field
as a fragment. Without this function no event from a device on the MQTT
Service reaches the tenant. The one the firmware sends itself is
`zephyr_Identify`: an operator at a board presses `sw0` twice, and the event
shows which cloud device it is.

## Parameters tab (DTM)

The tenant needs the `dtm` and `device-parameter` microservices, and the
Expand Down
6 changes: 6 additions & 0 deletions cumulocity/smart-functions/tedge-zephyr-events/data-prep.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
smartFunctionFile: events.js
description: "tedge-zephyr events (zephyr_Identify and any other type) to Cumulocity events"
tags: [tedge-zephyr, event]
input:
transport: mqtt
topicPattern: "te/device/*///e/*"
37 changes: 37 additions & 0 deletions cumulocity/smart-functions/tedge-zephyr-events/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Cumulocity Smart Function: tedge-zephyr events -> Cumulocity events.
//
// Topic: te/device/*///e/*
//
// The device sends {"text": "...", "time": "..."} (time only once its clock
// is set), e.g.
// te/device/<id>///e/zephyr_Identify the sw0 identify pattern
// te/device/<id>///e/app_test the `tedge event` test command
// Every type is mapped: the event type is the last topic segment, the text
// falls back to the type, and any further payload field is kept as a
// fragment.
export function onMessage(message, context) {
const type = message.topic.split('/').pop();
let data;
try {
data = JSON.parse(new TextDecoder().decode(message.payload));
} catch (e) {
return [];
}
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
return [];
}
// The device's own timestamp when it sent one (the runtime wants a Date).
const time = typeof data.time === 'string' ? new Date(data.time) : message.time;
const text = typeof data.text === 'string' && data.text !== '' ? data.text : type;
const payload = { type: type, text: text, time: time };
for (const [key, value] of Object.entries(data)) {
if (key !== 'time' && key !== 'text' && key !== 'type') {
payload[key] = value;
}
}
return [{
cumulocityType: 'event',
payload: payload,
externalSource: [{ externalId: message.clientID, type: 'c8y_Serial' }],
}];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
inputs:
- payload: '{"time":"2026-10-06T08:15:00Z","text":"door opened","door":{"id":3}}'
payloadFormat: json
topic: "te/device/tedge-agent7c0c5f5a6eb8///e/door"
clientID: "tedge-agent7c0c5f5a6eb8"
time: "2026-10-06T08:15:01.000Z"
expectedOutput:
- cumulocityType: event
externalSource:
- externalId: tedge-agent7c0c5f5a6eb8
type: c8y_Serial
payload:
type: door
text: door opened
time: "2026-10-06T08:15:00.000Z"
door: {id: 3}
15 changes: 15 additions & 0 deletions cumulocity/smart-functions/tedge-zephyr-events/tests/identify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
inputs:
- payload: '{"time":"2026-10-06T08:15:00Z","text":"Identify button pressed on tedge-modbus7c0c5f5a6eb8"}'
payloadFormat: json
topic: "te/device/tedge-modbus7c0c5f5a6eb8///e/zephyr_Identify"
clientID: "tedge-modbus7c0c5f5a6eb8"
time: "2026-10-06T08:15:01.000Z"
expectedOutput:
- cumulocityType: event
externalSource:
- externalId: tedge-modbus7c0c5f5a6eb8
type: c8y_Serial
payload:
type: zephyr_Identify
text: Identify button pressed on tedge-modbus7c0c5f5a6eb8
time: "2026-10-06T08:15:00.000Z"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Before the device's clock is set it sends no time: the message time is used.
inputs:
- payload: '{}'
payloadFormat: json
topic: "te/device/tedge-agent7c0c5f5a6eb8///e/zephyr_Identify"
clientID: "tedge-agent7c0c5f5a6eb8"
time: "2026-10-06T08:15:01.000Z"
expectedOutput:
- cumulocityType: event
externalSource:
- externalId: tedge-agent7c0c5f5a6eb8
type: c8y_Serial
payload:
type: zephyr_Identify
text: zephyr_Identify
time: "2026-10-06T08:15:01.000Z"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
inputs:
- payload: '{"time":"2026-10-06T08:15:00Z","text":"test event"}'
payloadFormat: json
topic: "te/device/tedge-agent7c0c5f5a6eb8///e/app_test"
clientID: "tedge-agent7c0c5f5a6eb8"
time: "2026-10-06T08:15:01.000Z"
expectedOutput:
- cumulocityType: event
externalSource:
- externalId: tedge-agent7c0c5f5a6eb8
type: c8y_Serial
payload:
type: app_test
text: test event
time: "2026-10-06T08:15:00.000Z"
15 changes: 11 additions & 4 deletions lib/common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ zephyr_library_sources(status_led.c)

# Wi-Fi provisioning support: the boot request that selects the provisioner
# image, the identity record it names itself after, and the application-side
# hand-off with its sw0 gesture classifier. The provisioner itself (Improv over
# BLE) is its own application: apps/wifi-provisioner.
# hand-off. The provisioner itself (Improv over BLE) is its own application:
# apps/wifi-provisioner.
zephyr_library_sources_ifdef(CONFIG_APP_BOOT_REQUEST boot_request.c)
zephyr_library_sources_ifdef(CONFIG_APP_WIFI_CRED_STORE prov_identity.c prov_c8y.c)
zephyr_library_sources_ifdef(CONFIG_APP_PROV_HANDOFF
prov_handoff.c
zephyr_library_sources_ifdef(CONFIG_APP_PROV_HANDOFF prov_handoff.c)

# Identify: `tedge identify` blinks the status LED, the sw0 identify pattern
# sends an event.
zephyr_library_sources_ifdef(CONFIG_APP_IDENTIFY identify.c)

# The sw0 watcher and its gesture classifier, shared by the two above.
zephyr_library_sources_ifdef(CONFIG_APP_BUTTON
button.c
button_gesture.c
)

Expand Down
37 changes: 37 additions & 0 deletions lib/common/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,43 @@ config APP_WIFI_PROV_ERASE_HOLD_S

endif # APP_PROV_HANDOFF

config APP_IDENTIFY
bool "Identify the device from the cloud and from its button"
default y
depends on TEDGE
help
Two ways to match a physical board with its cloud device. From the
cloud: the shell command `tedge identify [seconds]` (needs the shell;
the cloud runs it through c8y_Command once it is on the
TEDGE_SHELL_COMMAND_ALLOW_LIST) blinks the status LED in the identify
pattern. From the device: the sw0 identify pattern sends a
zephyr_Identify event. Boards without a status LED or sw0 build with
that half absent.

if APP_IDENTIFY

config APP_IDENTIFY_DURATION_S
int "Seconds `tedge identify` blinks when given no duration"
default 30
range 1 300

config APP_IDENTIFY_PRESS_COUNT
int "Short sw0 presses in the identify pattern"
default 2
range 2 6
help
Must differ from APP_WIFI_PROV_PRESS_COUNT; the build fails if not.
The pattern uses the provisioning pattern's window when that is
built in, 2 s otherwise.

endif # APP_IDENTIFY

# The sw0 watcher (button.c), shared by the provisioning hand-off and
# identify.
config APP_BUTTON
def_bool APP_PROV_HANDOFF || APP_IDENTIFY
select GPIO if $(dt_alias_enabled,sw0)

config APP_WIFI_PROVISIONER
bool "This image is the Wi-Fi provisioner"
depends on APP_WIFI_CRED_STORE && APP_BOOT_REQUEST
Expand Down
Loading
Loading