diff --git a/c/docs/connecting.md b/c/docs/connecting.md index d0b1b5d1..81edc39b 100644 --- a/c/docs/connecting.md +++ b/c/docs/connecting.md @@ -193,6 +193,7 @@ Examples: [`samples/unified/websockets`](../samples/unified/websockets/main.c), | `session_expiry_seconds` | mqttv5 only. How long the broker keeps the session after a disconnect. Default 1 hour. | | `lwt` | Optional Last Will. The SDK sets none of its own. | | `keep_alive_seconds`, `connect_timeout_seconds` | Default 30 s each. | +| `twin_push` | mqttv5 only. Fixed for the client's lifetime. `push_desired` (default `false`): the service pushes the desired snapshot on connect and each desired patch while connected. When `false`, no desired patches are sent; the twin client fetches the snapshot on connect when behind, and later changes are seen only on reconnect or after `az_iot_mqttv5_twin_client_get()`. `push_reported` (default `false`): the service pushes the reported section on connect. | ## Certificates diff --git a/c/inc/azure/iot/az_iot_connection_client.h b/c/inc/azure/iot/az_iot_connection_client.h index b2895070..f1adb557 100644 --- a/c/inc/azure/iot/az_iot_connection_client.h +++ b/c/inc/azure/iot/az_iot_connection_client.h @@ -649,9 +649,12 @@ extern "C" * client: changing push mode at runtime would desynchronize the device's * expectation from the service's most recently recorded decision. * - * Both default to false (pull-only). With push_desired, the service pushes - * the desired snapshot on connect instead of the mqttv5 twin client fetching - * it. With push_reported, a pushed reported section reaches + * Both default to false. With push_desired, the service pushes the desired + * snapshot on connect and each desired patch while connected. Without it + * (pull mode) the service sends no desired patches: the mqttv5 twin client + * fetches the snapshot on connect when behind, and later changes are seen + * only on reconnect or after az_iot_mqttv5_twin_client_get(). With + * push_reported, a pushed reported section reaches * az_iot_mqttv5_twin_client_set_reported_handler(); without a handler it is * dropped. Ignored for MQTTv3 hubs and for DPS sessions. * diff --git a/c/inc/azure/iot/mqttv5/az_iot_twin_client.h b/c/inc/azure/iot/mqttv5/az_iot_twin_client.h index 104106fc..75a5448c 100644 --- a/c/inc/azure/iot/mqttv5/az_iot_twin_client.h +++ b/c/inc/azure/iot/mqttv5/az_iot_twin_client.h @@ -7,13 +7,15 @@ * @file * @brief MQTT v5 (MQTTv5 hub) twin client. * - * Desired properties are kept in sync with the service: the handler receives a - * @ref AZ_IOT_MQTTV5_TWIN_DESIRED_SNAPSHOT (full document) whenever the device is - * behind, then each in-order @ref AZ_IOT_MQTTV5_TWIN_DESIRED_PATCH. The client - * fetches the snapshot itself: on connect when the birth-ack shows the device - * behind (unless `options.twin_push.push_desired` has the service push it), and - * whenever a patch or probe shows it behind. It holds no twin content, only - * versions. + * Desired properties reach the handler as a + * @ref AZ_IOT_MQTTV5_TWIN_DESIRED_SNAPSHOT (full document) whenever the device + * is behind, then as each in-order @ref AZ_IOT_MQTTV5_TWIN_DESIRED_PATCH. The + * service sends patches and version probes only when + * `options.twin_push.push_desired` is set, and then also pushes the snapshot on + * connect. Otherwise (the default), changes made while connected reach the + * handler only on reconnect or after a GET. The client fetches a snapshot when + * the birth-ack (without push_desired), a GET, a patch or a probe shows it + * behind. It holds no twin content, only versions. * * GET and reported patches are requests: each completes exactly once, with the * service's answer, AZ_IOT_ERR_TIMEOUT, or AZ_IOT_ERR_NOT_CONNECTED. The SDK diff --git a/c/src/mqttv5/twin_client.c b/c/src/mqttv5/twin_client.c index 91f21680..3d668356 100644 --- a/c/src/mqttv5/twin_client.c +++ b/c/src/mqttv5/twin_client.c @@ -736,9 +736,9 @@ static void adopt_session(az_iot_mqttv5_twin_client* t) /** * @brief Hub CONNECTED: requests can now be published, so catch up. * - * `twin_push.push_desired` only replaces the birth-ack fetch. A replaced twin - * not yet settled by a pushed snapshot, or a gap a patch or probe showed before - * CONNECTED, is still fetched. + * With `twin_push.push_desired` the service pushes the snapshot, so the birth-ack + * fetch is skipped; a replaced twin not yet settled by a pushed snapshot, or a + * gap a patch or probe showed before CONNECTED, is still fetched. */ static void on_hub_connected(az_iot_mqttv5_twin_client* t) { diff --git a/c/tests/unit/connection_client_test.c b/c/tests/unit/connection_client_test.c index b95a806b..31036f72 100644 --- a/c/tests/unit/connection_client_test.c +++ b/c/tests/unit/connection_client_test.c @@ -817,6 +817,15 @@ static void hub_mqtt_v5_birth_advertises_configured_twin_push(void** state) assert_memory_equal(birth->payload, expect_body, sizeof(expect_body)); } +/* Twin push is opt-in: by default the birth requests no desired or reported push. */ +static void options_default_leaves_twin_push_off(void** state) +{ + (void)state; + az_iot_connection_client_options opts = az_iot_connection_client_options_default(); + assert_false(opts.twin_push.push_desired); + assert_false(opts.twin_push.push_reported); +} + /* The connection nonce must be a well-formed RFC 4122 version 4 UUID: the * service treats it as a UUID, and the .NET client produces one via * Guid.NewGuid(). */ @@ -2792,6 +2801,7 @@ int main(void) hub_mqtt_v5_birth_reports_session_present, setup_mqtt_v5, teardown), cmocka_unit_test_setup_teardown( hub_mqtt_v5_birth_advertises_configured_twin_push, setup_mqtt_v5_twin_push, teardown), + cmocka_unit_test(options_default_leaves_twin_push_off), cmocka_unit_test_setup_teardown(hub_mqtt_v5_connect_nonce_is_uuid_v4, setup_mqtt_v5, teardown), cmocka_unit_test_setup_teardown( hub_mqtt_v5_birth_ack_records_twin_versions, setup_mqtt_v5, teardown), diff --git a/c/tests/unit/mqttv5_twin_client_test.c b/c/tests/unit/mqttv5_twin_client_test.c index de04d58f..a3a239c9 100644 --- a/c/tests/unit/mqttv5_twin_client_test.c +++ b/c/tests/unit/mqttv5_twin_client_test.c @@ -1454,6 +1454,32 @@ static void connecting_current_does_not_fetch(void** state) assert_int_equal(count_gets(fx), 0); } +/* Without push_desired the service sends no patches, so an application GET is how + * a change made while connected is found: one showing desired ahead fetches a + * snapshot for the handler. */ +static void a_get_ahead_fetches_a_snapshot(void** state) +{ + fixture* fx = (fixture*)*state; + desired_record drec = { 0 }; + set_desired(fx, &drec); + open_to_connected(fx); + + get_record rec = { 0 }; + uint8_t corr[16]; + issue_get(fx, &rec, corr); + /* TwinGetResponse { 1: desired_version=3, 2: reported_version=1 } */ + const uint8_t body[] = { 0x08, 0x03, 0x10, 0x01 }; + inject_twin(fx, "get-response:1", corr, body, sizeof(body)); + assert_true(rec.fired); + assert_false(drec.fired); + assert_int_equal(count_gets(fx), 2); + + answer_last_get(fx, 3, "{\"s\":3}"); + assert_int_equal(drec.kind, AZ_IOT_MQTTV5_TWIN_DESIRED_SNAPSHOT); + assert_int_equal(drec.version, 3); + assert_string_equal(drec.payload, "{\"s\":3}"); +} + /* A handler set while connected starts from a snapshot. */ static void setting_a_handler_while_connected_fetches_a_snapshot(void** state) { @@ -2561,6 +2587,7 @@ int main(void) cmocka_unit_test_setup_teardown( with_push_desired_connecting_does_not_fetch, setup_push_desired, teardown), cmocka_unit_test_setup_teardown(connecting_current_does_not_fetch, setup, teardown), + cmocka_unit_test_setup_teardown(a_get_ahead_fetches_a_snapshot, setup, teardown), cmocka_unit_test_setup_teardown( setting_a_handler_while_connected_fetches_a_snapshot, setup, teardown), cmocka_unit_test_setup_teardown(