Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/node config update #4

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

jamienz
Copy link
Contributor

@jamienz jamienz commented Feb 8, 2024

Adds the ability for a Host to remotely configure a Node. When the Node is about to send a message, it sends a "challenge" message to the Host first and includes its firmware version, and the Host may respond with the details for a firmware update. This PR adds the Node's config version to the challenge request, so the Host has the option to return a new configuration to the Node as part of a challenge response.

The configuration is application-specific, e.g.

  • the length of time the Node should sleep
  • how many samples the Node should take of an environmental sensor before averaging them
  • how long the Node should ignore signals from a PIR sensor after it has been triggered

EspNowNetwork already supports "generic" firmware, that can be the same for multiple Nodes. This PR allows Nodes to run generic firmware, but to fetch a potentially-Node-specific config from the Host.

Copy link
Owner

@Johboh Johboh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @jamienz.
I have had this in my mind/to do as well, but so far haven't had a real need for sending configurations. Its a good idea, and I think we should add it (given the comments I added).
Also, did you see my comments on #1 and #3?

@@ -42,7 +47,8 @@ EspNowHost::OnApplicationMessage _on_application_message = [](EspNowHost::Messag
const uint8_t *message) {
// Callback for new application messages.
auto id = message[0];
Serial.println("Got message from 0x" + String(metadata.mac_address) + " with ID " + id);
Serial.println("Got message from 0x" + String(metadata.mac_address) + " with ID " +
id); // + " rssi=" + String(metadata.rssi));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
id); // + " rssi=" + String(metadata.rssi));
id);

Unless you address and merge #3 first :).

Also remember to update the Esp-idf example versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Esp-idf host example doesn't set up a WiFi connection, so isn't a working example. Is that intentional? Should I add in the code to connect it to an access point, or just add in the changes to support this new configuration feature?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is, as I wanted to keep the examples simple and that is more code involved setting up WiFi using ESP-IDF. I should have added a "setup WiFi here" comment to make it more complete, as I have here.

When I myself find examples, I appreciate when the examples are as minimal as possible, and doesn't contains extras that is not actually needed. The extras can be in dedicated examples describing these extra features. Of course, as you say, without WiFi the node is not fully functional (thus the comment).

As you are using the Ardunio library (and not necessarily have the ESP-IDF framework setup), you can skip modifying the esp-idf examples, I can adjust them afterwards.


#define FIRMWARE_VERSION 90201


Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding this the Configuration example as another example that demonstrate how configuration works. This to keep this node example to a minimum as Configuration is optional (and will make it easier for the reader).
Suggest node_with_configuration or around those lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node_with_configuration and host_with_configuration have been added for Arduino, and the original node and host examples left as-is.

}

uint32_t timestamp2 = millis();
_on_log(("elapsed " + std::to_string(timestamp2 - timestamp1) + "ms").c_str(), ESP_LOG_DEBUG);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on why you added this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been removed. When it comes to battery powered nodes, the execution time and boot time are something I measure and try to minimize, but it shouldn't have been left in.

@@ -73,6 +75,9 @@ class EspNowHost {
typedef std::function<std::optional<FirmwareUpdate>(uint64_t mac_address, uint32_t firmware_version)>
FirmwareUpdateAvailable;

typedef std::function<std::optional<EspNowConfigEnvelope>(uint64_t mac_address, uint8_t config_version)>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CPP beginner question: For these callbacks, is it correct that the memory allocation for the struct that is returned is done within the function? Off the heap? For the FirmwareUpdate, there isn't an example of what this looks like. Also, in EspNowHost.cpp the struct that is received from the callback isn't explicitly deleted. Is this taken care of some other way? Would it be better to pass into the callback a struct that the callback can optionally populate, and return an int that indicates if it did so or not?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "callbacks" we are using here are just pure function calls, in contrast to other observer patterns where callbacks might run at a later point by the scheduler (e.g. using FreeRTOS or similar queues,event busses etc).
As we are returning the struct here by value, the struct and its content will be copied to the caller. The struct allocated in the lambda will be destroyed when the function goes out of scope, and the same will be true for the "received" struct on the caller side.

(and yes, I should a better "FirmwareUpdate" example).

This is how my firmware update implementation looks like in my host:

std::optional<EspNowHost::FirmwareUpdate> HostDriver::onFirmwareUpdate(uint64_t mac_address, uint32_t firmware_version) {
  auto optdev = _device_manager.deviceForMac(mac_address);
  if (!optdev) {
    return std::optional<EspNowHost::FirmwareUpdate>{std::nullopt};
  }
  auto type = optdev->get().type();
  auto name = optdev->get().name();
  auto hardware = optdev->get().hardware();
  auto firmware_mqtt_path =
      _mqtt_remote.clientId() + "/firmware/current/" + type + "/" + hardware + "/" + makeMqttPathCompatible(name);

  // Do we have a newer firmware version for this type?
  auto update_information = _firmware_checker.getUpdateUrl(firmware_version, type, hardware);
  if (update_information) {
    _mqtt_remote.publishMessage(firmware_mqtt_path, "Updating to " + std::to_string(update_information->version), true);

    EspNowHost::FirmwareUpdate firmware_update;
    strncpy(firmware_update.wifi_ssid, _wifi_ssid, sizeof(firmware_update.wifi_ssid));
    strncpy(firmware_update.wifi_password, _wifi_password, sizeof(firmware_update.wifi_password));
    strncpy(firmware_update.url, update_information->url.c_str(), sizeof(firmware_update.url));
    strncpy(firmware_update.md5, update_information->md5.c_str(), sizeof(firmware_update.md5));
    return std::optional<EspNowHost::FirmwareUpdate>{firmware_update};
  }

  _mqtt_remote.publishMessage(firmware_mqtt_path, std::to_string(firmware_version), true);

  return std::optional<EspNowHost::FirmwareUpdate>{std::nullopt};
}

@@ -96,7 +101,7 @@ class EspNowHost {
*/
EspNowHost(EspNowCrypt &crypt, WiFiInterface wifi_interface, OnNewMessage on_new_message,
OnApplicationMessage on_application_message, FirmwareUpdateAvailable firwmare_update = {},
OnLog on_log = {});
ConfigUpdateAvailable config_update = {}, OnLog on_log = {});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @param documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@param docs added

* A container for the application-specific config data that is held within the payload.
* The payload could be anything, like a string, or json, but is probably a struct for efficiency.
*/
struct EspNowConfigEnvelope {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in esp-now-structs.h regarding EspNowConfigEnvelope

@@ -47,6 +49,7 @@ struct EspNowDiscoveryResponseV1 {
struct EspNowChallengeRequestV1 {
uint8_t id = MESSAGE_ID_CHALLENGE_REQUEST_V1;
Copy link
Owner

@Johboh Johboh Feb 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a breaking change (e.g. if nodes are updated they will not function until host has be updated, and vice versa), technically you should add this as a new version, e.g. EspNowChallengeRequestV2. This is why I have version to begin with :) (and then handle both versions in node and host impls). But, this might be overkill. If we just update all the nodes first (and can live with them non functioning until host is updated), then its OK. So lets keep as is.

@@ -224,6 +231,8 @@ bool EspNowNode::sendMessage(void *message, size_t message_size, int16_t retries
// The challenge we expect to get back in the challenge/firmware response.
request.challenge_challenge = esp_random();
request.firmware_version = _firmware_version;
request.config_version = _config_version;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
request.config_version = _config_version;
request.config_version = _configuration_revision;

@@ -224,6 +231,8 @@ bool EspNowNode::sendMessage(void *message, size_t message_size, int16_t retries
// The challenge we expect to get back in the challenge/firmware response.
request.challenge_challenge = esp_random();
request.firmware_version = _firmware_version;
request.config_version = _config_version;
log("challenge firmare version=" + std::to_string(_firmware_version) + " config version=" + std::to_string(_config_version), ESP_LOG_DEBUG);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log("challenge firmare version=" + std::to_string(_firmware_version) + " config version=" + std::to_string(_config_version), ESP_LOG_DEBUG);
log("challenge firmare version=" + std::to_string(_firmware_version) + " configuration revision=" + std::to_string(_configuration_revision), ESP_LOG_DEBUG);

@@ -211,6 +211,7 @@ class EspNowNode {
OnStatus _on_status;
EspNowCrypt &_crypt;
esp_netif_t *_netif_sta;
uint16_t _config_version;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
uint16_t _config_version;
uint16_t _configuration_revision;

@jamienz
Copy link
Contributor Author

jamienz commented Feb 10, 2024

Thanks for all the thorough feedback @Johboh , and also for your patience. As you will have deduced, c/cpp/embedded is not my native language.

@Johboh
Copy link
Owner

Johboh commented Feb 10, 2024

Thanks for all the thorough feedback @Johboh , and also for your patience. As you will have deduced, c/cpp/embedded is not my native language.

no worries, I appreciate the contribution and I'm glad if I can share some knowledge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants