Skip to content

Commit cc14cb5

Browse files
committed
[chip-tool] Add chip-tool dcl tc-display and tc-display-by-payload commands
1 parent d8fdbc1 commit cc14cb5

File tree

5 files changed

+344
-4
lines changed

5 files changed

+344
-4
lines changed

examples/chip-tool/BUILD.gn

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ static_library("chip-tool-utils") {
7272
"commands/common/RemoteDataModelLogger.h",
7373
"commands/dcl/DCLClient.cpp",
7474
"commands/dcl/DCLClient.h",
75+
"commands/dcl/DisplayTermsAndConditions.cpp",
76+
"commands/dcl/DisplayTermsAndConditions.h",
7577
"commands/dcl/HTTPSRequest.cpp",
7678
"commands/dcl/HTTPSRequest.h",
7779
"commands/dcl/JsonSchemaMacros.cpp",

examples/chip-tool/commands/dcl/Commands.h

+6-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ void registerCommandsDCL(Commands & commands)
2525
{
2626
const char * clusterName = "DCL";
2727
commands_list clusterCommands = {
28-
make_unique<DCLModelCommand>(), //
29-
make_unique<DCLModelByPayloadCommand>(), //
30-
make_unique<DCLTCCommand>(), //
31-
make_unique<DCLTCByPayloadCommand>(), //
28+
make_unique<DCLModelCommand>(), //
29+
make_unique<DCLModelByPayloadCommand>(), //
30+
make_unique<DCLTCCommand>(), //
31+
make_unique<DCLTCByPayloadCommand>(), //
32+
make_unique<DCLTCDisplayCommand>(), //
33+
make_unique<DCLTCDisplayByPayloadCommand>(), //
3234
};
3335

3436
commands.RegisterCommandSet(clusterName, clusterCommands, "Commands to interact with the DCL.");

examples/chip-tool/commands/dcl/DCLCommands.h

+62
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "../common/Command.h"
2020

2121
#include "DCLClient.h"
22+
#include "DisplayTermsAndConditions.h"
2223

2324
class DCLCommandBase : public Command
2425
{
@@ -138,3 +139,64 @@ class DCLTCCommand : public DCLIdsCommandBase
138139
return CHIP_NO_ERROR;
139140
}
140141
};
142+
143+
class DCLTCDisplayByPayloadCommand : public DCLPayloadCommandBase
144+
{
145+
public:
146+
DCLTCDisplayByPayloadCommand() : DCLPayloadCommandBase("tc-display-by-payload")
147+
{
148+
AddArgument("country-code", &mCountryCode,
149+
"The country code to retrieve terms and conditions for. Defaults to the country configured in the DCL.");
150+
AddArgument("language-code", &mLanguageCode,
151+
"The language code to retrieve terms and conditions for. Defaults to the language configured for the chosen "
152+
"country in the DCL.");
153+
}
154+
155+
CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client)
156+
{
157+
Json::Value tc;
158+
ReturnErrorOnFailure(client.TermsAndConditions(mPayload, tc));
159+
VerifyOrReturnError(tc != Json::nullValue, CHIP_NO_ERROR);
160+
161+
uint16_t version = 0;
162+
uint16_t userResponse = 0;
163+
ReturnErrorOnFailure(chip::tool::dcl::DisplayTermsAndConditions(tc, version, userResponse, mCountryCode, mLanguageCode));
164+
165+
ChipLogProgress(chipTool, "\nTerms and conditions\n\tRevision : %u\n\tUserResponse: %u", version, userResponse);
166+
return CHIP_NO_ERROR;
167+
}
168+
169+
private:
170+
chip::Optional<char *> mCountryCode;
171+
chip::Optional<char *> mLanguageCode;
172+
};
173+
174+
class DCLTCDisplayCommand : public DCLIdsCommandBase
175+
{
176+
public:
177+
DCLTCDisplayCommand() : DCLIdsCommandBase("tc-display")
178+
{
179+
AddArgument("country-code", &mCountryCode,
180+
"The country code to retrieve terms and conditions for. Defaults to the country configured in the DCL.");
181+
AddArgument("language-code", &mLanguageCode,
182+
"The language code to retrieve terms and conditions for. Defaults to the language configured for the chosen "
183+
"country in the DCL.");
184+
}
185+
CHIP_ERROR RunCommand(chip::tool::dcl::DCLClient & client)
186+
{
187+
Json::Value tc;
188+
ReturnErrorOnFailure(client.TermsAndConditions(static_cast<chip::VendorId>(mVendorId), mProductId, tc));
189+
VerifyOrReturnError(tc != Json::nullValue, CHIP_NO_ERROR);
190+
191+
uint16_t version = 0;
192+
uint16_t userResponse = 0;
193+
ReturnErrorOnFailure(chip::tool::dcl::DisplayTermsAndConditions(tc, version, userResponse, mCountryCode, mLanguageCode));
194+
195+
ChipLogProgress(chipTool, "\nTerms and conditions\n\tRevision : %u\n\tUserResponse: %u", version, userResponse);
196+
return CHIP_NO_ERROR;
197+
}
198+
199+
private:
200+
chip::Optional<char *> mCountryCode;
201+
chip::Optional<char *> mLanguageCode;
202+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/*
2+
* Copyright (c) 2024 Project CHIP Authors
3+
* All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
#include "DisplayTermsAndConditions.h"
20+
21+
#include <lib/support/CodeUtils.h>
22+
#include <lib/support/SafeInt.h>
23+
24+
#include <iostream>
25+
#include <regex>
26+
#include <unordered_map>
27+
28+
namespace chip {
29+
namespace tool {
30+
namespace dcl {
31+
namespace {
32+
constexpr const char * kAcceptTerms = "Do you accept these terms? [<b>Y</b>/n]: ";
33+
constexpr const char * kRequiredTerms = "Required";
34+
constexpr const char * kOptionalTerms = "Optional";
35+
constexpr const char * kTitleAllowedTags = R"(<(/?)(b|em|i|small|strong|u)>)";
36+
constexpr const char * kTextAllowedTags = R"(<(/?)(b|br|em|h1|h2|h3|h4|h5|h6|hr|i|li|ol|p|small|strong|u|ul)>)";
37+
constexpr const char * kAnsiCodeReset = "\033[0m";
38+
constexpr const char * kAnsiCodeBold = "\033[1m";
39+
constexpr const char * kAnsiCodeFaint = "\033[2m";
40+
constexpr const char * kAnsiCodeItalics = "\033[3m";
41+
constexpr const char * kAnsiCodeUnderline = "\033[4m";
42+
constexpr const char * kLineBreak = "\n";
43+
constexpr const char * kListItem = " - ";
44+
constexpr const char * kHorizontalLine = "\n==========================================\n";
45+
constexpr const char * kErrorInvalidInput = "Invalid input. Please enter 'Y' (yes) or 'N' (no). Default is 'Y'.";
46+
47+
// Fields names for the ESF JSON schema
48+
constexpr const char * kFieldCountryEntries = "countryEntries";
49+
constexpr const char * kFieldDefaultCountry = "defaultCountry";
50+
constexpr const char * kFieldLanguageEntries = "languageEntries";
51+
constexpr const char * kFieldDefaultLanguage = "defaultLanguage";
52+
constexpr const char * kFieldOrdinal = "ordinal";
53+
constexpr const char * kFieldRequired = "required";
54+
constexpr const char * kFieldSchemaVersion = "schemaVersion";
55+
constexpr const char * kFieldText = "text";
56+
constexpr const char * kFieldTitle = "title";
57+
58+
const std::unordered_map<std::string, std::string> kHtmlToAnsiCodes = {
59+
{ "b", kAnsiCodeBold }, //
60+
{ "br", kLineBreak }, //
61+
{ "em", kAnsiCodeItalics }, //
62+
{ "h1", kAnsiCodeBold }, //
63+
{ "h2", kAnsiCodeBold }, //
64+
{ "h3", kAnsiCodeBold }, //
65+
{ "h4", kAnsiCodeBold }, //
66+
{ "h5", kAnsiCodeBold }, //
67+
{ "h6", kAnsiCodeBold }, //
68+
{ "hr", kHorizontalLine }, //
69+
{ "i", kAnsiCodeItalics }, //
70+
{ "li", kListItem }, //
71+
{ "ol", kLineBreak }, //
72+
{ "p", kLineBreak }, //
73+
{ "small", kAnsiCodeFaint }, //
74+
{ "strong", kAnsiCodeBold }, //
75+
{ "u", kAnsiCodeUnderline }, //
76+
{ "ul", kLineBreak }, //
77+
};
78+
79+
std::string ToUpperCase(const std::string & input)
80+
{
81+
std::string output = input;
82+
std::transform(output.begin(), output.end(), output.begin(), [](unsigned char c) { return std::toupper(c); });
83+
return output;
84+
}
85+
86+
std::string ToLowerCase(const std::string & input)
87+
{
88+
std::string output = input;
89+
std::transform(output.begin(), output.end(), output.begin(), [](unsigned char c) { return std::tolower(c); });
90+
return output;
91+
}
92+
93+
std::string Center(const std::string & text)
94+
{
95+
size_t lineWidth = strlen(kHorizontalLine) - 1;
96+
if (text.length() >= lineWidth)
97+
{
98+
return text; // No padding if the text is longer than the width
99+
}
100+
101+
size_t totalPadding = lineWidth - text.length();
102+
size_t paddingLeft = totalPadding / 2;
103+
size_t paddingRight = totalPadding - paddingLeft;
104+
105+
return std::string(paddingLeft, ' ') + text + std::string(paddingRight, ' ');
106+
}
107+
108+
std::string HTMLTagToAnsiCode(const std::smatch & match)
109+
{
110+
if (match[1] == "/")
111+
{
112+
return kAnsiCodeReset;
113+
}
114+
115+
std::string tag = match[2];
116+
auto ansiCode = kHtmlToAnsiCodes.find(ToLowerCase(tag));
117+
if (ansiCode == kHtmlToAnsiCodes.end())
118+
{
119+
return "<" + tag + ">";
120+
}
121+
122+
return ansiCode->second;
123+
}
124+
125+
std::string renderHTMLInTerminal(const std::string & html, const std::string & allowedTags = kTextAllowedTags)
126+
{
127+
std::string formattedText;
128+
std::string::const_iterator current = html.cbegin();
129+
130+
std::regex regex(allowedTags, std::regex_constants::icase);
131+
for (std::sregex_iterator it(html.cbegin(), html.cend(), regex), end; it != end; ++it)
132+
{
133+
const auto & match = *it;
134+
135+
formattedText += std::string(current, html.cbegin() + match.position());
136+
formattedText += HTMLTagToAnsiCode(match);
137+
138+
current = html.cbegin() + match.position() + match.length();
139+
}
140+
141+
formattedText += std::string(current, html.cend());
142+
formattedText += kAnsiCodeReset;
143+
return formattedText;
144+
}
145+
146+
const char * ResolveValueOrDefault(const Json::Value & entries, const Optional<const char *> & userValue, const char * defaultValue,
147+
const char * valueType)
148+
{
149+
const char * resolvedValue = userValue.ValueOr(defaultValue);
150+
151+
if (userValue.HasValue() && !entries.isMember(resolvedValue))
152+
{
153+
ChipLogProgress(chipTool, "User-chosen %s ('%s') not found. Defaulting to '%s'", valueType, userValue.Value(),
154+
defaultValue);
155+
resolvedValue = defaultValue;
156+
}
157+
158+
return resolvedValue;
159+
}
160+
161+
const Json::Value & GetTexts(const Json::Value & tc, Optional<const char *> optionalCountryCode,
162+
Optional<const char *> optionalLanguageCode)
163+
{
164+
const char * defaultCountry = tc[kFieldDefaultCountry].asCString();
165+
const char * chosenCountry = ResolveValueOrDefault(tc[kFieldCountryEntries], optionalCountryCode, defaultCountry, "country");
166+
auto & countryEntry = tc[kFieldCountryEntries][chosenCountry];
167+
168+
const char * defaultLanguage = countryEntry[kFieldDefaultLanguage].asCString();
169+
const char * chosenLanguage =
170+
ResolveValueOrDefault(countryEntry[kFieldLanguageEntries], optionalLanguageCode, defaultLanguage, "language");
171+
auto & languageEntry = countryEntry[kFieldLanguageEntries][chosenLanguage];
172+
173+
return languageEntry;
174+
}
175+
176+
void PrintText(const Json::Value & json)
177+
{
178+
auto title = renderHTMLInTerminal(Center(ToUpperCase(json[kFieldTitle].asCString())), kTitleAllowedTags);
179+
auto text = renderHTMLInTerminal(json[kFieldText].asCString());
180+
auto userQuestion = renderHTMLInTerminal(kAcceptTerms);
181+
auto required = json[kFieldRequired].asBool() ? kRequiredTerms : kOptionalTerms;
182+
183+
printf("%s", kHorizontalLine);
184+
printf("%s", title.c_str());
185+
printf("%s", kHorizontalLine);
186+
printf("%s", text.c_str());
187+
printf("%s", kHorizontalLine);
188+
printf("[%s] %s", required, userQuestion.c_str());
189+
}
190+
191+
bool AcknowledgeText()
192+
{
193+
while (true)
194+
{
195+
std::string userInput;
196+
std::getline(std::cin, userInput);
197+
198+
VerifyOrReturnValue(!userInput.empty() && userInput != "Y" && userInput != "y", true);
199+
VerifyOrReturnValue(userInput != "N" && userInput != "n", false);
200+
201+
ChipLogError(chipTool, "%s", kErrorInvalidInput);
202+
}
203+
}
204+
205+
} // namespace
206+
207+
CHIP_ERROR DisplayTermsAndConditions(const Json::Value & tc, uint16_t & outVersion, uint16_t & outUserResponse,
208+
Optional<const char *> countryCode, Optional<const char *> languageCode)
209+
{
210+
VerifyOrReturnError(CanCastTo<uint16_t>(tc[kFieldSchemaVersion].asUInt()), CHIP_ERROR_INVALID_ARGUMENT);
211+
outVersion = static_cast<uint16_t>(tc[kFieldSchemaVersion].asUInt());
212+
213+
auto texts = GetTexts(tc, countryCode, languageCode);
214+
for (const auto & text : texts)
215+
{
216+
PrintText(text);
217+
218+
if (AcknowledgeText())
219+
{
220+
auto ordinal = text[kFieldOrdinal].asUInt();
221+
VerifyOrReturnError(ordinal < 16, CHIP_ERROR_INVALID_ARGUMENT); // Only 16 bits are available for user response
222+
uint16_t shiftedValue = static_cast<uint16_t>((1U << (ordinal & 0x0F)) & 0xFFFF);
223+
outUserResponse |= shiftedValue;
224+
}
225+
}
226+
return CHIP_NO_ERROR;
227+
}
228+
} // namespace dcl
229+
} // namespace tool
230+
} // namespace chip
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2024 Project CHIP Authors
3+
* All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
#include <lib/core/CHIPError.h>
20+
#include <lib/core/Optional.h>
21+
22+
#include <json/json.h>
23+
24+
namespace chip {
25+
namespace tool {
26+
namespace dcl {
27+
/**
28+
* Display the terms and conditions to the user and prompt for acceptance.
29+
*
30+
* @param[in] tc The terms and conditions JSON object.
31+
* @param[out] outVersion The schema version of the terms and conditions.
32+
* @param[out] outUserResponse The user response as a bitfield where each bit corresponds to the ordinal of the text.
33+
* @param[in] countryCode The country code to use for the terms and conditions. If not provided, the default country will be used.
34+
* @param[in] languageCode The language code to use for the terms and conditions. If not provided, the default language will be
35+
* used.
36+
*
37+
* @return CHIP_NO_ERROR on success, error code otherwise.
38+
*/
39+
CHIP_ERROR DisplayTermsAndConditions(const Json::Value & tc, uint16_t & outVersion, uint16_t & outUserResponse,
40+
Optional<const char *> countryCode = NullOptional,
41+
Optional<const char *> languageCode = NullOptional);
42+
} // namespace dcl
43+
} // namespace tool
44+
} // namespace chip

0 commit comments

Comments
 (0)