blob: 164ad1436e01abe6aef52644e41a1bc9fd1bade2 [file] [log] [blame]
/*
* Copyright (c) 2020 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#include <assert.h>
#include <chrono>
#include <errno.h>
#include <iostream>
#include <new>
#include <sstream>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <core/CHIPError.h>
#include <inet/InetLayer.h>
#include <inet/UDPEndPoint.h>
#include <platform/ConnectivityManager.h>
#include <support/CHIPLogging.h>
#include <support/CodeUtils.h>
#include <support/ErrorStr.h>
#include <controller/CHIPDeviceController.h>
#include <app/chip-zcl-zpro-codec.h>
// Delay, in seconds, between sends for the echo case.
#define SEND_DELAY 5
// Limits on endpoint values. Could be wrong, if we start using endpoint 0 for
// something.
#define CHIP_ZCL_ENDPOINT_MIN 0x01
#define CHIP_ZCL_ENDPOINT_MAX 0xF0
using namespace ::chip;
using namespace ::chip::Inet;
// NOTE: Remote device ID is in sync with the echo server device id
// At some point, we may want to add an option to connect to a device without
// knowing its id, because the ID can be learned on the first response that is received.
constexpr NodeId kLocalDeviceId = 112233;
constexpr NodeId kRemoteDeviceId = 12344321;
constexpr std::chrono::seconds kWaitingForResponseTimeout(1);
static const unsigned char local_private_key[] = { 0x00, 0xd1, 0x90, 0xd9, 0xb3, 0x95, 0x1c, 0x5f, 0xa4, 0xe7, 0x47,
0x92, 0x5b, 0x0a, 0xa9, 0xa7, 0xc1, 0x1c, 0xe7, 0x06, 0x10, 0xe2,
0xdd, 0x16, 0x41, 0x52, 0x55, 0xb7, 0xb8, 0x80, 0x8d, 0x87, 0xa1 };
static const unsigned char remote_public_key[] = { 0x04, 0xe2, 0x07, 0x64, 0xff, 0x6f, 0x6a, 0x91, 0xd9, 0xc2, 0xc3, 0x0a, 0xc4,
0x3c, 0x56, 0x4b, 0x42, 0x8a, 0xf3, 0xb4, 0x49, 0x29, 0x39, 0x95, 0xa2, 0xf7,
0x02, 0x8c, 0xa5, 0xce, 0xf3, 0xc9, 0xca, 0x24, 0xc5, 0xd4, 0x5c, 0x60, 0x79,
0x48, 0x30, 0x3c, 0x53, 0x86, 0xd9, 0x23, 0xe6, 0x61, 0x1f, 0x5a, 0x3d, 0xdf,
0x9f, 0xdc, 0x35, 0xea, 0xd0, 0xde, 0x16, 0x7e, 0x64, 0xde, 0x7f, 0x3c, 0xa6 };
static const char * PAYLOAD = "Message from Standalone CHIP echo client!";
bool isDeviceConnected = false;
static bool waitingForResponse = true;
// Device Manager Callbacks
static void OnConnect(DeviceController::ChipDeviceController * controller, Transport::PeerConnectionState * state,
void * appReqState)
{
isDeviceConnected = true;
if (state != NULL)
{
CHIP_ERROR err = controller->ManualKeyExchange(state, remote_public_key, sizeof(remote_public_key), local_private_key,
sizeof(local_private_key));
if (err != CHIP_NO_ERROR)
{
fprintf(stderr, "Failed to exchange keys\n");
}
}
}
static bool ContentMayBeADataModelMessage(System::PacketBuffer * buffer)
{
// A data model message has a first byte whose value is always one of 0x00,
// 0x01, 0x02, 0x03.
return buffer->DataLength() > 0 && buffer->Start()[0] < 0x04;
}
// This function consumes (i.e. frees) the buffer.
static void HandleDataModelMessage(System::PacketBuffer * buffer)
{
EmberApsFrame frame;
if (extractApsFrame(buffer->Start(), buffer->DataLength(), &frame) == 0)
{
printf("APS frame processing failure!\n");
System::PacketBuffer::Free(buffer);
return;
}
printf("APS frame processing success!\n");
uint8_t * message;
uint16_t messageLen = extractMessage(buffer->Start(), buffer->DataLength(), &message);
VerifyOrExit(messageLen >= 3, printf("Unexpected response length: %d\n", messageLen));
// Bit 3 of the frame control byte set means direction is server to client.
// We expect no other bits to be set.
VerifyOrExit(message[0] == 8, printf("Unexpected frame control byte: 0x%02x\n", message[0]));
VerifyOrExit(message[1] == 1, printf("Unexpected sequence number: %d\n", message[1]));
// message[2] is the command id.
switch (message[2])
{
case 0x0b: {
// Default Response command. Remaining bytes are the command id of the
// command that's being responded to and a status code.
VerifyOrExit(messageLen == 5, printf("Unexpected response length: %d\n", messageLen));
printf("Got default response to command '0x%02x' for cluster '0x%02x'. Status is '0x%02x'.\n", message[3], frame.clusterId,
message[4]);
break;
}
case 0x01: {
// Read Attributes Response command. Remaining bytes are a list of
// (attr id, 0, attr type, attr value) or (attr id, failure status)
// tuples.
//
// But for now we only support one attribute value, and that value is a
// boolean.
VerifyOrExit(messageLen >= 6, printf("Unexpected response length for Read Attributes command: %d\n", messageLen));
uint16_t attr_id;
memcpy(&attr_id, message + 3, sizeof(attr_id));
if (message[5] == 0)
{
// FIXME: Should we have a mapping of type ids to types, based on
// table 2.6.2.2 in Rev 8 of the ZCL spec? 0x10 is "Boolean".
VerifyOrExit(messageLen == 8,
printf("Unexpected response length for successful Read Attributes command: %d\n", messageLen));
printf("Read attribute '0x%04x' for cluster '0x%02x'. Type is '0x%02x', value is '0x%02x'.\n", attr_id,
frame.clusterId, message[6], message[7]);
}
else
{
VerifyOrExit(messageLen == 6,
printf("Unexpected response length for failed Read Attributes command: %d\n", messageLen));
printf("Reading attribute '0x%04x' for cluster '0x%02x' failed with status '0x%02x'.\n", attr_id, frame.clusterId,
message[5]);
}
break;
}
default: {
printf("Unexpected command '0x%02x'.\n", message[2]);
break;
}
}
exit:
System::PacketBuffer::Free(buffer);
}
static void OnMessage(DeviceController::ChipDeviceController * deviceController, void * appReqState, System::PacketBuffer * buffer)
{
size_t data_len = buffer->DataLength();
waitingForResponse = false;
printf("Message received: %zu bytes\n", data_len);
if (ContentMayBeADataModelMessage(buffer))
{
HandleDataModelMessage(buffer);
return;
}
// attempt to print the incoming message
char msg_buffer[data_len];
msg_buffer[data_len] = 0; // Null-terminate whatever we received and treat like a string...
memcpy(msg_buffer, buffer->Start(), data_len);
int compare = strncmp(msg_buffer, PAYLOAD, data_len);
if (compare == 0)
{
printf("Got expected Message...\n");
}
else
{
printf("Didn't get the expected Echo. Compare: %d\n", compare);
printf("\nSend: %s \nRecv: %s\n", PAYLOAD, msg_buffer);
}
System::PacketBuffer::Free(buffer);
}
static void OnError(DeviceController::ChipDeviceController * deviceController, void * appReqState, CHIP_ERROR error,
const IPPacketInfo * pi)
{
waitingForResponse = false;
printf("ERROR: %s\n Got error\n", ErrorStr(error));
}
void ShowUsage(const char * executable)
{
fprintf(stderr,
"Usage: \n"
" %s command [params]\n"
" Supported commands and their parameters:\n"
" echo-ble discriminator setupPINCode\n"
" echo device-ip-address device-port\n"
" off device-ip-address device-port endpoint-id\n"
" on device-ip-address device-port endpoint-id\n"
" toggle device-ip-address device-port endpoint-id\n"
" read device-ip-address device-port endpoint-id attr-name\n"
" Supported attribute names for the 'read' command:\n"
" onoff -- OnOff attribute from the On/Off cluster\n",
executable);
}
enum class Command
{
Off,
On,
Toggle,
Read,
Echo,
EchoBle,
};
template <int N>
bool EqualsLiteral(const char * str, const char (&literal)[N])
{
return strncmp(str, literal, N) == 0;
}
bool DetermineCommand(int argc, char * argv[], Command * command)
{
if (argc <= 1)
{
return false;
}
if (EqualsLiteral(argv[1], "off"))
{
*command = Command::Off;
return argc == 5;
}
if (EqualsLiteral(argv[1], "on"))
{
*command = Command::On;
return argc == 5;
}
if (EqualsLiteral(argv[1], "toggle"))
{
*command = Command::Toggle;
return argc == 5;
}
if (EqualsLiteral(argv[1], "read"))
{
*command = Command::Read;
return argc == 6;
}
if (EqualsLiteral(argv[1], "echo"))
{
*command = Command::Echo;
return argc == 4;
}
if (EqualsLiteral(argv[1], "echo-ble"))
{
*command = Command::EchoBle;
return argc == 4;
}
fprintf(stderr, "Unknown command: %s\n", argv[1]);
return false;
}
struct CommandArgs
{
IPAddress hostAddr;
uint16_t port;
uint16_t discriminator;
uint32_t setupPINCode;
uint8_t endpointId;
// attrName is only used for Read commands.
const char * attrName;
};
bool DetermineArgsBle(char * argv[], CommandArgs * commandArgs)
{
std::string discriminator_str(argv[2]);
commandArgs->discriminator = std::stoi(discriminator_str);
std::string setup_pin_code_str(argv[3]);
commandArgs->setupPINCode = std::stoi(setup_pin_code_str);
return true;
}
bool DetermineArgsEcho(char * argv[], CommandArgs * commandArgs)
{
if (!IPAddress::FromString(argv[2], commandArgs->hostAddr))
{
fputs("Error: Invalid device IP address", stderr);
return false;
}
std::string port_str(argv[3]);
std::stringstream ss(port_str);
ss >> commandArgs->port;
if (ss.fail() || !ss.eof())
{
fputs("Error: Invalid device port", stderr);
return false;
}
return true;
}
bool DetermineArgsOnOff(char * argv[], CommandArgs * commandArgs)
{
if (!DetermineArgsEcho(argv, commandArgs))
{
return false;
}
std::string endpoint_str(argv[4]);
std::stringstream ss(endpoint_str);
// stringstream treats uint8_t as char, which is not what we want here.
uint16_t endpoint;
ss >> endpoint;
if (ss.fail() || !ss.eof() || endpoint < CHIP_ZCL_ENDPOINT_MIN || endpoint > CHIP_ZCL_ENDPOINT_MAX)
{
fprintf(stderr, "Error: Invalid endpoint id '%s'\n", argv[4]);
return false;
}
commandArgs->endpointId = endpoint;
return true;
}
bool DetermineCommandArgs(char * argv[], Command command, CommandArgs * commandArgs)
{
switch (command)
{
case Command::EchoBle:
return DetermineArgsBle(argv, commandArgs);
case Command::Echo:
return DetermineArgsEcho(argv, commandArgs);
case Command::On:
case Command::Off:
case Command::Toggle:
case Command::Read: {
if (!DetermineArgsOnOff(argv, commandArgs))
{
return false;
}
if (command == Command::Read)
{
commandArgs->attrName = argv[5];
}
return true;
}
}
fprintf(stderr, "Need to define arg handling for command '%d'\n", int(command));
return false;
}
// Handle the echo case, where we just send a string and expect to get it back.
void DoEcho(DeviceController::ChipDeviceController * controller, const char * identifier)
{
CHIP_ERROR err = CHIP_NO_ERROR;
size_t payload_len = strlen(PAYLOAD);
// Run the client
while (1)
{
if (isDeviceConnected)
{
// Reallocate buffer on each run, as the secure transport encrypts and
// overwrites the buffer from previous iteration.
auto * buffer = System::PacketBuffer::NewWithAvailableSize(payload_len);
memcpy(buffer->Start(), PAYLOAD, payload_len);
buffer->SetDataLength(payload_len);
err = controller->SendMessage(NULL, buffer);
printf("Msg sent to server %s\n", err != CHIP_NO_ERROR ? ErrorStr(err) : identifier);
}
sleep(SEND_DELAY);
}
}
void DoEchoBle(DeviceController::ChipDeviceController * controller, const uint16_t discriminator)
{
char name[6];
snprintf(name, sizeof(name), "%u", discriminator);
DoEcho(controller, "");
}
void DoEchoIP(DeviceController::ChipDeviceController * controller, const IPAddress & hostAddr, uint16_t port)
{
char name[46];
char hostIpStr[40];
hostAddr.ToString(hostIpStr, sizeof(hostIpStr));
snprintf(name, sizeof(name), "%s:%d", hostIpStr, port);
DoEcho(controller, name);
}
// Handle the on/off/toggle case, where we are sending a ZCL command and not
// expecting a response at all.
void DoOnOff(DeviceController::ChipDeviceController * controller, Command command, const CommandArgs & commandArgs)
{
const uint8_t endpoint = commandArgs.endpointId;
// Make sure our buffer is big enough, but this will need a better setup!
static const size_t bufferSize = 1024;
auto * buffer = System::PacketBuffer::NewWithAvailableSize(bufferSize);
uint16_t dataLength = 0;
switch (command)
{
case Command::Off:
dataLength = encodeOffCommand(buffer->Start(), bufferSize, endpoint);
break;
case Command::On:
dataLength = encodeOnCommand(buffer->Start(), bufferSize, endpoint);
break;
case Command::Toggle:
dataLength = encodeToggleCommand(buffer->Start(), bufferSize, endpoint);
break;
case Command::Read:
if (!EqualsLiteral(commandArgs.attrName, "onoff"))
{
fprintf(stderr, "Don't know how to read '%s' attribute\n", commandArgs.attrName);
return;
}
dataLength = encodeReadOnOffCommand(buffer->Start(), bufferSize, endpoint);
break;
default:
fprintf(stderr, "Unknown command: %d\n", int(command));
return;
}
buffer->SetDataLength(dataLength);
#ifdef DEBUG
const size_t data_len = buffer->DataLength();
fprintf(stderr, "SENDING: %zu ", data_len);
for (size_t i = 0; i < data_len; ++i)
{
fprintf(stderr, "%d ", buffer->Start()[i]);
}
fprintf(stderr, "\n");
#endif
controller->SendMessage(NULL, buffer);
// FIXME: waitingForResponse is being written on other threads, presumably.
// We probably need some more synchronization here.
auto start = std::chrono::system_clock::now();
while (waitingForResponse &&
std::chrono::duration_cast<std::chrono::minutes>(std::chrono::system_clock::now() - start) < kWaitingForResponseTimeout)
{
// Just poll for the response.
sleep(1);
}
if (waitingForResponse)
{
fprintf(stderr, "No response from device.");
}
}
CHIP_ERROR ExecuteCommand(DeviceController::ChipDeviceController * controller, Command command, CommandArgs & commandArgs)
{
CHIP_ERROR err = CHIP_NO_ERROR;
switch (command)
{
case Command::EchoBle:
err = controller->ConnectDevice(kRemoteDeviceId, commandArgs.discriminator, commandArgs.setupPINCode, NULL, OnConnect,
OnMessage, OnError);
VerifyOrExit(err == CHIP_NO_ERROR, fprintf(stderr, "Failed to connect to the device"));
DoEchoBle(controller, commandArgs.discriminator);
break;
case Command::Echo:
err =
controller->ConnectDevice(kRemoteDeviceId, commandArgs.hostAddr, NULL, OnConnect, OnMessage, OnError, commandArgs.port);
VerifyOrExit(err == CHIP_NO_ERROR, fprintf(stderr, "Failed to connect to the device"));
DoEchoIP(controller, commandArgs.hostAddr, commandArgs.port);
break;
default:
err =
controller->ConnectDevice(kRemoteDeviceId, commandArgs.hostAddr, NULL, OnConnect, OnMessage, OnError, commandArgs.port);
VerifyOrExit(err == CHIP_NO_ERROR, fprintf(stderr, "Failed to connect to the device"));
DoOnOff(controller, command, commandArgs);
controller->ServiceEventSignal();
break;
}
exit:
return err;
}
// ================================================================================
// Main Code
// ================================================================================
int main(int argc, char * argv[])
{
Command command;
CommandArgs commandArgs;
if (!DetermineCommand(argc, argv, &command) || !DetermineCommandArgs(argv, command, &commandArgs))
{
ShowUsage(argv[0]);
return EXIT_FAILURE;
}
auto * controller = new DeviceController::ChipDeviceController();
CHIP_ERROR err = controller->Init(kLocalDeviceId);
VerifyOrExit(err == CHIP_NO_ERROR, fprintf(stderr, "Failed to initialize the device controller"));
err = controller->ServiceEvents();
VerifyOrExit(err == CHIP_NO_ERROR, fprintf(stderr, "Failed to initialize the run loop"));
err = ExecuteCommand(controller, command, commandArgs);
VerifyOrExit(err == CHIP_NO_ERROR, fprintf(stderr, "Failed to send command"));
exit:
if (err != CHIP_NO_ERROR)
{
fprintf(stderr, "ERROR: %s\n", ErrorStr(err));
}
controller->Shutdown();
delete controller;
return (err == CHIP_NO_ERROR) ? EXIT_SUCCESS : EXIT_FAILURE;
}