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
1 change: 1 addition & 0 deletions c/docs/connecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions c/inc/azure/iot/az_iot_connection_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
16 changes: 9 additions & 7 deletions c/inc/azure/iot/mqttv5/az_iot_twin_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions c/src/mqttv5/twin_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
10 changes: 10 additions & 0 deletions c/tests/unit/connection_client_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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(). */
Expand Down Expand Up @@ -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),
Expand Down
27 changes: 27 additions & 0 deletions c/tests/unit/mqttv5_twin_client_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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(
Expand Down
Loading