diff --git a/extras/test/CMakeLists.txt b/extras/test/CMakeLists.txt index 8e5148905..2aab9ab9b 100644 --- a/extras/test/CMakeLists.txt +++ b/extras/test/CMakeLists.txt @@ -36,11 +36,15 @@ set(TEST_SRCS src/test_CloudSchedule.cpp src/test_decode.cpp src/test_encode.cpp + src/test_command_decode.cpp + src/test_command_encode.cpp src/test_publishEvery.cpp src/test_publishOnChange.cpp src/test_publishOnChangeRateLimit.cpp src/test_readOnly.cpp src/test_writeOnly.cpp + src/test_writeOnDemand.cpp + src/test_writeOnChange.cpp ) set(TEST_UTIL_SRCS @@ -53,6 +57,9 @@ set(TEST_DUT_SRCS ../../src/property/PropertyContainer.cpp ../../src/cbor/CBORDecoder.cpp ../../src/cbor/CBOREncoder.cpp + ../../src/cbor/MessageDecoder.cpp + ../../src/cbor/MessageEncoder.cpp + ../../src/cbor/CBOR.cpp ../../src/cbor/lib/tinycbor/src/cborencoder.c ../../src/cbor/lib/tinycbor/src/cborencoder_close_container_checked.c ../../src/cbor/lib/tinycbor/src/cborerrorstrings.c diff --git a/extras/test/src/test_command_decode.cpp b/extras/test/src/test_command_decode.cpp new file mode 100644 index 000000000..818d02136 --- /dev/null +++ b/extras/test/src/test_command_decode.cpp @@ -0,0 +1,736 @@ +/* + Copyright (c) 2024 Arduino. All rights reserved. +*/ + +/****************************************************************************** + INCLUDE + ******************************************************************************/ + +#include +#include + +#include + +#include +#include + +/****************************************************************************** + TEST CODE + ******************************************************************************/ + +SCENARIO("Test the decoding of command messages") { + /****************************************************************************/ + + WHEN("Decode the ThingUpdateCmdId message") + { + CommandDown command; + /* + DA 00010400 # tag(66560) + 81 # array(1) + 78 24 # text(36) + 65343439346435352D383732612D346664322D393634362D393266383739343933393463 # "e4494d55-872a-4fd2-9646-92f87949394c" + */ + uint8_t const payload[] = {0xDA, 0x00, 0x01, 0x04, 0x00, 0x81, 0x78, 0x24, + 0x65, 0x34, 0x34, 0x39, 0x34, 0x64, 0x35, 0x35, + 0x2D, 0x38, 0x37, 0x32, 0x61, 0x2D, 0x34, 0x66, + 0x64, 0x32, 0x2D, 0x39, 0x36, 0x34, 0x36, 0x2D, + 0x39, 0x32, 0x66, 0x38, 0x37, 0x39, 0x34, 0x39, + 0x33, 0x39, 0x34, 0x63}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + const char *thingIdToMatch = "e4494d55-872a-4fd2-9646-92f87949394c"; + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Complete); + REQUIRE(strcmp(command.thingUpdateCmd.params.thing_id, thingIdToMatch) == 0); + REQUIRE(command.c.id == ThingUpdateCmdId); + } + } + + /****************************************************************************/ + + WHEN("Decode the ThingUpdateCmdId message containing a number instead of a string") + { + CommandDown command; + /* + DA 00010400 # tag(66560) + 81 # array(1) + 1A 65DCB821 # unsigned(1708963873) + */ + uint8_t const payload[] = {0xDA, 0x00, 0x01, 0x04, 0x00, 0x81, 0x1A, 0x65, + 0xDC, 0xB8, 0x21}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode the SetTimezoneCommand message") + { + CommandDown command; + + /* + DA 00010764 # tag(67840) + 82 # array(2) + 1A 65DCB821 # unsigned(1708963873) + 1A 78ACA191 # unsigned(2024579473) + */ + + uint8_t const payload[] = {0xDA, 0x00, 0x01, 0x09, 0x00, 0x82, 0x1A, 0x65, + 0xDC, 0xB8, 0x21, 0x1A, 0x78, 0xAC, 0xA1, 0x91}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Complete); + REQUIRE(command.timezoneCommandDown.params.offset == (uint32_t)1708963873); + REQUIRE(command.timezoneCommandDown.params.until == (uint32_t)2024579473); + REQUIRE(command.c.id == TimezoneCommandDownId); + } + } + + /****************************************************************************/ + + WHEN("Decode the LastValuesUpdateCmd message") + { + CommandDown command; + + /* + DA 00010600 # tag(67072) + 81 # array(1) + 4D # bytes(13) + 00010203040506070809101112 # "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\u0010\u0011\u0012" + + */ + + uint8_t const payload[] = {0xDA, 0x00, 0x01, 0x06, 0x00, 0x81, 0x4D, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x10, 0x11, 0x12}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Complete); + REQUIRE(command.lastValuesUpdateCmd.params.length == 13); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[0] == (uint8_t)0x00); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[1] == (uint8_t)0x01); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[2] == (uint8_t)0x02); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[3] == (uint8_t)0x03); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[4] == (uint8_t)0x04); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[5] == (uint8_t)0x05); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[6] == (uint8_t)0x06); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[7] == (uint8_t)0x07); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[8] == (uint8_t)0x08); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[9] == (uint8_t)0x09); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[10] == (uint8_t)0x10); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[11] == (uint8_t)0x11); + REQUIRE(command.lastValuesUpdateCmd.params.last_values[12] == (uint8_t)0x12); + REQUIRE(command.c.id == LastValuesUpdateCmdId); + } + free(command.lastValuesUpdateCmd.params.last_values); + } + + /****************************************************************************/ + + WHEN("Decode the OtaUpdateCmdDown message") + { + CommandDown command; + + /* + DA 00010100 # tag(65792) + 84 # array(4) + 50 # bytes(16) + C73CB045F9C2434585AFFA36A307BFE7"\xC7<\xB0E\xF9\xC2CE\x85\xAF\xFA6\xA3\a\xBF\xE7" + 78 72 # text(141) + 68747470733A2F2F626F617264732D69 + 6E742E6F6E69756472612E63632F7374 + 6F726167652F6669726D776172652F76 + 312F6466316561633963376264363334 + 37336666666231313766393837333730 + 33653465633935353933316532363766 + 32363236326230393439626331366463 + 3439 # "https://boards-int.oniudra.cc/storage/firmware/v1/df1eac9c7bd63473fffb117f9873703e4ec955931e267f26262b0949bc16dc49" + 58 20 # bytes(32) + 00000000000000000000000000000000 + 00000000000000000000000000000000# "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" + 58 20 # bytes(32) + DF1EAC9C7BD63473FFFB117F9873703E + 4EC955931E267F26262B0949BC16DC49# "\xDF\u001E\xAC\x9C{\xD64s\xFF\xFB\u0011\u007F\x98sp>N\xC9U\x93\u001E&\u007F&&+\tI\xBC\u0016\xDCI" + + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x01, 0x00, 0x84, 0x50, 0xc7, + 0x3c, 0xb0, 0x45, 0xf9, 0xc2, 0x43, 0x45, 0x85, + 0xaf, 0xfa, 0x36, 0xa3, 0x07, 0xbf, 0xe7, 0x78, + 0x72, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, + 0x2f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x73, 0x2d, + 0x69, 0x6e, 0x74, 0x2e, 0x6f, 0x6e, 0x69, 0x75, + 0x64, 0x72, 0x61, 0x2e, 0x63, 0x63, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x66, + 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2f, + 0x76, 0x31, 0x2f, 0x64, 0x66, 0x31, 0x65, 0x61, + 0x63, 0x39, 0x63, 0x37, 0x62, 0x64, 0x36, 0x33, + 0x34, 0x37, 0x33, 0x66, 0x66, 0x66, 0x62, 0x31, + 0x31, 0x37, 0x66, 0x39, 0x38, 0x37, 0x33, 0x37, + 0x30, 0x33, 0x65, 0x34, 0x65, 0x63, 0x39, 0x35, + 0x35, 0x39, 0x33, 0x31, 0x65, 0x32, 0x36, 0x37, + 0x66, 0x32, 0x36, 0x32, 0x36, 0x32, 0x62, 0x30, + 0x39, 0x34, 0x39, 0x62, 0x63, 0x31, 0x36, 0x64, + 0x63, 0x34, 0x39, 0x58, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x20, 0xdf, + 0x1e, 0xac, 0x9c, 0x7b, 0xd6, 0x34, 0x73, 0xff, + 0xfb, 0x11, 0x7f, 0x98, 0x73, 0x70, 0x3e, 0x4e, + 0xc9, 0x55, 0x93, 0x1e, 0x26, 0x7f, 0x26, 0x26, + 0x2b, 0x09, 0x49, 0xbc, 0x16, 0xdc, 0x49}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + uint8_t otaIdToMatch[ID_SIZE] = { 0xC7, 0x3C, 0xB0, 0x45, 0xF9, 0xC2, 0x43, 0x45, + 0x85, 0xAF, 0xFA, 0x36, 0xA3, 0x07, 0xBF, 0xE7}; + const char *urlToMatch = "https://boards-int.oniudra.cc/storage/firmware/v1/df1eac9c7bd63473fffb117f9873703e4ec955931e267f26262b0949bc16dc49"; + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Complete); + REQUIRE(memcmp(command.otaUpdateCmdDown.params.id, otaIdToMatch, ID_SIZE) == 0); + REQUIRE(strcmp(command.otaUpdateCmdDown.params.url, urlToMatch) == 0); + // Initial SHA256 check + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[0] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[1] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[2] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[3] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[4] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[5] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[6] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[7] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[8] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[9] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[10] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[11] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[12] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[13] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[14] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[15] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[16] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[17] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[18] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[19] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[20] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[21] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[22] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[23] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[24] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[25] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[26] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[27] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[28] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[29] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[30] == (uint8_t)0x00); + REQUIRE(command.otaUpdateCmdDown.params.initialSha256[31] == (uint8_t)0x00); + + // Final SHA256 check + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[0] == (uint8_t)0xdf); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[1] == (uint8_t)0x1e); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[2] == (uint8_t)0xac); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[3] == (uint8_t)0x9c); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[4] == (uint8_t)0x7b); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[5] == (uint8_t)0xd6); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[6] == (uint8_t)0x34); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[7] == (uint8_t)0x73); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[8] == (uint8_t)0xff); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[9] == (uint8_t)0xfb); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[10] == (uint8_t)0x11); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[11] == (uint8_t)0x7f); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[12] == (uint8_t)0x98); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[13] == (uint8_t)0x73); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[14] == (uint8_t)0x70); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[15] == (uint8_t)0x3e); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[16] == (uint8_t)0x4e); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[17] == (uint8_t)0xc9); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[18] == (uint8_t)0x55); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[19] == (uint8_t)0x93); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[20] == (uint8_t)0x1e); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[21] == (uint8_t)0x26); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[22] == (uint8_t)0x7f); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[23] == (uint8_t)0x26); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[24] == (uint8_t)0x26); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[25] == (uint8_t)0x2b); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[26] == (uint8_t)0x09); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[27] == (uint8_t)0x49); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[28] == (uint8_t)0xbc); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[29] == (uint8_t)0x16); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[30] == (uint8_t)0xdc); + REQUIRE(command.otaUpdateCmdDown.params.finalSha256[31] == (uint8_t)0x49); + + REQUIRE(command.c.id == OtaUpdateCmdDownId); + } + } + +/****************************************************************************/ + + WHEN("Decode the OtaUpdateCmdDown message with out of order fields 1") + { + CommandDown command; + + /* + DA 00010100 # tag(65792) + 84 # array(4) + 78 72 # text(141) + 68747470733A2F2F626F617264732D69 + 6E742E6F6E69756472612E63632F7374 + 6F726167652F6669726D776172652F76 + 312F6466316561633963376264363334 + 37336666666231313766393837333730 + 33653465633935353933316532363766 + 32363236326230393439626331366463 + 3439 # "https://boards-int.oniudra.cc/storage/firmware/v1/df1eac9c7bd63473fffb117f9873703e4ec955931e267f26262b0949bc16dc49" + 50 # bytes(16) + C73CB045F9C2434585AFFA36A307BFE7"\xC7<\xB0E\xF9\xC2CE\x85\xAF\xFA6\xA3\a\xBF\xE7" + 58 20 # bytes(32) + 00000000000000000000000000000000 + 00000000000000000000000000000000# "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" + 58 20 # bytes(32) + DF1EAC9C7BD63473FFFB117F9873703E + 4EC955931E267F26262B0949BC16DC49# "\xDF\u001E\xAC\x9C{\xD64s\xFF\xFB\u0011\u007F\x98sp>N\xC9U\x93\u001E&\u007F&&+\tI\xBC\u0016\xDCI" + + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x01, 0x00, 0x84, 0x78, 0x72, + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x73, 0x2d, 0x69, + 0x6e, 0x74, 0x2e, 0x6f, 0x6e, 0x69, 0x75, 0x64, + 0x72, 0x61, 0x2e, 0x63, 0x63, 0x2f, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x66, 0x69, + 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x64, 0x66, 0x31, 0x65, 0x61, 0x63, + 0x39, 0x63, 0x37, 0x62, 0x64, 0x36, 0x33, 0x34, + 0x37, 0x33, 0x66, 0x66, 0x66, 0x62, 0x31, 0x31, + 0x37, 0x66, 0x39, 0x38, 0x37, 0x33, 0x37, 0x30, + 0x33, 0x65, 0x34, 0x65, 0x63, 0x39, 0x35, 0x35, + 0x39, 0x33, 0x31, 0x65, 0x32, 0x36, 0x37, 0x66, + 0x32, 0x36, 0x32, 0x36, 0x32, 0x62, 0x30, 0x39, + 0x34, 0x39, 0x62, 0x63, 0x31, 0x36, 0x64, 0x63, + 0x34, 0x39, 0x50, 0xc7, 0x3c, 0xb0, 0x45, 0xf9, + 0xc2, 0x43, 0x45, 0x85, 0xaf, 0xfa, 0x36, 0xa3, + 0x07, 0xbf, 0xe7, 0x58, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x20, 0xdf, + 0x1e, 0xac, 0x9c, 0x7b, 0xd6, 0x34, 0x73, 0xff, + 0xfb, 0x11, 0x7f, 0x98, 0x73, 0x70, 0x3e, 0x4e, + 0xc9, 0x55, 0x93, 0x1e, 0x26, 0x7f, 0x26, 0x26, + 0x2b, 0x09, 0x49, 0xbc, 0x16, 0xdc, 0x49}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Error); + } + } + +/****************************************************************************/ + + WHEN("Decode the OtaUpdateCmdDown message with out of order fields 2") + { + CommandDown command; + + /* + DA 00010100 # tag(65792) + 84 # array(4) + 50 # bytes(16) + C73CB045F9C2434585AFFA36A307BFE7"\xC7<\xB0E\xF9\xC2CE\x85\xAF\xFA6\xA3\a\xBF\xE7" + 58 20 # bytes(32) + 00000000000000000000000000000000 + 00000000000000000000000000000000# "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" + 78 72 # text(141) + 68747470733A2F2F626F617264732D69 + 6E742E6F6E69756472612E63632F7374 + 6F726167652F6669726D776172652F76 + 312F6466316561633963376264363334 + 37336666666231313766393837333730 + 33653465633935353933316532363766 + 32363236326230393439626331366463 + 3439 # "https://boards-int.oniudra.cc/storage/firmware/v1/df1eac9c7bd63473fffb117f9873703e4ec955931e267f26262b0949bc16dc49" + 58 20 # bytes(32) + DF1EAC9C7BD63473FFFB117F9873703E + 4EC955931E267F26262B0949BC16DC49# "\xDF\u001E\xAC\x9C{\xD64s\xFF\xFB\u0011\u007F\x98sp>N\xC9U\x93\u001E&\u007F&&+\tI\xBC\u0016\xDCI" + + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x01, 0x00, 0x84, 0x50, 0xc7, + 0x3c, 0xb0, 0x45, 0xf9, 0xc2, 0x43, 0x45, 0x85, + 0xaf, 0xfa, 0x36, 0xa3, 0x07, 0xbf, 0xe7, 0x58, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x78, 0x72, 0x68, 0x74, 0x74, 0x70, 0x73, + 0x3a, 0x2f, 0x2f, 0x62, 0x6f, 0x61, 0x72, 0x64, + 0x73, 0x2d, 0x69, 0x6e, 0x74, 0x2e, 0x6f, 0x6e, + 0x69, 0x75, 0x64, 0x72, 0x61, 0x2e, 0x63, 0x63, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2f, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, + 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x66, 0x31, + 0x65, 0x61, 0x63, 0x39, 0x63, 0x37, 0x62, 0x64, + 0x36, 0x33, 0x34, 0x37, 0x33, 0x66, 0x66, 0x66, + 0x62, 0x31, 0x31, 0x37, 0x66, 0x39, 0x38, 0x37, + 0x33, 0x37, 0x30, 0x33, 0x65, 0x34, 0x65, 0x63, + 0x39, 0x35, 0x35, 0x39, 0x33, 0x31, 0x65, 0x32, + 0x36, 0x37, 0x66, 0x32, 0x36, 0x32, 0x36, 0x32, + 0x62, 0x30, 0x39, 0x34, 0x39, 0x62, 0x63, 0x31, + 0x36, 0x64, 0x63, 0x34, 0x39, 0x58, 0x20, 0xdf, + 0x1e, 0xac, 0x9c, 0x7b, 0xd6, 0x34, 0x73, 0xff, + 0xfb, 0x11, 0x7f, 0x98, 0x73, 0x70, 0x3e, 0x4e, + 0xc9, 0x55, 0x93, 0x1e, 0x26, 0x7f, 0x26, 0x26, + 0x2b, 0x09, 0x49, 0xbc, 0x16, 0xdc, 0x49}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Error); + } + } + +/****************************************************************************/ + + WHEN("Decode the OtaUpdateCmdDown message with corrupted fields 1") + { + CommandDown command; + + /* + DA 00010100 # tag(65792) + 84 # array(4) + 50 # bytes(16) + C73CB045F9C2434585AFFA36A307BFE7"\xC7<\xB0E\xF9\xC2CE\x85\xAF\xFA6\xA3\a\xBF\xE7" + 78 72 # text(141) + 68747470733A2F2F626F617264732D69 + 6E742E6F6E69756472612E63632F7374 + 6F726167652F6669726D776172652F76 + 312F6466316561633963376264363334 + 37336666666231313766393837333730 + 33653465633935353933316532363766 + 32363236326230393439626331366463 + 3439 # "https://boards-int.oniudra.cc/storage/firmware/v1/df1eac9c7bd63473fffb117f9873703e4ec955931e267f26262b0949bc16dc49" + 1A 65DCB821 # unsigned(1708963873) + 58 20 # bytes(32) + DF1EAC9C7BD63473FFFB117F9873703E + 4EC955931E267F26262B0949BC16DC49# "\xDF\u001E\xAC\x9C{\xD64s\xFF\xFB\u0011\u007F\x98sp>N\xC9U\x93\u001E&\u007F&&+\tI\xBC\u0016\xDCI" + + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x01, 0x00, 0x84, 0x50, 0xc7, + 0x3c, 0xb0, 0x45, 0xf9, 0xc2, 0x43, 0x45, 0x85, + 0xaf, 0xfa, 0x36, 0xa3, 0x07, 0xbf, 0xe7, 0x78, + 0x72, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, + 0x2f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x73, 0x2d, + 0x69, 0x6e, 0x74, 0x2e, 0x6f, 0x6e, 0x69, 0x75, + 0x64, 0x72, 0x61, 0x2e, 0x63, 0x63, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x66, + 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2f, + 0x76, 0x31, 0x2f, 0x64, 0x66, 0x31, 0x65, 0x61, + 0x63, 0x39, 0x63, 0x37, 0x62, 0x64, 0x36, 0x33, + 0x34, 0x37, 0x33, 0x66, 0x66, 0x66, 0x62, 0x31, + 0x31, 0x37, 0x66, 0x39, 0x38, 0x37, 0x33, 0x37, + 0x30, 0x33, 0x65, 0x34, 0x65, 0x63, 0x39, 0x35, + 0x35, 0x39, 0x33, 0x31, 0x65, 0x32, 0x36, 0x37, + 0x66, 0x32, 0x36, 0x32, 0x36, 0x32, 0x62, 0x30, + 0x39, 0x34, 0x39, 0x62, 0x63, 0x31, 0x36, 0x64, + 0x63, 0x34, 0x39, 0x1A, 0x65, 0xDC, 0xB8, 0x21, + 0x58, 0x20, 0xdf, 0x1e, 0xac, 0x9c, 0x7b, 0xd6, + 0x34, 0x73, 0xff, 0xfb, 0x11, 0x7f, 0x98, 0x73, + 0x70, 0x3e, 0x4e, 0xc9, 0x55, 0x93, 0x1e, 0x26, + 0x7f, 0x26, 0x26, 0x2b, 0x09, 0x49, 0xbc, 0x16, + 0xdc, 0x49}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Error); + } + } + +/****************************************************************************/ + + WHEN("Decode the OtaUpdateCmdDown message with corrupted fields 2") + { + CommandDown command; + + /* + DA 00010100 # tag(65792) + 84 # array(4) + 50 # bytes(16) + C73CB045F9C2434585AFFA36A307BFE7"\xC7<\xB0E\xF9\xC2CE\x85\xAF\xFA6\xA3\a\xBF\xE7" + 78 72 # text(141) + 68747470733A2F2F626F617264732D69 + 6E742E6F6E69756472612E63632F7374 + 6F726167652F6669726D776172652F76 + 312F6466316561633963376264363334 + 37336666666231313766393837333730 + 33653465633935353933316532363766 + 32363236326230393439626331366463 + 3439 # "https://boards-int.oniudra.cc/storage/firmware/v1/df1eac9c7bd63473fffb117f9873703e4ec955931e267f26262b0949bc16dc49" + 58 20 # bytes(32) + DF1EAC9C7BD63473FFFB117F9873703E + 4EC955931E267F26262B0949BC16DC49# "\xDF\u001E\xAC\x9C{\xD64s\xFF\xFB\u0011\u007F\x98sp>N\xC9U\x93\u001E&\u007F&&+\tI\xBC\u0016\xDCI" + 1A 65DCB821 # unsigned(1708963873) + + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x01, 0x00, 0x84, 0x50, 0xc7, + 0x3c, 0xb0, 0x45, 0xf9, 0xc2, 0x43, 0x45, 0x85, + 0xaf, 0xfa, 0x36, 0xa3, 0x07, 0xbf, 0xe7, 0x78, + 0x72, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, + 0x2f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x73, 0x2d, + 0x69, 0x6e, 0x74, 0x2e, 0x6f, 0x6e, 0x69, 0x75, + 0x64, 0x72, 0x61, 0x2e, 0x63, 0x63, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x66, + 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x2f, + 0x76, 0x31, 0x2f, 0x64, 0x66, 0x31, 0x65, 0x61, + 0x63, 0x39, 0x63, 0x37, 0x62, 0x64, 0x36, 0x33, + 0x34, 0x37, 0x33, 0x66, 0x66, 0x66, 0x62, 0x31, + 0x31, 0x37, 0x66, 0x39, 0x38, 0x37, 0x33, 0x37, + 0x30, 0x33, 0x65, 0x34, 0x65, 0x63, 0x39, 0x35, + 0x35, 0x39, 0x33, 0x31, 0x65, 0x32, 0x36, 0x37, + 0x66, 0x32, 0x36, 0x32, 0x36, 0x32, 0x62, 0x30, + 0x39, 0x34, 0x39, 0x62, 0x63, 0x31, 0x36, 0x64, + 0x63, 0x34, 0x39, 0x58, 0x20, 0xdf, 0x1e, 0xac, + 0x9c, 0x7b, 0xd6, 0x34, 0x73, 0xff, 0xfb, 0x11, + 0x7f, 0x98, 0x73, 0x70, 0x3e, 0x4e, 0xc9, 0x55, + 0x93, 0x1e, 0x26, 0x7f, 0x26, 0x26, 0x2b, 0x09, + 0x49, 0xbc, 0x16, 0xdc, 0x49, 0x1A, 0x65, 0xDC, + 0xB8, 0x21}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is successful") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode the OtaBeginUp message") + { + CommandDown command; + /* + DA 00010000 # tag(65536) + 81 # array(1) + 58 20 # bytes(32) + 01020304 + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x00, 0x00, 0x81, 0x58, 0x20, + 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful - OtaBeginUp is not supported") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode the ThingBeginCmd message") + { + CommandDown command; + /* + DA 00010300 # tag(66304) + 81 # array(1) + 68 # text(8) + 7468696E675F6964 # "thing_id" + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x03, 0x00, 0x81, 0x68, 0x74, + 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful - ThingBeginCmd is not supported") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode the LastValuesBeginCmd message") + { + CommandDown command; + /* + DA 00010500 # tag(66816) + 80 # array(0) + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x05, 0x00, 0x80}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful - LastValuesBeginCmd is not supported") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode the DeviceBeginCmd message") + { + CommandDown command; + /* + DA 00010700 # tag(67328) + 81 # array(1) + 65 # text(5) + 322E302E30 # "2.0.0" + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x07, 0x00, 0x81, 0x65, 0x32, + 0x2e, 0x30, 0x2e, 0x30}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful - DeviceBeginCmd is not supported") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode the OtaProgressCmdUp message") + { + CommandDown command; + /* + DA 00010200 # tag(66048) + 84 # array(4) + 50 # bytes(16) + 000102030405060708090A0B0C0D0E0F # "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f" + E1 # primitive(1) + 20 # negative(0) + 18 64 # unsigned(100) + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x02, 0x00, 0x84, 0x50, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xe1, + 0x20, 0x18, 0x64}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful - OtaProgressCmdUp is not supported") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode the TimezoneCommandUp message") + { + CommandDown command; + /* + DA 00010800 # tag(67584) + 80 # array(0) + */ + uint8_t const payload[] = {0xda, 0x00, 0x01, 0x08, 0x00, 0x80}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful - TimezoneCommandUp is not supported") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode a message with invalid CBOR tag") + { + CommandDown command; + + /* + DA ffffffff # invalid tag + 82 # array(2) + 1A 65DCB821 # unsigned(1708963873) + 1A 78ACA191 # unsigned(2024579473) + */ + + uint8_t const payload[] = {0xDA, 0xff, 0xff, 0xff, 0xff, 0x82, 0x1A, 0x65, + 0xDC, 0xB8, 0x21, 0x1A, 0x78, 0xAC, 0xA1, 0x91}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode a message not starting with a CBOR tag") + { + CommandDown command; + + /* + 82 # array(2) + 1A 65DCB821 # unsigned(1708963873) + 1A 78ACA191 # unsigned(2024579473) + */ + + uint8_t const payload[] = {0x82, 0x1A, 0x65, 0xDC, 0xB8, 0x21, 0x1A, 0x78, + 0xAC, 0xA1, 0x91}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful") { + REQUIRE(err == Decoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Decode an invalid CBOR message") + { + CommandDown command; + + uint8_t const payload[] = {0xFF}; + + size_t payload_length = sizeof(payload) / sizeof(uint8_t); + CBORMessageDecoder decoder; + Decoder::Status err = decoder.decode((Message*)&command, payload, payload_length); + + THEN("The decode is unsuccessful") { + REQUIRE(err == Decoder::Status::Error); + } + } + +} diff --git a/extras/test/src/test_command_encode.cpp b/extras/test/src/test_command_encode.cpp new file mode 100644 index 000000000..7e31c1487 --- /dev/null +++ b/extras/test/src/test_command_encode.cpp @@ -0,0 +1,322 @@ +/* + Copyright (c) 2024 Arduino. All rights reserved. +*/ + +/****************************************************************************** + INCLUDE + ******************************************************************************/ + +#include + +#include + +#include +#include + +/****************************************************************************** + TEST CODE + ******************************************************************************/ + +SCENARIO("Test the encoding of command messages") { + /****************************************************************************/ + + WHEN("Encode the OtaBeginUp message") + { + OtaBeginUp command; + uint8_t sha[SHA256_SIZE] = {0x01, 0x02, 0x03, 0x04}; + memcpy(command.params.sha, sha, SHA256_SIZE); + + command.c.id = CommandId::OtaBeginUpId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + uint8_t expected_result[] = { + 0xda, 0x00, 0x01, 0x00, 0x00, 0x81, 0x58, 0x20, + 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + // Test the encoding is + // DA 00010000 # tag(65536) + // 81 # array(1) + // 58 20 # bytes(32) + // 01020304 + THEN("The encoding is successful") { + REQUIRE(err == Encoder::Status::Complete); + REQUIRE(bytes_encoded == sizeof(expected_result)); + REQUIRE(memcmp(buffer, expected_result, sizeof(expected_result)) == 0); + } + } + + + /****************************************************************************/ + + WHEN("Encode the ThingBeginCmd message") + { + ThingBeginCmd command; + String thing_id = "thing_id"; + strcpy(command.params.thing_id, thing_id.c_str()); + + command.c.id = CommandId::ThingBeginCmdId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + uint8_t expected_result[] = { + 0xda, 0x00, 0x01, 0x03, 0x00, 0x81, 0x68, 0x74, + 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64 + }; + + // Test the encoding is + // DA 00010300 # tag(66304) + // 81 # array(1) + // 68 # text(8) + // 7468696E675F6964 # "thing_id" + + THEN("The encoding is successful") { + REQUIRE(err == Encoder::Status::Complete); + REQUIRE(bytes_encoded == sizeof(expected_result)); + REQUIRE(memcmp(buffer, expected_result, sizeof(expected_result)) == 0); + } + } + + /****************************************************************************/ + + WHEN("Encode the LastValuesBeginCmd message") + { + LastValuesBeginCmd command; + command.c.id = CommandId::LastValuesBeginCmdId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + uint8_t expected_result[] = { + 0xda, 0x00, 0x01, 0x05, 0x00, 0x80 + }; + + // Test the encoding is + // DA 00010500 # tag(66816) + // 80 # array(0) + THEN("The encoding is successful") { + REQUIRE(err == Encoder::Status::Complete); + REQUIRE(bytes_encoded == sizeof(expected_result)); + REQUIRE(memcmp(buffer, expected_result, sizeof(expected_result)) == 0); + } + } + + /**************************************************************************/ + + WHEN("Encode the DeviceBeginCmd message") + { + DeviceBeginCmd command; + String lib_version = "2.0.0"; + strcpy(command.params.lib_version, lib_version.c_str()); + + command.c.id = CommandId::DeviceBeginCmdId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + uint8_t expected_result[] = { + 0xda, 0x00, 0x01, 0x07, 0x00, 0x81, 0x65, 0x32, + 0x2e, 0x30, 0x2e, 0x30 + }; + + // Test the encoding is + // DA 00010700 # tag(67328) + // 81 # array(1) + // 65 # text(5) + // 322E302E30 # "2.0.0" + THEN("The encoding is successful") { + REQUIRE(err == Encoder::Status::Complete); + REQUIRE(bytes_encoded == sizeof(expected_result)); + REQUIRE(memcmp(buffer, expected_result, sizeof(expected_result)) == 0); + } + } + + /****************************************************************************/ + + WHEN("Encode the OtaProgressCmdUp message") + { + OtaProgressCmdUp command; + command.params.time = 2; + + uint8_t id[ID_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}; + memcpy(command.params.id, id, ID_SIZE); + command.params.state = 1; + command.params.state_data = -1; + command.params.time = 100; + + command.c.id = CommandId::OtaProgressCmdUpId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + uint8_t expected_result[] = { + 0xda, 0x00, 0x01, 0x02, 0x00, 0x84, 0x50, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xe1, + 0x20, 0x18, 0x64 + }; + + // Test the encoding is + // DA 00010200 # tag(66048) + // 84 # array(4) + // 50 # bytes(16) + // 000102030405060708090A0B0C0D0E0F # "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f" + // E1 # primitive(1) + // 20 # negative(0) + // 18 64 # unsigned(100) + THEN("The encoding is successful") { + REQUIRE(err == Encoder::Status::Complete); + REQUIRE(bytes_encoded == sizeof(expected_result)); + REQUIRE(memcmp(buffer, expected_result, sizeof(expected_result)) == 0); + } + } + + /****************************************************************************/ + + WHEN("Encode the TimezoneCommandUp message") + { + TimezoneCommandUp command; + command.c.id = CommandId::TimezoneCommandUpId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + uint8_t expected_result[] = { + 0xda, 0x00, 0x01, 0x08, 0x00, 0x80 + }; + + // Test the encoding is + // DA 00010800 # tag(67584) + // 80 # array(0) + THEN("The encoding is successful") { + REQUIRE(err == Encoder::Status::Complete); + REQUIRE(bytes_encoded == sizeof(expected_result)); + REQUIRE(memcmp(buffer, expected_result, sizeof(expected_result)) == 0); + } + } + + /****************************************************************************/ + + WHEN("Encode the ThingUpdateCmdId message") + { + ThingUpdateCmd command; + command.c.id = CommandId::ThingUpdateCmdId; + + String thing_id = "e4494d55-872a-4fd2-9646-92f87949394c"; + strcpy(command.params.thing_id, thing_id.c_str()); + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + THEN("The encoding is unsuccessful - ThingUpdateCmdId is not supported") { + REQUIRE(err == Encoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Encode the SetTimezoneCommand message") + { + TimezoneCommandDown command; + command.c.id = CommandId::TimezoneCommandDownId; + + command.params.offset = 1708963873; + command.params.until = 2024579473; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + THEN("The encoding is unsuccessful - SetTimezoneCommand is not supported") { + REQUIRE(err == Encoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Encode the LastValuesUpdateCmd message") + { + LastValuesUpdateCmd command; + command.c.id = CommandId::LastValuesUpdateCmdId; + + command.params.length = 13; + uint8_t last_values[13] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x10, 0x11, 0x12}; + command.params.last_values = last_values; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + THEN("The encoding is unsuccessful - LastValuesUpdateCmd is not supported") { + REQUIRE(err == Encoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Encode the OtaUpdateCmdDown message") + { + OtaUpdateCmdDown command; + command.c.id = CommandId::OtaUpdateCmdDownId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + THEN("The encoding is unsuccessful - OtaUpdateCmdDown is not supported") { + REQUIRE(err == Encoder::Status::Error); + } + } + + /****************************************************************************/ + + WHEN("Encode a message with unknown command Id") + { + OtaUpdateCmdDown command; + command.c.id = CommandId::UnknownCmdId; + + uint8_t buffer[512]; + size_t bytes_encoded = sizeof(buffer); + + CBORMessageEncoder encoder; + Encoder::Status err = encoder.encode((Message*)&command, buffer, bytes_encoded); + + THEN("The encoding is unsuccessful - UnknownCmdId is not supported") { + REQUIRE(err == Encoder::Status::Error); + } + } +} diff --git a/extras/test/src/test_writeOnChange.cpp b/extras/test/src/test_writeOnChange.cpp new file mode 100644 index 000000000..0896bbdfd --- /dev/null +++ b/extras/test/src/test_writeOnChange.cpp @@ -0,0 +1,33 @@ +/* + Copyright (c) 2019 Arduino. All rights reserved. +*/ + +/************************************************************************************** + INCLUDE + **************************************************************************************/ + +#include + +#include + +#include +#include + +/************************************************************************************** + TEST CODE + **************************************************************************************/ + +SCENARIO("An Arduino cloud property is marked 'write on change'", "[ArduinoCloudThing::decode]") +{ + PropertyContainer property_container; + + CloudInt test = 0; + addPropertyToContainer(property_container, test, "test", Permission::ReadWrite).writeOnChange(); + + /* [{0: "test", 2: 7}] = 81 A2 00 64 74 65 73 74 02 07 */ + uint8_t const payload[] = {0x81, 0xA2, 0x00, 0x64, 0x74, 0x65, 0x73, 0x74, 0x02, 0x07}; + int const payload_length = sizeof(payload) / sizeof(uint8_t); + CBORDecoder::decode(property_container, payload, payload_length); + + REQUIRE(test == 7); +} diff --git a/extras/test/src/test_writeOnDemand.cpp b/extras/test/src/test_writeOnDemand.cpp new file mode 100644 index 000000000..5bd0c3782 --- /dev/null +++ b/extras/test/src/test_writeOnDemand.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2019 Arduino. All rights reserved. +*/ + +/************************************************************************************** + INCLUDE + **************************************************************************************/ + +#include + +#include + +#include +#include + +/************************************************************************************** + TEST CODE + **************************************************************************************/ + +SCENARIO("An Arduino cloud property is marked 'write on demand'", "[ArduinoCloudThing::decode]") +{ + PropertyContainer property_container; + + CloudInt test = 0; + addPropertyToContainer(property_container, test, "test", Permission::ReadWrite).writeOnDemand(); + + /* [{0: "test", 2: 7}] = 81 A2 00 64 74 65 73 74 02 07 */ + uint8_t const payload[] = {0x81, 0xA2, 0x00, 0x64, 0x74, 0x65, 0x73, 0x74, 0x02, 0x07}; + int const payload_length = sizeof(payload) / sizeof(uint8_t); + CBORDecoder::decode(property_container, payload, payload_length); + + REQUIRE(test == 0); + + Property* p = getProperty(property_container, "test"); + p->fromCloudToLocal(); + + REQUIRE(test == 7); +} diff --git a/src/AIoTC_Config.h b/src/AIoTC_Config.h index 7828191f0..dad0c38ac 100644 --- a/src/AIoTC_Config.h +++ b/src/AIoTC_Config.h @@ -140,11 +140,13 @@ #define AIOT_CONFIG_INTERVAL_RETRY_DELAY_ms (10000UL) #define AIOT_CONFIG_RECONNECTION_RETRY_DELAY_ms (1000UL) #define AIOT_CONFIG_MAX_RECONNECTION_RETRY_DELAY_ms (32000UL) -#define AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms (5*1000UL) +#define AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms (2000UL) #define AIOT_CONFIG_MAX_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms (32000UL) +#define AIOT_CONFIG_DEVICE_TOPIC_MAX_RETRY_CNT (10UL) +#define AIOT_CONFIG_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms (20000UL) +#define AIOT_CONFIG_MAX_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms (1280000UL) #define AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_RETRY_DELAY_ms (1000UL) #define AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_MAX_RETRY_CNT (10UL) -#define AIOT_CONFIG_MAX_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms (1280000UL) #define AIOT_CONFIG_TIMEOUT_FOR_LASTVALUES_SYNC_ms (30000UL) #define AIOT_CONFIG_LASTVALUES_SYNC_MAX_RETRY_CNT (10UL) diff --git a/src/ArduinoIoTCloud.cpp b/src/ArduinoIoTCloud.cpp index 580521047..a565cac44 100644 --- a/src/ArduinoIoTCloud.cpp +++ b/src/ArduinoIoTCloud.cpp @@ -27,15 +27,11 @@ ArduinoIoTCloudClass::ArduinoIoTCloudClass() : _connection{nullptr} -, _last_checked_property_index{0} , _time_service(TimeService) -, _tz_offset{0} -, _tz_dst_until{0} -, _thing_id{""} +, _thing_id{"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"} , _lib_version{AIOT_CONFIG_LIB_VERSION} -, _device_id{""} +, _device_id{"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"} , _cloud_event_callback{nullptr} -, _thing_id_outdated{false} { } @@ -46,12 +42,12 @@ ArduinoIoTCloudClass::ArduinoIoTCloudClass() void ArduinoIoTCloudClass::push() { - requestUpdateForAllProperties(_thing_property_container); + requestUpdateForAllProperties(getThingPropertyContainer()); } bool ArduinoIoTCloudClass::setTimestamp(String const & prop_name, unsigned long const timestamp) { - Property * p = getProperty(_thing_property_container, prop_name); + Property * p = getProperty(getThingPropertyContainer(), prop_name); if (p == nullptr) return false; @@ -120,7 +116,7 @@ Property& ArduinoIoTCloudClass::addPropertyReal(String& property, String name, i } Property& ArduinoIoTCloudClass::addPropertyReal(Property& property, String name, int tag, Permission const permission) { - return addPropertyToContainer(_thing_property_container, property, name, permission, tag); + return addPropertyToContainer(getThingPropertyContainer(), property, name, permission, tag); } /* The following methods are deprecated but still used for non-LoRa boards */ @@ -197,9 +193,9 @@ void ArduinoIoTCloudClass::addPropertyRealInternal(Property& property, String na } if (seconds == ON_CHANGE) { - addPropertyToContainer(_thing_property_container, property, name, permission, tag).publishOnChange(minDelta, Property::DEFAULT_MIN_TIME_BETWEEN_UPDATES_MILLIS).onUpdate(fn).onSync(synFn); + addPropertyToContainer(getThingPropertyContainer(), property, name, permission, tag).publishOnChange(minDelta, Property::DEFAULT_MIN_TIME_BETWEEN_UPDATES_MILLIS).onUpdate(fn).onSync(synFn); } else { - addPropertyToContainer(_thing_property_container, property, name, permission, tag).publishEvery(seconds).onUpdate(fn).onSync(synFn); + addPropertyToContainer(getThingPropertyContainer(), property, name, permission, tag).publishEvery(seconds).onUpdate(fn).onSync(synFn); } } diff --git a/src/ArduinoIoTCloud.h b/src/ArduinoIoTCloud.h index 6154f25e9..7c0bcc5f9 100644 --- a/src/ArduinoIoTCloud.h +++ b/src/ArduinoIoTCloud.h @@ -96,23 +96,16 @@ class ArduinoIoTCloudClass inline void setDeviceId(String const device_id) { _device_id = device_id; }; inline String & getDeviceId() { return _device_id; }; - inline void setThingIdOutdatedFlag() { _thing_id_outdated = true ; } - inline void clrThingIdOutdatedFlag() { _thing_id_outdated = false ; } - inline bool getThingIdOutdatedFlag() { return _thing_id_outdated; } - - inline bool deviceNotAttached() { return _thing_id == ""; } - inline ConnectionHandler * getConnection() { return _connection; } inline unsigned long getInternalTime() { return _time_service.getTime(); } inline unsigned long getLocalTime() { return _time_service.getLocalTime(); } - inline void updateInternalTimezoneInfo() { _time_service.setTimeZoneData(_tz_offset, _tz_dst_until); } void addCallback(ArduinoIoTCloudEvent const event, OnCloudEventCallback callback); #define addProperty( v, ...) addPropertyReal(v, #v, __VA_ARGS__) - /* The following methods are used for non-LoRa boards which can use the + /* The following methods are used for non-LoRa boards which can use the * name of the property to identify a given property within a CBOR message. */ @@ -153,12 +146,7 @@ class ArduinoIoTCloudClass protected: ConnectionHandler * _connection; - PropertyContainer _device_property_container; - PropertyContainer _thing_property_container; - unsigned int _last_checked_property_index; TimeServiceClass & _time_service; - int _tz_offset; - unsigned int _tz_dst_until; String _thing_id; String _lib_version; @@ -166,11 +154,11 @@ class ArduinoIoTCloudClass private: - void addPropertyRealInternal(Property& property, String name, int tag, permissionType permission_type = READWRITE, long seconds = ON_CHANGE, void(*fn)(void) = NULL, float minDelta = 0.0f, void(*synFn)(Property & property) = CLOUD_WINS); + virtual PropertyContainer &getThingPropertyContainer() = 0; + void addPropertyRealInternal(Property& property, String name, int tag, permissionType permission_type = READWRITE, long seconds = ON_CHANGE, void(*fn)(void) = NULL, float minDelta = 0.0f, void(*synFn)(Property & property) = CLOUD_WINS); String _device_id; OnCloudEventCallback _cloud_event_callback[3]; - bool _thing_id_outdated; }; #ifdef HAS_TCP diff --git a/src/ArduinoIoTCloudDevice.cpp b/src/ArduinoIoTCloudDevice.cpp new file mode 100644 index 000000000..b580d16ac --- /dev/null +++ b/src/ArduinoIoTCloudDevice.cpp @@ -0,0 +1,144 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include + +#ifdef HAS_TCP + +#include "ArduinoIoTCloudDevice.h" +#include "interfaces/CloudProcess.h" + +/****************************************************************************** + CTOR/DTOR + ******************************************************************************/ +ArduinoCloudDevice::ArduinoCloudDevice(MessageStream *ms) +: CloudProcess(ms), +_state{State::Init}, +_attachAttempt(0, 0), +_attached(false), +_registered(false) { +} + +void ArduinoCloudDevice::begin() { + _attachAttempt.begin(AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms, + AIOT_CONFIG_MAX_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms); +} + +void ArduinoCloudDevice::update() { + /* Run through the state machine. */ + State nextState = _state; + switch (_state) { + case State::Init: nextState = handleInit(); break; + case State::SendCapabilities: nextState = handleSendCapabilities(); break; + case State::Connected: nextState = handleConnected(); break; + case State::Disconnected: nextState = handleDisconnected(); break; + } + + /* Handle external events */ + switch (_command) { + case DeviceAttachedCmdId: + _attached = true; + _registered = true; + DEBUG_VERBOSE("CloudDevice::%s Device is attached", __FUNCTION__); + nextState = State::Connected; + break; + + case DeviceDetachedCmdId: + _attached = false; + _registered = false; + nextState = State::Init; + break; + + case DeviceRegisteredCmdId: + _registered = true; + nextState = State::Connected; + break; + + /* We have received a reset command */ + case ResetCmdId: + nextState = State::Init; + break; + + default: + break; + } + + _command = UnknownCmdId; + _state = nextState; +} + +int ArduinoCloudDevice::connected() { + return _state != State::Disconnected ? 1 : 0; +} + +void ArduinoCloudDevice::handleMessage(Message *m) { + _command = UnknownCmdId; + if (m != nullptr) { + _command = m->id; + } +} + +ArduinoCloudDevice::State ArduinoCloudDevice::handleInit() { + /* Reset attempt struct for the nex retry after disconnection */ + _attachAttempt.begin(AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms, + AIOT_CONFIG_MAX_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms); + + _attached = false; + _registered = false; + + return State::SendCapabilities; +} + +ArduinoCloudDevice::State ArduinoCloudDevice::handleSendCapabilities() { + /* Sends device capabilities message */ + DeviceBeginCmd deviceBegin = { DeviceBeginCmdId, AIOT_CONFIG_LIB_VERSION }; + deliver(reinterpret_cast(&deviceBegin)); + + /* Subscribe to device topic to request */ + ThingBeginCmd thingBegin = { ThingBeginCmdId }; + deliver(reinterpret_cast(&thingBegin)); + + /* No device configuration received. Wait: 4s -> 8s -> 16s -> 32s -> 32s ...*/ + _attachAttempt.retry(); + DEBUG_VERBOSE("CloudDevice::%s not attached. %d next configuration request in %d ms", + __FUNCTION__, _attachAttempt.getRetryCount(), _attachAttempt.getWaitTime()); + return State::Connected; +} + +ArduinoCloudDevice::State ArduinoCloudDevice::handleConnected() { + /* Max retry than disconnect */ + if (_attachAttempt.getRetryCount() > AIOT_CONFIG_DEVICE_TOPIC_MAX_RETRY_CNT) { + return State::Disconnected; + } + + if (!_attached && _attachAttempt.isExpired()) { + if (_registered) { + /* Device configuration received, but invalid thing_id. Do not increase + * counter, but recompute delay. + * Wait: 4s -> 80s -> 160s -> 320s -> 640s -> 1280s -> 1280s ... + */ + _attachAttempt.reconfigure(AIOT_CONFIG_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms, + AIOT_CONFIG_MAX_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms); + } + return State::SendCapabilities; + } + + return State::Connected; +} + +ArduinoCloudDevice::State ArduinoCloudDevice::handleDisconnected() { + return State::Disconnected; +} + +#endif /* HAS_TCP */ diff --git a/src/ArduinoIoTCloudDevice.h b/src/ArduinoIoTCloudDevice.h new file mode 100644 index 000000000..294acfde5 --- /dev/null +++ b/src/ArduinoIoTCloudDevice.h @@ -0,0 +1,70 @@ +/* + This file is part of the Arduino_SecureElement library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef ARDUINO_IOT_CLOUD_DEVICE_H +#define ARDUINO_IOT_CLOUD_DEVICE_H + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include "interfaces/CloudProcess.h" +#include "utility/time/TimedAttempt.h" +#include "property/PropertyContainer.h" + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class ArduinoCloudDevice : public CloudProcess { +public: + + ArduinoCloudDevice(MessageStream* stream); + virtual void update() override; + virtual void handleMessage(Message* m) override; + + virtual void begin(); + virtual int connected(); + + inline PropertyContainer &getPropertyContainer() { + return _propertyContainer; + }; + inline unsigned int &getPropertyContainerIndex() { + return _propertyContainerIndex; + } + inline bool isAttached() { + return _attached; + }; + + +private: + + enum class State { + Disconnected, + Init, + SendCapabilities, + Connected, + }; + + State _state; + CommandId _command; + TimedAttempt _attachAttempt; + PropertyContainer _propertyContainer; + unsigned int _propertyContainerIndex; + bool _attached; + bool _registered; + + State handleInit(); + State handleSendCapabilities(); + State handleConnected(); + State handleDisconnected(); +}; + +#endif /* ARDUINO_IOT_CLOUD_DEVICE_H */ diff --git a/src/ArduinoIoTCloudLPWAN.cpp b/src/ArduinoIoTCloudLPWAN.cpp index e3f625fb7..22234f97c 100644 --- a/src/ArduinoIoTCloudLPWAN.cpp +++ b/src/ArduinoIoTCloudLPWAN.cpp @@ -51,6 +51,8 @@ ArduinoIoTCloudLPWAN::ArduinoIoTCloudLPWAN() , _retryEnable{false} , _maxNumRetry{5} , _intervalRetry{AIOT_CONFIG_INTERVAL_RETRY_DELAY_ms} +, _thing_property_container{0} +, _last_checked_property_index{0} { } diff --git a/src/ArduinoIoTCloudLPWAN.h b/src/ArduinoIoTCloudLPWAN.h index 8d1f42d28..46c9e242d 100644 --- a/src/ArduinoIoTCloudLPWAN.h +++ b/src/ArduinoIoTCloudLPWAN.h @@ -49,6 +49,8 @@ class ArduinoIoTCloudLPWAN : public ArduinoIoTCloudClass inline void setMaxRetry (int val) { _maxNumRetry = val; } inline void setIntervalRetry(long val) { _intervalRetry = val; } + inline PropertyContainer &getThingPropertyContainer() { return _thing_property_container; } + private: @@ -64,6 +66,9 @@ class ArduinoIoTCloudLPWAN : public ArduinoIoTCloudClass int _maxNumRetry; long _intervalRetry; + PropertyContainer _thing_property_container; + unsigned int _last_checked_property_index; + State handle_ConnectPhy(); State handle_SyncTime(); State handle_Connected(); diff --git a/src/ArduinoIoTCloudTCP.cpp b/src/ArduinoIoTCloudTCP.cpp index a78915031..516a667c5 100644 --- a/src/ArduinoIoTCloudTCP.cpp +++ b/src/ArduinoIoTCloudTCP.cpp @@ -43,6 +43,7 @@ #include #include "cbor/CBOREncoder.h" #include "utility/watchdog/Watchdog.h" +#include /****************************************************************************** LOCAL MODULE FUNCTIONS @@ -53,30 +54,16 @@ unsigned long getTime() return ArduinoCloud.getInternalTime(); } -void updateTimezoneInfo() -{ - ArduinoCloud.updateInternalTimezoneInfo(); -} - -void setThingIdOutdated() -{ - ArduinoCloud.setThingIdOutdatedFlag(); -} - /****************************************************************************** CTOR/DTOR ******************************************************************************/ ArduinoIoTCloudTCP::ArduinoIoTCloudTCP() : _state{State::ConnectPhy} -, _next_connection_attempt_tick{0} -, _last_connection_attempt_cnt{0} -, _next_device_subscribe_attempt_tick{0} -, _last_device_subscribe_cnt{0} -, _last_sync_request_tick{0} -, _last_sync_request_cnt{0} -, _last_subscribe_request_tick{0} -, _last_subscribe_request_cnt{0} +, _connection_attempt(0,0) +, _message_stream(std::bind(&ArduinoIoTCloudTCP::sendMessage, this, std::placeholders::_1)) +, _thing(&_message_stream) +, _device(&_message_stream) , _mqtt_data_buf{0} , _mqtt_data_len{0} , _mqtt_data_request_retransmit{false} @@ -89,11 +76,10 @@ ArduinoIoTCloudTCP::ArduinoIoTCloudTCP() , _mqttClient{nullptr} , _deviceTopicOut("") , _deviceTopicIn("") -, _shadowTopicOut("") -, _shadowTopicIn("") +, _messageTopicOut("") +, _messageTopicIn("") , _dataTopicOut("") , _dataTopicIn("") -, _deviceSubscribedToThing{false} #if OTA_ENABLED , _ota_cap{false} , _ota_error{static_cast(OTAError::None)} @@ -120,7 +106,12 @@ int ArduinoIoTCloudTCP::begin(ConnectionHandler & connection, bool const enable_ #else _brokerPort = brokerPort; #endif + + /* Setup TimeService */ _time_service.begin(&connection); + + /* Setup retry timers */ + _connection_attempt.begin(AIOT_CONFIG_RECONNECTION_RETRY_DELAY_ms, AIOT_CONFIG_MAX_RECONNECTION_RETRY_DELAY_ms); return begin(enable_watchdog, _brokerAddress, _brokerPort); } @@ -129,25 +120,21 @@ int ArduinoIoTCloudTCP::begin(bool const enable_watchdog, String brokerAddress, _brokerAddress = brokerAddress; _brokerPort = brokerPort; -#if OTA_ENABLED - _ota_img_sha256 = OTA::getImageSHA256(); - DEBUG_VERBOSE("SHA256: HASH(%d) = %s", strlen(_ota_img_sha256.c_str()), _ota_img_sha256.c_str()); -#endif /* OTA_ENABLED */ - #if defined(BOARD_HAS_SECRET_KEY) /* If board is not configured for username and password login */ if(!_password.length()) { #endif + #if defined(BOARD_HAS_SECURE_ELEMENT) if (!_selement.begin()) { DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not initialize secure element.", __FUNCTION__); -#if defined(ARDUINO_UNOWIFIR4) + #if defined(ARDUINO_UNOWIFIR4) if (String(WiFi.firmwareVersion()) < String("0.4.1")) { DEBUG_ERROR("ArduinoIoTCloudTCP::%s In order to read device certificate, WiFi firmware needs to be >= 0.4.1, current %s", __FUNCTION__, WiFi.firmwareVersion()); } -#endif + #endif return 0; } if (!SElementArduinoCloudDeviceId::read(_selement, getDeviceId(), SElementArduinoCloudSlot::DeviceId)) @@ -194,6 +181,7 @@ int ArduinoIoTCloudTCP::begin(bool const enable_watchdog, String brokerAddress, _mqttClient.setUsernamePassword(getDeviceId(), _password); } #endif + _mqttClient.onMessage(ArduinoIoTCloudTCP::onMessage); _mqttClient.setKeepAliveInterval(30 * 1000); _mqttClient.setConnectionTimeout(1500); @@ -201,31 +189,30 @@ int ArduinoIoTCloudTCP::begin(bool const enable_watchdog, String brokerAddress, _deviceTopicOut = getTopic_deviceout(); _deviceTopicIn = getTopic_devicein(); + _messageTopicIn = getTopic_messagein(); + _messageTopicOut = getTopic_messageout(); + _thing.begin(); + _device.begin(); + +#if OTA_ENABLED Property* p; - p = new CloudWrapperString(_lib_version); - addPropertyToContainer(_device_property_container, *p, "LIB_VERSION", Permission::Read, -1); -#if OTA_ENABLED p = new CloudWrapperBool(_ota_cap); - addPropertyToContainer(_device_property_container, *p, "OTA_CAP", Permission::Read, -1); + addPropertyToContainer(_device.getPropertyContainer(), *p, "OTA_CAP", Permission::Read, -1); p = new CloudWrapperInt(_ota_error); - addPropertyToContainer(_device_property_container, *p, "OTA_ERROR", Permission::Read, -1); + addPropertyToContainer(_device.getPropertyContainer(), *p, "OTA_ERROR", Permission::Read, -1); p = new CloudWrapperString(_ota_img_sha256); - addPropertyToContainer(_device_property_container, *p, "OTA_SHA256", Permission::Read, -1); + addPropertyToContainer(_device.getPropertyContainer(), *p, "OTA_SHA256", Permission::Read, -1); p = new CloudWrapperString(_ota_url); - addPropertyToContainer(_device_property_container, *p, "OTA_URL", Permission::ReadWrite, -1); + addPropertyToContainer(_device.getPropertyContainer(), *p, "OTA_URL", Permission::ReadWrite, -1); p = new CloudWrapperBool(_ota_req); - addPropertyToContainer(_device_property_container, *p, "OTA_REQ", Permission::ReadWrite, -1); -#endif /* OTA_ENABLED */ - p = new CloudWrapperString(_thing_id); - addPropertyToContainer(_device_property_container, *p, "thing_id", Permission::ReadWrite, -1).onUpdate(setThingIdOutdated); + addPropertyToContainer(_device.getPropertyContainer(), *p, "OTA_REQ", Permission::ReadWrite, -1); - addPropertyReal(_tz_offset, "tz_offset", Permission::ReadWrite).onSync(CLOUD_WINS).onUpdate(updateTimezoneInfo); - addPropertyReal(_tz_dst_until, "tz_dst_until", Permission::ReadWrite).onSync(CLOUD_WINS).onUpdate(updateTimezoneInfo); - -#if OTA_ENABLED _ota_cap = OTA::isCapable(); -#endif + + _ota_img_sha256 = OTA::getImageSHA256(); + DEBUG_VERBOSE("SHA256: HASH(%d) = %s", strlen(_ota_img_sha256.c_str()), _ota_img_sha256.c_str()); +#endif // OTA_ENABLED #ifdef BOARD_HAS_OFFLOADED_ECCX08 if (String(WiFi.firmwareVersion()) < String("1.4.4")) { @@ -266,7 +253,6 @@ void ArduinoIoTCloudTCP::update() watchdog_reset(); #endif - /* Run through the state machine. */ State next_state = _state; switch (_state) @@ -274,25 +260,13 @@ void ArduinoIoTCloudTCP::update() case State::ConnectPhy: next_state = handle_ConnectPhy(); break; case State::SyncTime: next_state = handle_SyncTime(); break; case State::ConnectMqttBroker: next_state = handle_ConnectMqttBroker(); break; - case State::SendDeviceProperties: next_state = handle_SendDeviceProperties(); break; - case State::SubscribeDeviceTopic: next_state = handle_SubscribeDeviceTopic(); break; - case State::WaitDeviceConfig: next_state = handle_WaitDeviceConfig(); break; - case State::CheckDeviceConfig: next_state = handle_CheckDeviceConfig(); break; - case State::SubscribeThingTopics: next_state = handle_SubscribeThingTopics(); break; - case State::RequestLastValues: next_state = handle_RequestLastValues(); break; case State::Connected: next_state = handle_Connected(); break; case State::Disconnect: next_state = handle_Disconnect(); break; } _state = next_state; -#if OTA_ENABLED - if (_state > State::SubscribeDeviceTopic && _state <= State::Connected) { - handle_OTARequest(); - } -#endif /* OTA_ENABLED */ - /* This watchdog feed is actually needed only by the RP2040 Connect because its - * maximum watchdog window is 8389 ms; despite this we feed it for all + * maximum watchdog window is 8389 ms; despite this we feed it for all * supported ARCH to keep code aligned. */ #if defined (ARDUINO_ARCH_SAMD) || defined (ARDUINO_ARCH_MBED) @@ -324,8 +298,7 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectPhy() { if (_connection->check() == NetworkConnectionState::CONNECTED) { - bool const is_retry_attempt = (_last_connection_attempt_cnt > 0); - if (!is_retry_attempt || (is_retry_attempt && (millis() > _next_connection_attempt_tick))) + if (!_connection_attempt.isRetry() || (_connection_attempt.isRetry() && _connection_attempt.isExpired())) return State::SyncTime; } @@ -334,273 +307,72 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectPhy() ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SyncTime() { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-variable" - unsigned long const internal_posix_time = _time_service.getTime(); -#pragma GCC diagnostic pop - DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s internal clock configured to posix timestamp %d", __FUNCTION__, internal_posix_time); - return State::ConnectMqttBroker; -} - -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectMqttBroker() -{ - if (_mqttClient.connect(_brokerAddress.c_str(), _brokerPort)) + if (TimeServiceClass::isTimeValid(getTime())) { - _last_connection_attempt_cnt = 0; - return State::SendDeviceProperties; + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s internal clock configured to posix timestamp %d", __FUNCTION__, getTime()); + return State::ConnectMqttBroker; } - _last_connection_attempt_cnt++; - unsigned long reconnection_retry_delay = (1 << _last_connection_attempt_cnt) * AIOT_CONFIG_RECONNECTION_RETRY_DELAY_ms; - reconnection_retry_delay = min(reconnection_retry_delay, static_cast(AIOT_CONFIG_MAX_RECONNECTION_RETRY_DELAY_ms)); - _next_connection_attempt_tick = millis() + reconnection_retry_delay; - - DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not connect to %s:%d", __FUNCTION__, _brokerAddress.c_str(), _brokerPort); - DEBUG_ERROR("ArduinoIoTCloudTCP::%s %d connection attempt at tick time %d", __FUNCTION__, _last_connection_attempt_cnt, _next_connection_attempt_tick); return State::ConnectPhy; } -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SendDeviceProperties() -{ - if (!_mqttClient.connected()) - { - return State::Disconnect; - } - - sendDevicePropertiesToCloud(); - return State::SubscribeDeviceTopic; -} - -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SubscribeDeviceTopic() -{ - if (!_mqttClient.connected()) - { - return State::Disconnect; - } - - if (!_mqttClient.subscribe(_deviceTopicIn)) - { - DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not subscribe to %s", __FUNCTION__, _deviceTopicIn.c_str()); - return State::SubscribeDeviceTopic; - } - - if (_last_device_subscribe_cnt > AIOT_CONFIG_LASTVALUES_SYNC_MAX_RETRY_CNT) - { - _last_device_subscribe_cnt = 0; - _next_device_subscribe_attempt_tick = 0; - _mqttClient.stop(); - execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT); - return State::ConnectPhy; - } - - /* No device configuration reply. Wait: 5s -> 10s -> 20s -> 30s */ - unsigned long subscribe_retry_delay = (1 << _last_device_subscribe_cnt) * AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms; - subscribe_retry_delay = min(subscribe_retry_delay, static_cast(AIOT_CONFIG_MAX_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms)); - _next_device_subscribe_attempt_tick = millis() + subscribe_retry_delay; - _last_device_subscribe_cnt++; - - return State::WaitDeviceConfig; -} - -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_WaitDeviceConfig() -{ - if (!_mqttClient.connected()) - { - return State::Disconnect; - } - - if (getThingIdOutdatedFlag()) - { - return State::CheckDeviceConfig; - } - - if (millis() > _next_device_subscribe_attempt_tick) - { - /* Configuration not received or device not attached to a valid thing. Try to resubscribe */ - if (_mqttClient.unsubscribe(_deviceTopicIn)) - { - DEBUG_ERROR("ArduinoIoTCloudTCP::%s device waiting for valid thing_id", __FUNCTION__); - return State::SubscribeDeviceTopic; - } - } - return State::WaitDeviceConfig; -} - -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_CheckDeviceConfig() +ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectMqttBroker() { - if (!_mqttClient.connected()) + if (_mqttClient.connect(_brokerAddress.c_str(), _brokerPort)) { - return State::Disconnect; - } + /* Subscribe to message topic to receive commands */ + _mqttClient.subscribe(_messageTopicIn); - if(_deviceSubscribedToThing == true) - { - /* Unsubscribe from old things topics and go on with a new subscription */ - _mqttClient.unsubscribe(_shadowTopicIn); - _mqttClient.unsubscribe(_dataTopicIn); - _deviceSubscribedToThing = false; - DEBUG_INFO("Disconnected from Arduino IoT Cloud"); - execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT); - } + /* Temoporarly subscribe to device topic to receive OTA properties */ + _mqttClient.subscribe(_deviceTopicIn); - updateThingTopics(); + /* Reconfigure timers for next state */ + _connection_attempt.begin(AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms, AIOT_CONFIG_MAX_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms); - if (deviceNotAttached()) - { - /* Configuration received but device not attached. Wait: 40s */ - unsigned long attach_retry_delay = (1 << _last_device_attach_cnt) * AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms; - attach_retry_delay = min(attach_retry_delay, static_cast(AIOT_CONFIG_MAX_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms)); - _next_device_subscribe_attempt_tick = millis() + attach_retry_delay; - _last_device_attach_cnt++; - return State::WaitDeviceConfig; + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s connected to %s:%d", __FUNCTION__, _brokerAddress.c_str(), _brokerPort); + return State::Connected; } - DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s Device attached to a new valid Thing %s", __FUNCTION__, getThingId().c_str()); - _last_device_attach_cnt = 0; + /* Can't connect to the broker. Wait: 2s -> 4s -> 8s -> 16s -> 32s -> 32s ... */ + _connection_attempt.retry(); - return State::SubscribeThingTopics; + DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not connect to %s:%d", __FUNCTION__, _brokerAddress.c_str(), _brokerPort); + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s %d next connection attempt in %d ms", __FUNCTION__, _connection_attempt.getRetryCount(), _connection_attempt.getWaitTime()); + /* Go back to ConnectPhy and retry to get time from network (invalid time for SSL handshake?)*/ + return State::ConnectPhy; } -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SubscribeThingTopics() +ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Connected() { - if (!_mqttClient.connected()) + if (!_mqttClient.connected() || !_thing.connected() || !_device.connected()) { + Serial.println("State::Disconnect"); return State::Disconnect; } - if (getThingIdOutdatedFlag()) - { - return State::CheckDeviceConfig; - } - - unsigned long const now = millis(); - bool const is_subscribe_retry_delay_expired = (now - _last_subscribe_request_tick) > AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_RETRY_DELAY_ms; - bool const is_first_subscribe_request = (_last_subscribe_request_cnt == 0); - - if (!is_first_subscribe_request && !is_subscribe_retry_delay_expired) - { - return State::SubscribeThingTopics; - } - - if (_last_subscribe_request_cnt > AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_MAX_RETRY_CNT) - { - _last_subscribe_request_cnt = 0; - _last_subscribe_request_tick = 0; - _mqttClient.stop(); - execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT); - return State::ConnectPhy; - } - - _last_subscribe_request_tick = now; - _last_subscribe_request_cnt++; - - if (!_mqttClient.subscribe(_dataTopicIn)) - { - DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not subscribe to %s", __FUNCTION__, _dataTopicIn.c_str()); - DEBUG_ERROR("Check your thing configuration, and press the reset button on your board."); - return State::SubscribeThingTopics; - } - - if (!_mqttClient.subscribe(_shadowTopicIn)) - { - DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not subscribe to %s", __FUNCTION__, _shadowTopicIn.c_str()); - DEBUG_ERROR("Check your thing configuration, and press the reset button on your board."); - return State::SubscribeThingTopics; - } - - DEBUG_INFO("Connected to Arduino IoT Cloud"); - DEBUG_INFO("Thing ID: %s", getThingId().c_str()); - execCloudEventCallback(ArduinoIoTCloudEvent::CONNECT); - _deviceSubscribedToThing = true; - - /*Add retry wait time otherwise we are trying to reconnect every 250 ms...*/ - return State::RequestLastValues; -} - -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_RequestLastValues() -{ - if (!_mqttClient.connected()) - { - return State::Disconnect; + /* Retransmit data in case there was a lost transaction due + * to phy layer or MQTT connectivity loss. + */ + if (_mqtt_data_request_retransmit && (_mqtt_data_len > 0)) { + write(_dataTopicOut, _mqtt_data_buf, _mqtt_data_len); + _mqtt_data_request_retransmit = false; } - if (getThingIdOutdatedFlag()) - { - return State::CheckDeviceConfig; - } + /* Call CloudDevice process to get configuration */ + _device.update(); - /* Check whether or not we need to send a new request. */ - unsigned long const now = millis(); - bool const is_sync_request_timeout = (now - _last_sync_request_tick) > AIOT_CONFIG_TIMEOUT_FOR_LASTVALUES_SYNC_ms; - bool const is_first_sync_request = (_last_sync_request_cnt == 0); - if (is_first_sync_request || is_sync_request_timeout) - { - DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values requested", __FUNCTION__, now); - requestLastValue(); - _last_sync_request_tick = now; - /* Track the number of times a get-last-values request was sent to the cloud. - * If no data is received within a certain number of retry-requests it's a better - * strategy to disconnect and re-establish connection from the ground up. - */ - _last_sync_request_cnt++; - if (_last_sync_request_cnt > AIOT_CONFIG_LASTVALUES_SYNC_MAX_RETRY_CNT) - { - _last_sync_request_cnt = 0; - _last_sync_request_tick = 0; - _mqttClient.stop(); - execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT); - return State::ConnectPhy; - } + if (_device.isAttached()) { + /* Call CloudThing process to synchronize properties */ + _thing.update(); } - return State::RequestLastValues; -} - -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Connected() -{ - if (!_mqttClient.connected()) - { - /* The last message was definitely lost, trigger a retransmit. */ - _mqtt_data_request_retransmit = true; - return State::Disconnect; +#if OTA_ENABLED + if (_device.connected()) { + handle_OTARequest(); } - /* We are connected so let's to our stuff here. */ - else - { - if (getThingIdOutdatedFlag()) - { - return State::CheckDeviceConfig; - } - - /* Check if a primitive property wrapper is locally changed. - * This function requires an existing time service which in - * turn requires an established connection. Not having that - * leads to a wrong time set in the time service which inhibits - * the connection from being established due to a wrong data - * in the reconstructed certificate. - */ - updateTimestampOnLocallyChangedProperties(_thing_property_container); - - /* Retransmit data in case there was a lost transaction due - * to phy layer or MQTT connectivity loss. - */ - if (_mqtt_data_request_retransmit && (_mqtt_data_len > 0)) { - write(_dataTopicOut, _mqtt_data_buf, _mqtt_data_len); - _mqtt_data_request_retransmit = false; - } - - /* Check if any properties need encoding and send them to - * the cloud if necessary. - */ - sendThingPropertiesToCloud(); +#endif /* OTA_ENABLED */ - unsigned long const internal_posix_time = _time_service.getTime(); - if(internal_posix_time < _tz_dst_until) { - return State::Connected; - } else { - return State::RequestLastValues; - } - } + return State::Connected; } #if OTA_ENABLED @@ -636,9 +408,22 @@ void ArduinoIoTCloudTCP::handle_OTARequest() { ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Disconnect() { - DEBUG_ERROR("ArduinoIoTCloudTCP::%s MQTT client connection lost", __FUNCTION__); - _mqttClient.stop(); + if (!_mqttClient.connected()) { + DEBUG_ERROR("ArduinoIoTCloudTCP::%s MQTT client connection lost", __FUNCTION__); + } else { + /* No need to manually unsubscribe because we are using clean sessions */ + _mqttClient.stop(); + } + + Message message = { ResetCmdId }; + _thing.handleMessage(&message); + _device.handleMessage(&message); + + DEBUG_INFO("Disconnected from Arduino IoT Cloud"); execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT); + + /* Setup timer for broker connection and restart */ + _connection_attempt.begin(AIOT_CONFIG_RECONNECTION_RETRY_DELAY_ms, AIOT_CONFIG_MAX_RECONNECTION_RETRY_DELAY_ms); return State::ConnectPhy; } @@ -659,26 +444,105 @@ void ArduinoIoTCloudTCP::handleMessage(int length) /* Topic for OTA properties and device configuration */ if (_deviceTopicIn == topic) { - CBORDecoder::decode(_device_property_container, (uint8_t*)bytes, length); - _last_device_subscribe_cnt = 0; - _next_device_subscribe_attempt_tick = 0; + CBORDecoder::decode(_device.getPropertyContainer(), (uint8_t*)bytes, length); } /* Topic for user input data */ if (_dataTopicIn == topic) { - CBORDecoder::decode(_thing_property_container, (uint8_t*)bytes, length); + CBORDecoder::decode(_thing.getPropertyContainer(), (uint8_t*)bytes, length); + } + + /* Topic for device commands */ + if (_messageTopicIn == topic) { + CommandDown command; + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] received %d bytes", __FUNCTION__, millis(), length); + CBORMessageDecoder decoder; + + size_t buffer_length = length; + if (decoder.decode((Message*)&command, bytes, buffer_length) != Decoder::Status::Error) { + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] received command id %d", __FUNCTION__, millis(), command.c.id); + switch (command.c.id) + { + case CommandId::ThingUpdateCmdId: + { + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] device configuration received", __FUNCTION__, millis()); + if ( _thing_id != String(command.thingUpdateCmd.params.thing_id)) { + _thing_id = String(command.thingUpdateCmd.params.thing_id); + Message message; + /* If we are attached we need first to detach */ + if (_device.isAttached()) { + detachThing(); + message = { DeviceDetachedCmdId }; + } + /* If received thing id is valid attach to the new thing */ + if (_thing_id.length()) { + attachThing(); + message = { DeviceAttachedCmdId }; + } else { + /* Send message to device state machine to inform we have received a null thing-id */ + _thing_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + message = { DeviceRegisteredCmdId }; + } + _device.handleMessage(&message); + } + } + break; + + case CommandId::LastValuesUpdateCmdId: + { + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values received", __FUNCTION__, millis()); + CBORDecoder::decode(_thing.getPropertyContainer(), + (uint8_t*)command.lastValuesUpdateCmd.params.last_values, + command.lastValuesUpdateCmd.params.length, true); + _thing.handleMessage((Message*)&command); + execCloudEventCallback(ArduinoIoTCloudEvent::SYNC); + + /* + * NOTE: in this current version properties are not properly integrated with the new paradigm of + * modeling the messages with C structs. The current CBOR library allocates an array in the heap + * thus we need to delete it after decoding it with the old CBORDecoder + */ + free(command.lastValuesUpdateCmd.params.last_values); + } + break; + + default: + break; + } + } } +} - /* Topic for sync Thing last values on connect */ - if ((_shadowTopicIn == topic) && (_state == State::RequestLastValues)) - { - DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values received", __FUNCTION__, millis()); - CBORDecoder::decode(_thing_property_container, (uint8_t*)bytes, length, true); - _time_service.setTimeZoneData(_tz_offset, _tz_dst_until); - execCloudEventCallback(ArduinoIoTCloudEvent::SYNC); - _last_sync_request_cnt = 0; - _last_sync_request_tick = 0; - _state = State::Connected; +void ArduinoIoTCloudTCP::sendMessage(Message * msg) +{ + uint8_t data[MQTT_TRANSMIT_BUFFER_SIZE]; + size_t bytes_encoded = sizeof(data); + CBORMessageEncoder encoder; + + switch (msg->id) { + case PropertiesUpdateCmdId: + return sendPropertyContainerToCloud(_dataTopicOut, + _thing.getPropertyContainer(), + _thing.getPropertyContainerIndex()); + break; + +#if OTA_ENABLED + case DeviceBeginCmdId: + sendDevicePropertyToCloud("OTA_CAP"); + sendDevicePropertyToCloud("OTA_ERROR"); + sendDevicePropertyToCloud("OTA_SHA256"); + break; +#endif + + default: + break; + } + + if (encoder.encode(msg, data, bytes_encoded) == Encoder::Status::Complete && + bytes_encoded > 0) { + write(_messageTopicOut, data, bytes_encoded); + } else { + DEBUG_ERROR("error encoding %d", msg->id); } } @@ -688,6 +552,7 @@ void ArduinoIoTCloudTCP::sendPropertyContainerToCloud(String const topic, Proper uint8_t data[MQTT_TRANSMIT_BUFFER_SIZE]; if (CBOREncoder::encode(property_container, data, sizeof(data), bytes_encoded, current_property_index, false) == CborNoError) + { if (bytes_encoded > 0) { /* If properties have been encoded store them in the back-up buffer @@ -698,30 +563,7 @@ void ArduinoIoTCloudTCP::sendPropertyContainerToCloud(String const topic, Proper /* Transmit the properties to the MQTT broker */ write(topic, _mqtt_data_buf, _mqtt_data_len); } -} - -void ArduinoIoTCloudTCP::sendThingPropertiesToCloud() -{ - sendPropertyContainerToCloud(_dataTopicOut, _thing_property_container, _last_checked_property_index); -} - -void ArduinoIoTCloudTCP::sendDevicePropertiesToCloud() -{ - PropertyContainer ro_device_property_container; - unsigned int last_device_property_index = 0; - - std::list ro_device_property_list {"LIB_VERSION", "OTA_CAP", "OTA_ERROR", "OTA_SHA256"}; - std::for_each(ro_device_property_list.begin(), - ro_device_property_list.end(), - [this, &ro_device_property_container ] (String const & name) - { - Property* p = getProperty(this->_device_property_container, name); - if(p != nullptr) - addPropertyToContainer(ro_device_property_container, *p, p->name(), p->isWriteableByCloud() ? Permission::ReadWrite : Permission::Read); - } - ); - - sendPropertyContainerToCloud(_deviceTopicOut, ro_device_property_container, last_device_property_index); + } } #if OTA_ENABLED @@ -730,7 +572,7 @@ void ArduinoIoTCloudTCP::sendDevicePropertyToCloud(String const name) PropertyContainer temp_device_property_container; unsigned int last_device_property_index = 0; - Property* p = getProperty(this->_device_property_container, name); + Property* p = getProperty(this->_device.getPropertyContainer(), name); if(p != nullptr) { addPropertyToContainer(temp_device_property_container, *p, p->name(), p->isWriteableByCloud() ? Permission::ReadWrite : Permission::Read); @@ -739,13 +581,31 @@ void ArduinoIoTCloudTCP::sendDevicePropertyToCloud(String const name) } #endif -void ArduinoIoTCloudTCP::requestLastValue() +void ArduinoIoTCloudTCP::attachThing() { - // Send the getLastValues CBOR message to the cloud - // [{0: "r:m", 3: "getLastValues"}] = 81 A2 00 63 72 3A 6D 03 6D 67 65 74 4C 61 73 74 56 61 6C 75 65 73 - // Use http://cbor.me to easily generate CBOR encoding - const uint8_t CBOR_REQUEST_LAST_VALUE_MSG[] = { 0x81, 0xA2, 0x00, 0x63, 0x72, 0x3A, 0x6D, 0x03, 0x6D, 0x67, 0x65, 0x74, 0x4C, 0x61, 0x73, 0x74, 0x56, 0x61, 0x6C, 0x75, 0x65, 0x73 }; - write(_shadowTopicOut, CBOR_REQUEST_LAST_VALUE_MSG, sizeof(CBOR_REQUEST_LAST_VALUE_MSG)); + + _dataTopicIn = getTopic_datain(); + _dataTopicOut = getTopic_dataout(); + if (!_mqttClient.subscribe(_dataTopicIn)) { + DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not subscribe to %s", __FUNCTION__, _dataTopicIn.c_str()); + DEBUG_ERROR("Check your thing configuration, and press the reset button on your board."); + return; + } + + DEBUG_INFO("Connected to Arduino IoT Cloud"); + DEBUG_INFO("Thing ID: %s", getThingId().c_str()); + execCloudEventCallback(ArduinoIoTCloudEvent::CONNECT); +} + +void ArduinoIoTCloudTCP::detachThing() +{ + if (!_mqttClient.unsubscribe(_dataTopicIn)) { + DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not unsubscribe from %s", __FUNCTION__, _dataTopicIn.c_str()); + return; + } + + DEBUG_INFO("Disconnected from Arduino IoT Cloud"); + execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT); } int ArduinoIoTCloudTCP::write(String const topic, byte const data[], int const length) @@ -760,16 +620,6 @@ int ArduinoIoTCloudTCP::write(String const topic, byte const data[], int const l return 0; } -void ArduinoIoTCloudTCP::updateThingTopics() -{ - _shadowTopicOut = getTopic_shadowout(); - _shadowTopicIn = getTopic_shadowin(); - _dataTopicOut = getTopic_dataout(); - _dataTopicIn = getTopic_datain(); - - clrThingIdOutdatedFlag(); -} - /****************************************************************************** * EXTERN DEFINITION ******************************************************************************/ diff --git a/src/ArduinoIoTCloudTCP.h b/src/ArduinoIoTCloudTCP.h index 60b6649cc..ef1a93105 100644 --- a/src/ArduinoIoTCloudTCP.h +++ b/src/ArduinoIoTCloudTCP.h @@ -25,6 +25,8 @@ #include #include #include +#include +#include #if defined(BOARD_HAS_SECURE_ELEMENT) #include @@ -50,6 +52,13 @@ #include #endif +#if OTA_ENABLED +#include +#endif + +#include "cbor/MessageDecoder.h" +#include "cbor/MessageEncoder.h" + /****************************************************************************** CONSTANTS ******************************************************************************/ @@ -74,7 +83,6 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass ArduinoIoTCloudTCP(); virtual ~ArduinoIoTCloudTCP() { } - virtual void update () override; virtual int connected () override; virtual void printDebugInfo() override; @@ -90,6 +98,8 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass inline String getBrokerAddress() const { return _brokerAddress; } inline uint16_t getBrokerPort () const { return _brokerPort; } + inline PropertyContainer &getThingPropertyContainer() { return _thing.getPropertyContainer(); } + #if OTA_ENABLED /* The callback is triggered when the OTA is initiated and it gets executed until _ota_req flag is cleared. * It should return true when the OTA can be applied or false otherwise. @@ -99,8 +109,7 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass _get_ota_confirmation = cb; _ask_user_before_executing_ota = true; } - - void handle_OTARequest(); + void handle_OTARequest(); #endif private: @@ -111,27 +120,16 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass ConnectPhy, SyncTime, ConnectMqttBroker, - SendDeviceProperties, - SubscribeDeviceTopic, - WaitDeviceConfig, - CheckDeviceConfig, - SubscribeThingTopics, - RequestLastValues, Connected, Disconnect, }; State _state; + TimedAttempt _connection_attempt; + MessageStream _message_stream; + ArduinoCloudThing _thing; + ArduinoCloudDevice _device; - unsigned long _next_connection_attempt_tick; - unsigned int _last_connection_attempt_cnt; - unsigned long _next_device_subscribe_attempt_tick; - unsigned int _last_device_subscribe_cnt; - unsigned int _last_device_attach_cnt; - unsigned long _last_sync_request_tick; - unsigned int _last_sync_request_cnt; - unsigned long _last_subscribe_request_tick; - unsigned int _last_subscribe_request_cnt; String _brokerAddress; uint16_t _brokerPort; uint8_t _mqtt_data_buf[MQTT_TRANSMIT_BUFFER_SIZE]; @@ -169,13 +167,11 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass String _deviceTopicOut; String _deviceTopicIn; - String _shadowTopicOut; - String _shadowTopicIn; + String _messageTopicOut; + String _messageTopicIn; String _dataTopicOut; String _dataTopicIn; - bool _deviceSubscribedToThing; - #if OTA_ENABLED bool _ota_cap; int _ota_error; @@ -188,36 +184,31 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass inline String getTopic_deviceout() { return String("/a/d/" + getDeviceId() + "/e/o");} inline String getTopic_devicein () { return String("/a/d/" + getDeviceId() + "/e/i");} - inline String getTopic_shadowout() { return ( getThingId().length() == 0) ? String("") : String("/a/t/" + getThingId() + "/shadow/o"); } - inline String getTopic_shadowin () { return ( getThingId().length() == 0) ? String("") : String("/a/t/" + getThingId() + "/shadow/i"); } + + inline String getTopic_messageout() { return String("/a/d/" + getDeviceId() + "/c/up");} + inline String getTopic_messagein () { return String("/a/d/" + getDeviceId() + "/c/dw");} + inline String getTopic_dataout () { return ( getThingId().length() == 0) ? String("") : String("/a/t/" + getThingId() + "/e/o"); } inline String getTopic_datain () { return ( getThingId().length() == 0) ? String("") : String("/a/t/" + getThingId() + "/e/i"); } State handle_ConnectPhy(); State handle_SyncTime(); State handle_ConnectMqttBroker(); - State handle_SendDeviceProperties(); - State handle_WaitDeviceConfig(); - State handle_CheckDeviceConfig(); - State handle_SubscribeDeviceTopic(); - State handle_SubscribeThingTopics(); - State handle_RequestLastValues(); State handle_Connected(); State handle_Disconnect(); static void onMessage(int length); void handleMessage(int length); + void sendMessage(Message * msg); void sendPropertyContainerToCloud(String const topic, PropertyContainer & property_container, unsigned int & current_property_index); - void sendThingPropertiesToCloud(); - void sendDevicePropertiesToCloud(); - void requestLastValue(); + + void attachThing(); + void detachThing(); int write(String const topic, byte const data[], int const length); #if OTA_ENABLED void sendDevicePropertyToCloud(String const name); #endif - - void updateThingTopics(); }; /****************************************************************************** diff --git a/src/ArduinoIoTCloudThing.cpp b/src/ArduinoIoTCloudThing.cpp new file mode 100644 index 000000000..6ea6dd904 --- /dev/null +++ b/src/ArduinoIoTCloudThing.cpp @@ -0,0 +1,172 @@ +/* + This file is part of the Arduino_SecureElement library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include + +#ifdef HAS_TCP + +#include "ArduinoIoTCloudThing.h" +#include "interfaces/CloudProcess.h" +#include "property/types/CloudWrapperInt.h" +#include "property/types/CloudWrapperUnsignedInt.h" + +/****************************************************************************** + * CTOR/DTOR + ******************************************************************************/ +ArduinoCloudThing::ArduinoCloudThing(MessageStream* ms) +: CloudProcess(ms), +_state{State::Init}, +_syncAttempt(0, 0), +_propertyContainer(0), +_propertyContainerIndex(0), +_utcOffset(0), +_utcOffsetProperty(nullptr), +_utcOffsetExpireTime(0), +_utcOffsetExpireTimeProperty(nullptr) { +} + +/****************************************************************************** + * PUBLIC MEMBER FUNCTIONS + ******************************************************************************/ + +void ArduinoCloudThing::begin() { + Property* property; + + property = new CloudWrapperInt(_utcOffset); + _utcOffsetProperty = &addPropertyToContainer(getPropertyContainer(), + *property, + "tz_offset", + Permission::ReadWrite, -1); + _utcOffsetProperty->writeOnDemand(); + property = new CloudWrapperUnsignedInt(_utcOffsetExpireTime); + _utcOffsetExpireTimeProperty = &addPropertyToContainer(getPropertyContainer(), + *property, + "tz_dst_until", + Permission::ReadWrite, -1); + _utcOffsetExpireTimeProperty->writeOnDemand(); +} + +void ArduinoCloudThing::update() { + /* Run through the state machine. */ + State nextState = _state; + switch (_state) { + case State::Init: nextState = handleInit(); break; + case State::RequestLastValues: nextState = handleRequestLastValues(); break; + case State::Connected: nextState = handleConnected(); break; + case State::Disconnect: nextState = handleDisconnect(); break; + } + + /* Handle external events */ + switch (_command) { + case LastValuesUpdateCmdId: + if (_state == State::RequestLastValues) { + DEBUG_VERBOSE("CloudThing::%s Thing is synced", __FUNCTION__); + nextState = State::Connected; + } + break; + + /* We have received a reset command */ + case ResetCmdId: + nextState = State::Init; + break; + + default: + break; + } + + _command = UnknownCmdId; + _state = nextState; +} + +int ArduinoCloudThing::connected() { + return _state > State::Disconnect ? 1 : 0; +} + +void ArduinoCloudThing::handleMessage(Message* m) { + _command = UnknownCmdId; + if (m != nullptr) { + _command = m->id; + } +} + +ArduinoCloudThing::State ArduinoCloudThing::handleInit() { + _syncAttempt.begin(AIOT_CONFIG_TIMEOUT_FOR_LASTVALUES_SYNC_ms); + return State::RequestLastValues; +} + +ArduinoCloudThing::State ArduinoCloudThing::handleRequestLastValues() { + /* Check whether or not we need to send a new request. */ + if (_syncAttempt.isRetry() && !_syncAttempt.isExpired()) { + return State::RequestLastValues; + } + + /* Track the number of times a get-last-values request was sent to the cloud. + * If no data is received within a certain number of retry-requests it's a + * better strategy to disconnect and re-establish connection from the ground up. + */ + if (_syncAttempt.getRetryCount() > AIOT_CONFIG_LASTVALUES_SYNC_MAX_RETRY_CNT) { + return State::Disconnect; + } + + _syncAttempt.retry(); + + /* Send message upstream to inform infrastructure we need to request thing + * last values + */ + DEBUG_VERBOSE("CloudThing::%s not int sync. %d next sync request in %d ms", + __FUNCTION__, _syncAttempt.getRetryCount(), _syncAttempt.getWaitTime()); + Message message = { LastValuesBeginCmdId }; + deliver(&message); + + return State::RequestLastValues; +} + +ArduinoCloudThing::State ArduinoCloudThing::handleConnected() { + /* Check if a primitive property wrapper is locally changed. + * This function requires an existing time service which in + * turn requires an established connection. Not having that + * leads to a wrong time set in the time service which inhibits + * the connection from being established due to a wrong data + * in the reconstructed certificate. + */ + updateTimestampOnLocallyChangedProperties(getPropertyContainer()); + + /* Configure Time service with timezone data: + * _utcOffset [offset + dst] + * _utcOffsetExpireTime [posix timestamp until _utcOffset is valid] + */ + if (_utcOffsetProperty->isDifferentFromCloud() || + _utcOffsetExpireTimeProperty->isDifferentFromCloud()) { + _utcOffsetProperty->fromCloudToLocal(); + _utcOffsetExpireTimeProperty->fromCloudToLocal(); + TimeService.setTimeZoneData(_utcOffset, _utcOffsetExpireTime); + } + + /* Check if any property needs encoding and send them to the cloud */ + Message message = { PropertiesUpdateCmdId }; + deliver(&message); + + if (getTime() > _utcOffsetExpireTime) { + return State::RequestLastValues; + } + + return State::Connected; +} + +ArduinoCloudThing::State ArduinoCloudThing::handleDisconnect() { + return State::Disconnect; +} + +#endif /* HAS_TCP */ diff --git a/src/ArduinoIoTCloudThing.h b/src/ArduinoIoTCloudThing.h new file mode 100644 index 000000000..de52bc002 --- /dev/null +++ b/src/ArduinoIoTCloudThing.h @@ -0,0 +1,69 @@ +/* + This file is part of the Arduino_SecureElement library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + + +#ifndef ARDUINO_IOT_CLOUD_THING_H +#define ARDUINO_IOT_CLOUD_THING_H + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include "interfaces/CloudProcess.h" +#include "utility/time/TimedAttempt.h" +#include "property/PropertyContainer.h" + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class ArduinoCloudThing : public CloudProcess { +public: + + ArduinoCloudThing(MessageStream *stream); + virtual void update() override; + virtual void handleMessage(Message *m) override; + + virtual void begin(); + virtual int connected(); + + inline PropertyContainer &getPropertyContainer() { + return _propertyContainer; + }; + inline unsigned int &getPropertyContainerIndex() { + return _propertyContainerIndex; + } + +private: + + enum class State { + Disconnect, + Init, + RequestLastValues, + Connected, + }; + + State _state; + CommandId _command; + TimedAttempt _syncAttempt; + PropertyContainer _propertyContainer; + unsigned int _propertyContainerIndex; + int _utcOffset; + Property *_utcOffsetProperty; + unsigned int _utcOffsetExpireTime; + Property *_utcOffsetExpireTimeProperty; + + State handleInit(); + State handleRequestLastValues(); + State handleConnected(); + State handleDisconnect(); +}; + +#endif /* ARDUINO_IOT_CLOUD_THING_H */ diff --git a/src/cbor/CBOR.cpp b/src/cbor/CBOR.cpp new file mode 100644 index 000000000..ced5e3e7f --- /dev/null +++ b/src/cbor/CBOR.cpp @@ -0,0 +1,73 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include "CBOR.h" + +/****************************************************************************** + * FUNCTION DEFINITION + ******************************************************************************/ + +CommandId toCommandId(CBORCommandTag tag) { + switch(tag) { + case CBORCommandTag::CBOROtaBeginUp: + return CommandId::OtaBeginUpId; + case CBORCommandTag::CBORThingBeginCmd: + return CommandId::ThingBeginCmdId; + case CBORCommandTag::CBORLastValuesBeginCmd: + return CommandId::LastValuesBeginCmdId; + case CBORCommandTag::CBORDeviceBeginCmd: + return CommandId::DeviceBeginCmdId; + case CBORCommandTag::CBOROtaProgressCmdUp: + return CommandId::OtaProgressCmdUpId; + case CBORCommandTag::CBORTimezoneCommandUp: + return CommandId::TimezoneCommandUpId; + case CBORCommandTag::CBOROtaUpdateCmdDown: + return CommandId::OtaUpdateCmdDownId; + case CBORCommandTag::CBORThingUpdateCmd: + return CommandId::ThingUpdateCmdId; + case CBORCommandTag::CBORLastValuesUpdate: + return CommandId::LastValuesUpdateCmdId; + case CBORCommandTag::CBORTimezoneCommandDown: + return CommandId::TimezoneCommandDownId; + default: + return CommandId::UnknownCmdId; + } +} + +CBORCommandTag toCBORCommandTag(CommandId id) { + switch(id) { + case CommandId::OtaBeginUpId: + return CBORCommandTag::CBOROtaBeginUp; + case CommandId::ThingBeginCmdId: + return CBORCommandTag::CBORThingBeginCmd; + case CommandId::LastValuesBeginCmdId: + return CBORCommandTag::CBORLastValuesBeginCmd; + case CommandId::DeviceBeginCmdId: + return CBORCommandTag::CBORDeviceBeginCmd; + case CommandId::OtaProgressCmdUpId: + return CBORCommandTag::CBOROtaProgressCmdUp; + case CommandId::TimezoneCommandUpId: + return CBORCommandTag::CBORTimezoneCommandUp; + case CommandId::OtaUpdateCmdDownId: + return CBORCommandTag::CBOROtaUpdateCmdDown; + case CommandId::ThingUpdateCmdId: + return CBORCommandTag::CBORThingUpdateCmd; + case CommandId::LastValuesUpdateCmdId: + return CBORCommandTag::CBORLastValuesUpdate; + case CommandId::TimezoneCommandDownId: + return CBORCommandTag::CBORTimezoneCommandDown; + default: + return CBORCommandTag::CBORUnknownCmdTag; + } +} diff --git a/src/cbor/CBOR.h b/src/cbor/CBOR.h new file mode 100644 index 000000000..999570455 --- /dev/null +++ b/src/cbor/CBOR.h @@ -0,0 +1,49 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ +#include + +/****************************************************************************** + TYPEDEF + ******************************************************************************/ + +enum CBORCommandTag: uint64_t { + // Commands UP + CBOROtaBeginUp = 0x010000, + CBORThingBeginCmd = 0x010300, + CBORLastValuesBeginCmd = 0x010500, + CBORDeviceBeginCmd = 0x010700, + CBOROtaProgressCmdUp = 0x010200, + CBORTimezoneCommandUp = 0x010800, + + // Commands DOWN + CBOROtaUpdateCmdDown = 0x010100, + CBORThingUpdateCmd = 0x010400, + CBORLastValuesUpdate = 0x010600, + CBORTimezoneCommandDown = 0x010900, + + // Unknown Command Tag https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml + CBORUnknownCmdTag16b = 0xffff, // invalid tag + CBORUnknownCmdTag32b = 0xffffffff, // invalid tag + CBORUnknownCmdTag64b = 0xffffffffffffffff, // invalid tag + CBORUnknownCmdTag = CBORUnknownCmdTag32b +}; + +/****************************************************************************** + * FUNCTION DECLARATION + ******************************************************************************/ + +CommandId toCommandId(CBORCommandTag tag); +CBORCommandTag toCBORCommandTag(CommandId id); diff --git a/src/cbor/MessageDecoder.cpp b/src/cbor/MessageDecoder.cpp new file mode 100644 index 000000000..c500ceae9 --- /dev/null +++ b/src/cbor/MessageDecoder.cpp @@ -0,0 +1,230 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +/****************************************************************************** + INCLUDE + ******************************************************************************/ + +#include + +#undef max +#undef min +#include + +#include "MessageDecoder.h" +#include + +/****************************************************************************** + PUBLIC MEMBER FUNCTIONS + ******************************************************************************/ + +Decoder::Status CBORMessageDecoder::decode(Message * message, uint8_t const * const payload, size_t& length) +{ + CborValue main_iter, array_iter; + CborTag tag; + CborParser parser; + + if (cbor_parser_init(payload, length, 0, &parser, &main_iter) != CborNoError) + return Decoder::Status::Error; + + if (main_iter.type != CborTagType) + return Decoder::Status::Error; + + if (cbor_value_get_tag(&main_iter, &tag) == CborNoError) { + message->id = toCommandId(CBORCommandTag(tag)); + } + + if (cbor_value_advance(&main_iter) != CborNoError) { + return Decoder::Status::Error; + } + + ArrayParserState current_state = ArrayParserState::EnterArray, + next_state = ArrayParserState::Error; + + while (current_state != ArrayParserState::Complete) { + switch (current_state) { + case ArrayParserState::EnterArray : next_state = handle_EnterArray(&main_iter, &array_iter); break; + case ArrayParserState::ParseParam : next_state = handle_Param(&array_iter, message); break; + case ArrayParserState::LeaveArray : next_state = handle_LeaveArray(&main_iter, &array_iter); break; + case ArrayParserState::Complete : return Decoder::Status::Complete; + case ArrayParserState::MessageNotSupported : return Decoder::Status::Error; + case ArrayParserState::Error : return Decoder::Status::Error; + } + + current_state = next_state; + } + + return Decoder::Status::Complete; +} + +/****************************************************************************** + PRIVATE MEMBER FUNCTIONS + ******************************************************************************/ + +bool copyCBORStringToArray(CborValue * param, char * dest, size_t dest_size) { + if (cbor_value_is_text_string(param)) { + // NOTE: keep in mind that _cbor_value_copy_string tries to put a \0 at the end of the string + if(_cbor_value_copy_string(param, dest, &dest_size, NULL) == CborNoError) { + return true; + } + } + + return false; +} + +// FIXME dest_size should be also returned, the copied byte array can have a different size from the starting one +// for the time being we need this on SHA256 only +bool copyCBORByteToArray(CborValue * param, uint8_t * dest, size_t dest_size) { + if (cbor_value_is_byte_string(param)) { + // NOTE: keep in mind that _cbor_value_copy_string tries to put a \0 at the end of the string + if(_cbor_value_copy_string(param, dest, &dest_size, NULL) == CborNoError) { + return true; + } + } + + return false; +} + +CBORMessageDecoder::ArrayParserState CBORMessageDecoder::handle_EnterArray(CborValue * main_iter, CborValue * array_iter) { + ArrayParserState next_state = ArrayParserState::Error; + if (cbor_value_get_type(main_iter) == CborArrayType) { + if (cbor_value_enter_container(main_iter, array_iter) == CborNoError) { + next_state = ArrayParserState::ParseParam; + } + } + + return next_state; +} + +CBORMessageDecoder::ArrayParserState CBORMessageDecoder::handle_LeaveArray(CborValue * main_iter, CborValue * array_iter) { + // Advance to the next parameter (the last one in the array) + if (cbor_value_advance(array_iter) == CborNoError) { + // Leave the array + if (cbor_value_leave_container(main_iter, array_iter) == CborNoError) { + return ArrayParserState::Complete; + } + } + + return ArrayParserState::Error; +} + +/****************************************************************************** + MESSAGE DECODE FUNCTIONS + ******************************************************************************/ + +CBORMessageDecoder::ArrayParserState CBORMessageDecoder::decodeThingUpdateCmd(CborValue * param, Message * message) { + ThingUpdateCmd * thingCommand = (ThingUpdateCmd *) message; + + // Message is composed of a single parameter, a string (thing_id) + if (!copyCBORStringToArray(param, thingCommand->params.thing_id, sizeof(thingCommand->params.thing_id))) { + return ArrayParserState::Error; + } + + return ArrayParserState::LeaveArray; +} + +CBORMessageDecoder::ArrayParserState CBORMessageDecoder::decodeTimezoneCommandDown(CborValue * param, Message * message) { + TimezoneCommandDown * setTz = (TimezoneCommandDown *) message; + + // Message is composed of 2 parameters, offset 32-bit signed integer and until 32-bit unsigned integer + // Get offset + if (cbor_value_is_integer(param)) { + int64_t val = 0; + if (cbor_value_get_int64(param, &val) == CborNoError) { + setTz->params.offset = static_cast(val); + } + } + + // Next + if (cbor_value_advance(param) != CborNoError) { + return ArrayParserState::Error; + } + + // Get until + if (cbor_value_is_integer(param)) { + uint64_t val = 0; + if (cbor_value_get_uint64(param, &val) == CborNoError) { + setTz->params.until = static_cast(val); + } + } + + return ArrayParserState::LeaveArray; +} + +CBORMessageDecoder::ArrayParserState CBORMessageDecoder::decodeLastValuesUpdateCmd(CborValue * param, Message * message) { + LastValuesUpdateCmd * setLv = (LastValuesUpdateCmd *) message; + + // Message is composed by a single parameter, a variable length byte array. + if (cbor_value_is_byte_string(param)) { + // Cortex M0 is not able to assign a value to pointed memory that is not 32bit aligned + // we use a support variable to cope with that + size_t s; + if (cbor_value_dup_byte_string(param, &setLv->params.last_values, &s, NULL) != CborNoError) { + return ArrayParserState::Error; + } + + setLv->params.length = s; + } + + return ArrayParserState::LeaveArray; +} + +CBORMessageDecoder::ArrayParserState CBORMessageDecoder::decodeOtaUpdateCmdDown(CborValue * param, Message * message) { + CborError error = CborNoError; + OtaUpdateCmdDown * ota = (OtaUpdateCmdDown *) message; + + // Message is composed 4 parameters: id, url, initialSha, finalSha + if (!copyCBORByteToArray(param, ota->params.id, sizeof(ota->params.id))) { + return ArrayParserState::Error; + } + + error = cbor_value_advance(param); + + if ((error != CborNoError) || !copyCBORStringToArray(param, ota->params.url, sizeof(ota->params.url))) { + return ArrayParserState::Error; + } + + error = cbor_value_advance(param); + + if ((error != CborNoError) || !copyCBORByteToArray(param, ota->params.initialSha256, sizeof(ota->params.initialSha256))) { + return ArrayParserState::Error; + } + + error = cbor_value_advance(param); + + if ((error != CborNoError) || !copyCBORByteToArray(param, ota->params.finalSha256, sizeof(ota->params.finalSha256))) { + return ArrayParserState::Error; + } + + return ArrayParserState::LeaveArray; +} + +CBORMessageDecoder::ArrayParserState CBORMessageDecoder::handle_Param(CborValue * param, Message * message) { + + switch (message->id) + { + case CommandId::ThingUpdateCmdId: + return CBORMessageDecoder::decodeThingUpdateCmd(param, message); + + case CommandId::TimezoneCommandDownId: + return CBORMessageDecoder::decodeTimezoneCommandDown(param, message); + + case CommandId::LastValuesUpdateCmdId: + return CBORMessageDecoder::decodeLastValuesUpdateCmd(param, message); + + case CommandId::OtaUpdateCmdDownId: + return CBORMessageDecoder::decodeOtaUpdateCmdDown(param, message); + + default: + return ArrayParserState::MessageNotSupported; + } + + return ArrayParserState::LeaveArray; +} diff --git a/src/cbor/MessageDecoder.h b/src/cbor/MessageDecoder.h new file mode 100644 index 000000000..712289c9d --- /dev/null +++ b/src/cbor/MessageDecoder.h @@ -0,0 +1,74 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef ARDUINO_CBOR_MESSAGE_DECODER_H_ +#define ARDUINO_CBOR_MESSAGE_DECODER_H_ + +/****************************************************************************** + INCLUDE + ******************************************************************************/ + +#include + +#undef max +#undef min +#include + +#include "CBOR.h" +#include "../interfaces/Decoder.h" +#include "lib/tinycbor/cbor-lib.h" + +/****************************************************************************** + CLASS DECLARATION + ******************************************************************************/ + +class CBORMessageDecoder: public Decoder +{ +public: + CBORMessageDecoder() { } + CBORMessageDecoder(CBORMessageDecoder const &) { } + + /* decode a CBOR payload received from the cloud */ + Decoder::Status decode(Message * msg, uint8_t const * const payload, size_t& length); + +private: + + enum class DecoderState { + Success, + MessageNotSupported, + MalformedMessage, + Error + }; + + enum class ArrayParserState { + EnterArray, + ParseParam, + LeaveArray, + Complete, + Error, + MessageNotSupported + }; + + ArrayParserState handle_EnterArray(CborValue * main_iter, CborValue * array_iter); + ArrayParserState handle_Param(CborValue * param, Message * message); + ArrayParserState handle_LeaveArray(CborValue * main_iter, CborValue * array_iter); + + bool ifNumericConvertToDouble(CborValue * value_iter, double * numeric_val); + double convertCborHalfFloatToDouble(uint16_t const half_val); + + // Message specific decoders + ArrayParserState decodeThingUpdateCmd(CborValue * param, Message * message); + ArrayParserState decodeTimezoneCommandDown(CborValue * param, Message * message); + ArrayParserState decodeLastValuesUpdateCmd(CborValue * param, Message * message); + ArrayParserState decodeOtaUpdateCmdDown(CborValue * param, Message * message); + +}; + +#endif /* ARDUINO_CBOR_MESSAGE_DECODER_H_ */ diff --git a/src/cbor/MessageEncoder.cpp b/src/cbor/MessageEncoder.cpp new file mode 100644 index 000000000..cdeb0f8ed --- /dev/null +++ b/src/cbor/MessageEncoder.cpp @@ -0,0 +1,175 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include "CBOREncoder.h" + +#undef max +#undef min +#include +#include + +#include "lib/tinycbor/cbor-lib.h" +#include "MessageEncoder.h" + +/****************************************************************************** + * PUBLIC MEMBER FUNCTIONS + ******************************************************************************/ + +Encoder::Status CBORMessageEncoder::encode(Message * message, uint8_t * data, size_t& len) +{ + EncoderState current_state = EncoderState::EncodeTag, + next_state = EncoderState::Error; + + CborEncoder encoder; + CborEncoder arrayEncoder; + + cbor_encoder_init(&encoder, data, len, 0); + + while (current_state != EncoderState::Complete) { + + switch (current_state) { + case EncoderState::EncodeTag : next_state = handle_EncodeTag(&encoder, message); break; + case EncoderState::EncodeArray : next_state = handle_EncodeArray(&encoder, &arrayEncoder, message); break; + case EncoderState::EncodeParam : next_state = handle_EncodeParam(&arrayEncoder, message); break; + case EncoderState::CloseArray : next_state = handle_CloseArray(&encoder, &arrayEncoder); break; + case EncoderState::Complete : /* Nothing to do */ break; + case EncoderState::MessageNotSupported : + case EncoderState::Error : return Encoder::Status::Error; + } + + current_state = next_state; + } + + len = cbor_encoder_get_buffer_size(&encoder, data); + + return Encoder::Status::Complete; +} + +/****************************************************************************** + PRIVATE MEMBER FUNCTIONS + ******************************************************************************/ + +CBORMessageEncoder::EncoderState CBORMessageEncoder::handle_EncodeTag(CborEncoder * encoder, Message * message) +{ + CborTag commandTag = toCBORCommandTag(message->id); + if (commandTag == CBORCommandTag::CBORUnknownCmdTag16b || + commandTag == CBORCommandTag::CBORUnknownCmdTag32b || + commandTag == CBORCommandTag::CBORUnknownCmdTag64b || + cbor_encode_tag(encoder, commandTag) != CborNoError) { + return EncoderState::Error; + } + + return EncoderState::EncodeArray; +} + +CBORMessageEncoder::EncoderState CBORMessageEncoder::handle_EncodeArray(CborEncoder * encoder, CborEncoder * array_encoder, Message * message) +{ + // Set array size based on the message id + size_t array_size = 0; + switch (message->id) + { + case CommandId::OtaBeginUpId: + array_size = 1; + break; + case CommandId::ThingBeginCmdId: + array_size = 1; + break; + case CommandId::DeviceBeginCmdId: + array_size = 1; + break; + case CommandId::LastValuesBeginCmdId: + break; + case CommandId::OtaProgressCmdUpId: + array_size = 4; + break; + case CommandId::TimezoneCommandUpId: + break; + default: + return EncoderState::MessageNotSupported; + } + + // Start an array with fixed width based on message type + if (cbor_encoder_create_array(encoder, array_encoder, array_size) != CborNoError){ + return EncoderState::Error; + } + + return EncoderState::EncodeParam; +} + +CBORMessageEncoder::EncoderState CBORMessageEncoder::handle_EncodeParam(CborEncoder * array_encoder, Message * message) +{ + CborError error = CborNoError; + switch (message->id) + { + case CommandId::OtaBeginUpId: + error = CBORMessageEncoder::encodeOtaBeginUp(array_encoder, message); + break; + case CommandId::ThingBeginCmdId: + error = CBORMessageEncoder::encodeThingBeginCmd(array_encoder, message); + break; + case CommandId::DeviceBeginCmdId: + error = CBORMessageEncoder::encodeDeviceBeginCmd(array_encoder, message); + break; + case CommandId::LastValuesBeginCmdId: + break; + case CommandId::OtaProgressCmdUpId: + error = CBORMessageEncoder::encodeOtaProgressCmdUp(array_encoder, message); + break; + case CommandId::TimezoneCommandUpId: + break; + default: + return EncoderState::MessageNotSupported; + } + + return (error != CborNoError) ? EncoderState::Error : EncoderState::CloseArray; +} + +CBORMessageEncoder::EncoderState CBORMessageEncoder::handle_CloseArray(CborEncoder * encoder, CborEncoder * array_encoder) +{ + CborError error = cbor_encoder_close_container(encoder, array_encoder); + + return (error != CborNoError) ? EncoderState::Error : EncoderState::Complete; +} + +// Message specific encoders +CborError CBORMessageEncoder::encodeOtaBeginUp(CborEncoder * array_encoder, Message * message) +{ + OtaBeginUp * otaBeginUp = (OtaBeginUp *) message; + CHECK_CBOR(cbor_encode_byte_string(array_encoder, otaBeginUp->params.sha, SHA256_SIZE)); + return CborNoError; +} + +CborError CBORMessageEncoder::encodeThingBeginCmd(CborEncoder * array_encoder, Message * message) +{ + ThingBeginCmd * thingBeginCmd = (ThingBeginCmd *) message; + CHECK_CBOR(cbor_encode_text_stringz(array_encoder, thingBeginCmd->params.thing_id)); + return CborNoError; +} + +CborError CBORMessageEncoder::encodeDeviceBeginCmd(CborEncoder * array_encoder, Message * message) +{ + DeviceBeginCmd * deviceBeginCmd = (DeviceBeginCmd *) message; + CHECK_CBOR(cbor_encode_text_stringz(array_encoder, deviceBeginCmd->params.lib_version)); + return CborNoError; +} + +CborError CBORMessageEncoder::encodeOtaProgressCmdUp(CborEncoder * array_encoder, Message * message) +{ + OtaProgressCmdUp * ota = (OtaProgressCmdUp *)message; + CHECK_CBOR(cbor_encode_byte_string(array_encoder, ota->params.id, ID_SIZE)); + CHECK_CBOR(cbor_encode_simple_value(array_encoder, ota->params.state)); + CHECK_CBOR(cbor_encode_int(array_encoder, ota->params.state_data)); + CHECK_CBOR(cbor_encode_uint(array_encoder, ota->params.time)); + return CborNoError; +} diff --git a/src/cbor/MessageEncoder.h b/src/cbor/MessageEncoder.h new file mode 100644 index 000000000..86792eb19 --- /dev/null +++ b/src/cbor/MessageEncoder.h @@ -0,0 +1,65 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef ARDUINO_CBOR_MESSAGE_ENCODER_H_ +#define ARDUINO_CBOR_MESSAGE_ENCODER_H_ + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include + +#undef max +#undef min +#include + +#include "CBOR.h" +#include "../interfaces/Encoder.h" +#include "lib/tinycbor/cbor-lib.h" + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class CBORMessageEncoder: public Encoder +{ + +public: + CBORMessageEncoder() { } + CBORMessageEncoder(CborEncoder const &) { } + Encoder::Status encode(Message * message, uint8_t * data, size_t& len); + +private: + + enum class EncoderState + { + EncodeTag, + EncodeArray, + EncodeParam, + CloseArray, + MessageNotSupported, + Complete, + Error + }; + + EncoderState handle_EncodeTag(CborEncoder * encoder, Message * message); + EncoderState handle_EncodeArray(CborEncoder * encoder, CborEncoder * array_encoder, Message * message); + EncoderState handle_EncodeParam(CborEncoder * array_encoder, Message * message); + EncoderState handle_CloseArray(CborEncoder * encoder, CborEncoder * array_encoder); + + // Message specific encoders + CborError encodeThingBeginCmd(CborEncoder * array_encoder, Message * message); + CborError encodeOtaBeginUp(CborEncoder * array_encoder, Message * message); + CborError encodeDeviceBeginCmd(CborEncoder * array_encoder, Message * message); + CborError encodeOtaProgressCmdUp(CborEncoder * array_encoder, Message * message); +}; + +#endif /* ARDUINO_CBOR_MESSAGE_ENCODER_H_ */ diff --git a/src/interfaces/CloudProcess.h b/src/interfaces/CloudProcess.h new file mode 100644 index 000000000..7462e867f --- /dev/null +++ b/src/interfaces/CloudProcess.h @@ -0,0 +1,56 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef ARDUINO_IOT_CLOUD_PROCESS +#define ARDUINO_IOT_CLOUD_PROCESS + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include +#include +#include +#include + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class CloudProcess { +public: + CloudProcess(MessageStream* stream): stream(stream) {} + + /** + * Abstract method that is called whenever a message comes from Message stream + * @param m: the incoming message + */ + virtual void handleMessage(Message* m) = 0; + + /** + * Abstract method that is called to update the FSM of the CloudProcess + */ + virtual void update() = 0; + +protected: + /** + * Used by a derived class to send a message to the underlying messageStream + * @param msg: the message to send + */ + void deliver(Message* msg) { + assert(stream != nullptr); + stream->sendUpstream(msg); + } + +private: + MessageStream* stream; +}; + +#endif /* ARDUINO_IOT_CLOUD_PROCESS */ diff --git a/src/interfaces/Decoder.h b/src/interfaces/Decoder.h new file mode 100644 index 000000000..fc725ec8b --- /dev/null +++ b/src/interfaces/Decoder.h @@ -0,0 +1,41 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include +#include + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class Decoder { +public: + enum Status: uint8_t { + Complete, + InProgress, + Error + }; + + /** + * Decode a buffer into a provided message structure + * @param msg: the message structure that is going to be filled with data provided in the buffer + * @param buf: the incoming buffer that needs to be decoded + * @param len: the length of the incoming buffer, value will be updated with the used len of the buffer + * @return SUCCESS: if the message is decoded correctly + * ERROR: if the message wasn't decoded correctly + */ + virtual Status decode(Message* msg, const uint8_t* const buf, size_t &len) = 0; +}; diff --git a/src/interfaces/Encoder.h b/src/interfaces/Encoder.h new file mode 100644 index 000000000..38fbe0edd --- /dev/null +++ b/src/interfaces/Encoder.h @@ -0,0 +1,40 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ +#include +#include + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class Encoder { +public: + enum Status: uint8_t { + Complete, + InProgress, + Error + }; + + /** + * Encode a message into a buffer in a single shot + * @param msg: the message that needs to be encoded + * @param buf: the buffer the message will be encoded into + * @param len: the length of the provided buffer, value will be updated with the consumed len of the buffer + * @return SUCCESS: if the message is encoded correctly + * ERROR: error during the encoding of the message + */ + virtual Status encode(Message* msg, uint8_t* buf, size_t& len) = 0; +}; diff --git a/src/interfaces/MessageStream.h b/src/interfaces/MessageStream.h new file mode 100644 index 000000000..ac2b18ac9 --- /dev/null +++ b/src/interfaces/MessageStream.h @@ -0,0 +1,40 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include +#include + +using upstreamFunction = std::function; + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class MessageStream { +public: + MessageStream(upstreamFunction upstream): upstream(upstream) {} + + /** + * Send message upstream + * @param m: message to send + */ + virtual inline void sendUpstream(Message* m) { + upstream(m); + } + +private: + upstreamFunction upstream; +}; diff --git a/src/message/Commands.h b/src/message/Commands.h new file mode 100644 index 000000000..ce71f9c81 --- /dev/null +++ b/src/message/Commands.h @@ -0,0 +1,149 @@ +/* + This file is part of the ArduinoIoTCloud library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include +#include + +/****************************************************************************** + * DEFINE + ******************************************************************************/ + +#define THING_ID_SIZE 37 +#define SHA256_SIZE 32 +#define URL_SIZE 256 +#define ID_SIZE 16 +#define MAX_LIB_VERSION_SIZE 10 + +/****************************************************************************** + TYPEDEF + ******************************************************************************/ + +enum CommandId: uint32_t { + + /* Device commands */ + DeviceBeginCmdId, + ThingBeginCmdId, + ThingUpdateCmdId, + DeviceRegisteredCmdId, + DeviceAttachedCmdId, + DeviceDetachedCmdId, + + /* Thing commands */ + LastValuesBeginCmdId, + LastValuesUpdateCmdId, + PropertiesUpdateCmdId, + + /* Generic commands */ + ResetCmdId, + + /* OTA commands */ + OtaBeginUpId, + OtaProgressCmdUpId, + OtaUpdateCmdDownId, + + /* Timezone commands */ + TimezoneCommandUpId, + TimezoneCommandDownId, + + /* Unknown command id */ + UnknownCmdId +}; + +struct Command { + CommandId id; +}; + +typedef Command Message; + +struct DeviceBeginCmd { + Command c; + struct { + char lib_version[MAX_LIB_VERSION_SIZE]; + } params; +}; + +struct ThingBeginCmd { + Command c; + struct { + char thing_id[THING_ID_SIZE]; + } params; +}; + +struct ThingUpdateCmd { + Command c; + struct { + char thing_id[THING_ID_SIZE]; + } params; +}; + +struct LastValuesBeginCmd { + Command c; +}; + +struct LastValuesUpdateCmd { + Command c; + struct { + uint8_t * last_values; + size_t length; + } params; +}; + +struct OtaBeginUp { + Command c; + struct { + uint8_t sha [SHA256_SIZE]; + } params; +}; + +struct OtaProgressCmdUp { + Command c; + struct { + uint8_t id[ID_SIZE]; + uint8_t state; + int32_t state_data; + uint64_t time; + } params; +}; + +struct OtaUpdateCmdDown { + Command c; + struct { + uint8_t id[ID_SIZE]; + char url[URL_SIZE]; + uint8_t initialSha256[SHA256_SIZE]; + uint8_t finalSha256[SHA256_SIZE]; + } params; +}; + +struct TimezoneCommandUp { + Command c; +}; + +struct TimezoneCommandDown { + Command c; + struct { + int32_t offset; + uint32_t until; + } params; +}; + +union CommandDown { + struct Command c; + struct OtaUpdateCmdDown otaUpdateCmdDown; + struct ThingUpdateCmd thingUpdateCmd; + struct LastValuesUpdateCmd lastValuesUpdateCmd; + struct TimezoneCommandDown timezoneCommandDown; +}; diff --git a/src/property/Property.cpp b/src/property/Property.cpp index b26494ed9..6297c09cb 100644 --- a/src/property/Property.cpp +++ b/src/property/Property.cpp @@ -29,6 +29,7 @@ Property::Property() , _min_delta_property{0.0f} , _min_time_between_updates_millis{DEFAULT_MIN_TIME_BETWEEN_UPDATES_MILLIS} , _permission{Permission::Read} +, _write_policy{WritePolicy::Auto} , _get_time_func{nullptr} , _update_callback_func{nullptr} , _on_sync_callback_func{nullptr} @@ -102,6 +103,18 @@ Property & Property::encodeTimestamp() return (*this); } +Property & Property::writeOnChange() +{ + _write_policy = WritePolicy::Auto; + return (*this); +} + +Property & Property::writeOnDemand() +{ + _write_policy = WritePolicy::Manual; + return (*this); +} + void Property::setTimestamp(unsigned long const timestamp) { _timestamp = timestamp; diff --git a/src/property/Property.h b/src/property/Property.h index 7393da192..71a8447c9 100644 --- a/src/property/Property.h +++ b/src/property/Property.h @@ -125,6 +125,10 @@ enum class UpdatePolicy { OnChange, TimeInterval, OnDemand }; +enum class WritePolicy { + Auto, Manual +}; + typedef void(*UpdateCallbackFunc)(void); typedef unsigned long(*GetTimeCallbackFunc)(); class Property; @@ -147,6 +151,8 @@ class Property Property & publishEvery(unsigned long const seconds); Property & publishOnDemand(); Property & encodeTimestamp(); + Property & writeOnChange(); + Property & writeOnDemand(); inline String name() const { return _name; @@ -160,6 +166,9 @@ class Property inline bool isWriteableByCloud() const { return (_permission == Permission::Write) || (_permission == Permission::ReadWrite); } + inline bool isWritableOnChange() const { + return _write_policy == WritePolicy::Auto; + } void setTimestamp(unsigned long const timestamp); bool shouldBeUpdated(); @@ -209,6 +218,7 @@ class Property private: Permission _permission; + WritePolicy _write_policy; GetTimeCallbackFunc _get_time_func; UpdateCallbackFunc _update_callback_func; OnSyncCallbackFunc _on_sync_callback_func; @@ -219,7 +229,7 @@ class Property _has_been_appended_but_not_sended; /* Variables used for UpdatePolicy::TimeInterval */ unsigned long _last_updated_millis, - _update_interval_millis; + _update_interval_millis; /* Variables used for reconnection sync*/ unsigned long _last_local_change_timestamp; unsigned long _last_cloud_change_timestamp; diff --git a/src/property/PropertyContainer.cpp b/src/property/PropertyContainer.cpp index 3ab4e75e0..1ea1edcaa 100644 --- a/src/property/PropertyContainer.cpp +++ b/src/property/PropertyContainer.cpp @@ -121,7 +121,9 @@ void updateProperty(PropertyContainer & prop_cont, String propertyName, unsigned if (is_sync_message) { property->execCallbackOnSync(); } else { - property->fromCloudToLocal(); + if (property->isWritableOnChange()) { + property->fromCloudToLocal(); + } property->execCallbackOnChange(); property->provideEcho(); } diff --git a/src/utility/time/TimeService.h b/src/utility/time/TimeService.h index 6e6af6bb5..794ec344d 100644 --- a/src/utility/time/TimeService.h +++ b/src/utility/time/TimeService.h @@ -56,6 +56,8 @@ class TimeServiceClass */ static unsigned long getTimeFromString(const String& input); + static bool isTimeValid(unsigned long const time); + private: ConnectionHandler * _con_hdl; @@ -74,7 +76,6 @@ class TimeServiceClass void initRTC(); void setRTC(unsigned long time); unsigned long getRTC(); - static bool isTimeValid(unsigned long const time); static bool isTimeZoneOffsetValid(long const offset); }; diff --git a/src/utility/time/TimedAttempt.cpp b/src/utility/time/TimedAttempt.cpp new file mode 100644 index 000000000..01da506d7 --- /dev/null +++ b/src/utility/time/TimedAttempt.cpp @@ -0,0 +1,79 @@ +/* + This file is part of the Arduino_SecureElement library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include +#include "TimedAttempt.h" + +/****************************************************************************** + * CTOR/DTOR + ******************************************************************************/ + +TimedAttempt::TimedAttempt(unsigned long minDelay, unsigned long maxDelay) +: _minDelay(minDelay) +, _maxDelay(maxDelay) { +} + +/****************************************************************************** + * PUBLIC MEMBER FUNCTIONS + ******************************************************************************/ + +void TimedAttempt::begin(unsigned long delay) { + _retryCount = 0; + _minDelay = delay; + _maxDelay = delay; +} + +void TimedAttempt::begin(unsigned long minDelay, unsigned long maxDelay) { + _retryCount = 0; + _minDelay = minDelay; + _maxDelay = maxDelay; +} + +unsigned long TimedAttempt::reconfigure(unsigned long minDelay, unsigned long maxDelay) { + _minDelay = minDelay; + _maxDelay = maxDelay; + return reload(); +} + +unsigned long TimedAttempt::retry() { + _retryCount++; + return reload(); +} + +unsigned long TimedAttempt::reload() { + unsigned long retryDelay = (1 << _retryCount) * _minDelay; + _retryDelay = min(retryDelay, _maxDelay); + _nextRetryTick = millis() + retryDelay; + return retryDelay; +} + +void TimedAttempt::reset() { + _retryCount = 0; +} + +bool TimedAttempt::isRetry() { + return _retryCount > 0; +} + +bool TimedAttempt::isExpired() { + return millis() > _nextRetryTick; +} + +unsigned int TimedAttempt::getRetryCount() { + return _retryCount; +} + +unsigned int TimedAttempt::getWaitTime() { + return _retryDelay; +} diff --git a/src/utility/time/TimedAttempt.h b/src/utility/time/TimedAttempt.h new file mode 100644 index 000000000..67a1931c0 --- /dev/null +++ b/src/utility/time/TimedAttempt.h @@ -0,0 +1,42 @@ +/* + This file is part of the Arduino_SecureElement library. + + Copyright (c) 2024 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +#ifndef TIMED_ATTEMPT_H +#define TIMED_ATTEMPT_H + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class TimedAttempt { + +public: + TimedAttempt(unsigned long minDelay, unsigned long maxDelay); + + void begin(unsigned long delay); + void begin(unsigned long minDelay, unsigned long maxDelay); + unsigned long reconfigure(unsigned long minDelay, unsigned long maxDelay); + unsigned long retry(); + unsigned long reload(); + void reset(); + bool isRetry(); + bool isExpired(); + unsigned int getRetryCount(); + unsigned int getWaitTime(); + +private: + unsigned long _minDelay; + unsigned long _maxDelay; + unsigned long _nextRetryTick; + unsigned long _retryDelay; + unsigned int _retryCount; +}; + +#endif /* TIMED_ATTEMPT_H */