Skip to content
Snippets Groups Projects
Verified Commit 3b43bb90 authored by Zoe Michaela Dietmar Pfister's avatar Zoe Michaela Dietmar Pfister :gay_pride_flag:
Browse files

Possibility to send 4 messages with one ESPNow-send action

parent de64ec6e
No related branches found
No related tags found
2 merge requests!39Merge Develop into Main,!27Move from json-string based transfer between client and host to C struct / class based transfer
......@@ -6,6 +6,7 @@ monitor_speed = 115200
lib_ldf_mode = deep
lib_extra_dirs =
../libs
../../shared-libs
; C++17 https://community.platformio.org/t/esp32-c-17-toolchain-missing-std-optional/25850/6
; we use c++17 features (i.e. optionals in ClientDataPackage.hpp)
build_flags =
......
......@@ -15,112 +15,121 @@ MockSensor mock_channel2;
MockSensor mock_channel3;
void send_msgs(const std::__cxx11::list<Message> msgs) {
for (const Message &msg: msgs) {
if (msg.send() != ESP_OK) {
RtcMemory::store(msg.getMessageAsMinifiedJsonString());
}
unsigned long ts = millis();
// it takes ~110ms for receiving an acknowledgement by the host in perfect conditions
uint16_t message_timeout = 2000;
while (!was_msg_received()) {
if ((millis() - ts) > message_timeout) {
RtcMemory::store(msg.getMessageAsMinifiedJsonString());
ESP_LOGE(TAG, "Timeout: Host not available\n");
break;
}
}
ESP_LOGD(TAG, "Time until acknowledgement: %ld", millis() - ts);
for (const Message &msg: msgs) {
if (msg.send() != ESP_OK) {
RtcMemory::store(msg.getMessageAsMinifiedJsonString());
}
unsigned long ts = millis();
// it takes ~110ms for receiving an acknowledgement by the host in perfect conditions
uint16_t message_timeout = 2000;
while (!was_msg_received()) {
if ((millis() - ts) > message_timeout) {
RtcMemory::store(msg.getMessageAsMinifiedJsonString());
ESP_LOGE(TAG, "Timeout: Host not available\n");
break;
}
}
ESP_LOGD(TAG, "Time until acknowledgement: %ld", millis() - ts);
}
}
// one loop takes ~2200 ms
void setup() {
// disable brownout
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
unsigned long ts = millis();
Serial.begin(115200);
DeepSleep::print_wakeup_reason();
DeepSleep::bootCount++;
ESP_LOGD(TAG, "Boot number: %d", DeepSleep::bootCount);
// delay(100);
mock_channel0.setup();
mock_channel1.setup();
mock_channel2.setup();
mock_channel3.setup();
mock_channel0.setChannel(0);
mock_channel1.setChannel(1);
mock_channel2.setChannel(2);
mock_channel3.setChannel(3);
ESP_LOGD(TAG, "Setup took %ld ms", millis() - ts);
try {
// FIXME: put me into seperate trys? No data will be sent when 1 exception occurs
ts = millis();
auto messages0 = mock_channel0.buildMessages();
auto messages1 = mock_channel1.buildMessages();
auto messages2 = mock_channel2.buildMessages();
auto messages3 = mock_channel3.buildMessages();
// roughly takes 500ms, ~120ms for each adc channel, barely anything for battery monitor
ESP_LOGD(TAG, "Reading data and building messages took %ld ms", millis() - ts);
gpio_set_level(GPIO_NUM_32, 0);
ESP_LOGD(TAG, "Size of message to be sent: %d", sizeof(messages0.front()));
ESP_LOGD(TAG, "Size of Message class: %d", sizeof(Message));
ESP_LOGD(TAG, "Size of ClientDataPackage class: %d", sizeof(ClientDataPackage));
// sizeof string
ESP_LOGD(TAG, "Size of string: %d", sizeof(std::string));
// sizeof string with 5 char
ESP_LOGD(TAG, "Size of string with 5 char: %d", sizeof(char[5]));
// sizeof optional int
ESP_LOGD(TAG, "Size of optional int: %d", sizeof(std::optional<int>));
// sizeof int
ESP_LOGD(TAG, "Size of int: %d", sizeof(int));
// sizeof double
ESP_LOGD(TAG, "Size of double: %d", sizeof(double));
// sizeof float
ESP_LOGD(TAG, "Size of float: %d", sizeof(float));
// list of 5 compresseddatapackage
CompressedDataPackage compresseddatapackage{};
ESP_LOGD(TAG, "Size of list of 4 CompressedDataPackage: %d", sizeof(std::list<CompressedDataPackage>) + (sizeof(CompressedDataPackage) * 4));
// sizeof compresseddatapackage
ESP_LOGD(TAG, "Size of CompressedDataPackage: %d", sizeof(CompressedDataPackage));
// sizeof list
ESP_LOGD(TAG, "Size of list: %d", sizeof(std::list<CompressedDataPackage>));
// sizeof vector
ESP_LOGD(TAG, "Size of vector: %d", sizeof(std::vector<CompressedDataPackage>));
// FIXME: put this outside the try loop?
ts = millis();
espnow_setup();
ESP_LOGD(TAG, "EPSNow setup took %ld ms", millis() - ts);
// make a list of messages
std::list<Message> messages;
messages.insert(messages.end(), messages0.begin(), messages0.end());
messages.insert(messages.end(), messages1.begin(), messages1.end());
messages.insert(messages.end(), messages2.begin(), messages2.end());
messages.insert(messages.end(), messages3.begin(), messages3.end());
ts = millis();
// Message::sendMessages(messages);
send_msgs(messages0);
// disable brownout
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
unsigned long ts = millis();
Serial.begin(115200);
DeepSleep::print_wakeup_reason();
DeepSleep::bootCount++;
ESP_LOGD(TAG, "Boot number: %d", DeepSleep::bootCount);
// delay(100);
mock_channel0.setup();
mock_channel1.setup();
mock_channel2.setup();
mock_channel3.setup();
mock_channel0.setChannel(0);
mock_channel1.setChannel(1);
mock_channel2.setChannel(2);
mock_channel3.setChannel(3);
ESP_LOGD(TAG, "Setup took %ld ms", millis() - ts);
try {
// FIXME: put me into seperate trys? No data will be sent when 1 exception occurs
ts = millis();
auto messages0 = mock_channel0.buildMessages();
auto messages1 = mock_channel1.buildMessages();
auto messages2 = mock_channel2.buildMessages();
auto messages3 = mock_channel3.buildMessages();
// roughly takes 500ms, ~120ms for each adc channel, barely anything for battery monitor
ESP_LOGD(TAG,
"Reading data and building messages took %ld ms",
millis() - ts);
gpio_set_level(GPIO_NUM_32, 0);
ESP_LOGD(TAG, "Size of message to be sent: %d", sizeof(messages0.front()));
ESP_LOGD(TAG, "Size of Message class: %d", sizeof(Message));
ESP_LOGD(TAG,
"Size of ClientDataPackage class: %d",
sizeof(ClientDataPackage));
// sizeof string
ESP_LOGD(TAG, "Size of string: %d", sizeof(std::string));
// sizeof string with 5 char
ESP_LOGD(TAG, "Size of string with 5 char: %d", sizeof(char[5]));
// sizeof optional int
ESP_LOGD(TAG, "Size of optional int: %d", sizeof(std::optional<int>));
// sizeof int
ESP_LOGD(TAG, "Size of int: %d", sizeof(int));
// sizeof double
ESP_LOGD(TAG, "Size of double: %d", sizeof(double));
// sizeof float
ESP_LOGD(TAG, "Size of float: %d", sizeof(float));
// list of 5 compresseddatapackage
CompressedDataPackage compresseddatapackage{};
ESP_LOGD(TAG,
"Size of list of 4 CompressedDataPackage: %d",
sizeof(std::list<CompressedDataPackage>)
+ (sizeof(CompressedDataPackage) * 4));
// sizeof compresseddatapackage
ESP_LOGD(TAG,
"Size of CompressedDataPackage: %d",
sizeof(CompressedDataPackage));
// sizeof list
ESP_LOGD(TAG, "Size of list: %d", sizeof(std::list<CompressedDataPackage>));
// sizeof vector
ESP_LOGD(TAG,
"Size of vector: %d",
sizeof(std::vector<CompressedDataPackage>));
// FIXME: put this outside the try loop?
ts = millis();
espnow_setup();
ESP_LOGD(TAG, "EPSNow setup took %ld ms", millis() - ts);
// make a list of messages
std::array<Message, 4> messages =
{messages0.front(), messages1.front(), messages2.front(),
Message::nullMessage()};
ts = millis();
Message::sendMessages(messages);
// send_msgs(messages0);
// send_msgs(messages1);
// send_msgs(messages2);
// send_msgs(messages3);
// roughly takes 3s in ideal conditions
ESP_LOGD(TAG, "Sending messages took %ld ms", millis() - ts);
// roughly takes 3s in ideal conditions
ESP_LOGD(TAG, "Sending messages took %ld ms", millis() - ts);
} catch (const NoDataAvailableException &e) {
std::cerr << e.what() << '\n';
}
} catch (const NoDataAvailableException &e) {
std::cerr << e.what() << '\n';
}
// battery protection: go to deep sleep for unlimited time when voltage less than 3.2V
DeepSleep::deep_sleep(5);
// battery protection: go to deep sleep for unlimited time when voltage less than 3.2V
DeepSleep::deep_sleep(5);
}
......
......@@ -4,75 +4,85 @@
static const char *TAG = "MESSAGE";
esp_err_t Message::send() const {
ESP_LOGD(TAG, "Sending message");
esp_err_t success;
auto messageData = getCompressedDataPackage();
ESP_LOGD(TAG, "Sending message");
esp_err_t success;
auto messageData = getCompressedDataPackage();
// conversion from std::string to c_str adds null terminator, which is why we add 1 to message length
success = esp_now_send(recipient, (uint8_t * ) & messageData, sizeof(CompressedDataPackage));
if (success != ESP_OK) {
ESP_LOGE(TAG, "Error sending the data");
// Removed caching from here, better do this in main
}
// conversion from std::string to c_str adds null terminator, which is why we add 1 to message length
success = esp_now_send(recipient,
(uint8_t *) &messageData,
sizeof(CompressedDataPackage));
if (success != ESP_OK) {
ESP_LOGE(TAG, "Error sending the data");
// Removed caching from here, better do this in main
}
// ESP_LOGD(TAG, "Sent data: %s", messageData.c_str());
ESP_LOGD(TAG, "Timestamp sent: %ld", clientDataPackage.getTimestamp());
ESP_LOGD(TAG, "send status: %d", success);
ESP_LOGD(TAG, "Timestamp sent: %ld", clientDataPackage.getTimestamp());
ESP_LOGD(TAG, "send status: %d", success);
return success;
return success;
}
esp_err_t Message::sendMessages(const std::list<Message> &messages) {
// recipient
uint8_t rec[6]{};
get_host_mac(rec);
ESP_LOGD(TAG, "Sending messages");
esp_err_t success;
// list of compressed data
std::list<CompressedDataPackage> compressedDataPackages;
// max 4 messages
int i = 0;
for (const auto &message: messages) {
i++;
compressedDataPackages.push_back(message.getCompressedDataPackage());
if (i == 4) {
break;
}
esp_err_t Message::sendMessages(const std::array<Message, 4> &messages) {
// recipient
uint8_t rec[6]{};
get_host_mac(rec);
ESP_LOGD(TAG, "Sending messages");
esp_err_t success;
// list of compressed data. Sending 4 at a time
std::array<CompressedDataPackage, 4> compressedDataPackages{};
// max 4 messages
for (int i = 0; i < 4; i++) {
compressedDataPackages[i] = messages[i].getCompressedDataPackage();
if (messages[i].clientDataPackage.getTimestamp() == NULL_TIMESTAMP) {
compressedDataPackages[i].errorType = ErrorTypes::NULL_MESSAGE;
}
}
// conversion from std::string to c_str adds null terminator, which is why we add 1 to message length
success = esp_now_send(rec, (uint8_t * ) & compressedDataPackages,
sizeof(std::list<CompressedDataPackage>) +
4 * sizeof(CompressedDataPackage));
if (success != ESP_OK) {
ESP_LOGE(TAG, "Error sending the data");
// Removed caching from here, better do this in main
// conversion from std::string to c_str adds null terminator, which is why we add 1 to message length
success = esp_now_send(rec, (uint8_t *) &compressedDataPackages,
sizeof(std::array<CompressedDataPackage, 4>));
if (success != ESP_OK) {
ESP_LOGE(TAG, "Error sending the data");
// Removed caching from here, better do this in main
}
unsigned long ts = millis();
// it takes ~110ms for receiving an acknowledgement by the host in perfect conditions
uint16_t message_timeout = 2000;
while (!was_msg_received()) {
if ((millis() - ts) > message_timeout) {
ESP_LOGE(TAG, "Timeout: Host not available\n");
break;
}
}
ESP_LOGD(TAG, "Time until acknowledgement: %ld", millis() - ts);
// ESP_LOGD(TAG, "Sent data: %s", messageData.c_str());
// ESP_LOGD(TAG, "Timestamp sent: %ld", clientDataPackage.getTimestamp());
ESP_LOGD(TAG, "send status: %d", success);
ESP_LOGD(TAG, "send status: %d", success);
return success;
return success;
}
std::string Message::getMessageAsMinifiedJsonString() const {
return clientDataPackage.getDataPackageAsMinifiedJsonString();
return clientDataPackage.getDataPackageAsMinifiedJsonString();
}
CompressedDataPackage Message::getCompressedDataPackage() const {
return clientDataPackage.getCompressedDataPackage();
return clientDataPackage.getCompressedDataPackage();
}
Message::Message(ClientDataPackage data) : clientDataPackage(std::move(data)) {
// check for existing host mac address, use broadcast otherwise
get_host_mac(recipient);
// check for existing host mac address, use broadcast otherwise
get_host_mac(recipient);
}
Message::Message(MeasurementData const &data, const SensorInformation &information,
Message::Message(MeasurementData const &data,
const SensorInformation &information,
unsigned long timestamp)
: clientDataPackage(data, information, timestamp) {
// check for existing host mac address, use broadcast otherwise
get_host_mac(recipient);
: clientDataPackage(data, information, timestamp) {
// check for existing host mac address, use broadcast otherwise
get_host_mac(recipient);
}
......@@ -14,19 +14,29 @@
// Format of the message sent from host to client_satellite
// if more things are sent from the host the name might not be accurate anymore
class Message {
public:
explicit Message(ClientDataPackage data);
public:
explicit Message(ClientDataPackage data);
Message(MeasurementData const &data, const SensorInformation &information, unsigned long timestamp);
esp_err_t send() const;
[[nodiscard]] std::string getMessageAsMinifiedJsonString() const;
Message(MeasurementData const &data,
const SensorInformation &information,
unsigned long timestamp);
esp_err_t send() const;
[[nodiscard]] std::string getMessageAsMinifiedJsonString() const;
static Message nullMessage() {
return Message{MeasurementData(ERROR_VALUE,
std::nullopt,
std::nullopt,
"null"),
SensorInformation("null", SensorProtocol::NULL_PROTOCOL),
NULL_TIMESTAMP};
}
static esp_err_t sendMessages(const std::list<Message>& messages);
private:
ClientDataPackage clientDataPackage;
static esp_err_t sendMessages(const std::array<Message, 4> &messages);
private:
ClientDataPackage clientDataPackage;
uint8_t recipient[6]{};
uint8_t recipient[6]{};
CompressedDataPackage getCompressedDataPackage() const;
CompressedDataPackage getCompressedDataPackage() const;
};
//
// Created by zoe on 1/27/23.
//
#ifndef CLIENT_SATELLITE_ERRORTYPES_H
#define CLIENT_SATELLITE_ERRORTYPES_H
enum ERROR_TYPES {
SENSOR_DOES_NOT_RETURN_DATA = 1,
BATTERY_VOLTAGE_TOO_LOW = 2,
INVALID_VALUE = 3,
SENSOR_NOT_CONNECTED = 4,
};
#endif //CLIENT_SATELLITE_ERRORTYPES_H
......@@ -6,10 +6,12 @@
#define CLIENT_PROTOCOL_HPP
#include <map>
enum class SensorProtocol { I2C, RS485, Analog, Mock };
enum class SensorProtocol { I2C, RS485, Analog, Mock, NULL_PROTOCOL };
// sensorProtocol to string
const static std::map<SensorProtocol, const char *> protocolToString = {
{SensorProtocol::I2C, "I2C"}, {SensorProtocol::RS485, "RS485"}, {SensorProtocol::Analog, "ANALOG"}, {SensorProtocol::Mock, "MOCK"}};
{SensorProtocol::I2C, "I2C"}, {SensorProtocol::RS485, "RS485"},
{SensorProtocol::Analog, "ANALOG"}, {SensorProtocol::Mock, "MOCK"},
{SensorProtocol::NULL_PROTOCOL, "NULL"}};
#endif // CLIENT_PROTOCOL_HPP
......@@ -4,31 +4,33 @@ static const char *TAG = "MOCK";
void MockSensor::setup() {
ESP_LOGD(TAG, "MOCK Sensor initialized");
delay(100);
channel = 0;
ESP_LOGD(TAG, "MOCK Sensor initialized");
delay(100);
channel = 0;
}
float MockSensor::readData() {
// generate a random float value between 0 and 100
float randomValue = (float)rand() / (float)RAND_MAX * 100.0;
ESP_LOGD(TAG, "MOCK Sensor read value: %f", randomValue);
return randomValue;
// generate a random float value between 0 and 100
float randomValue = (float) rand() / (float) RAND_MAX * 100.0;
ESP_LOGD(TAG, "MOCK Sensor read value: %f", randomValue);
return randomValue;
}
void MockSensor::setChannel(int c) {
channel = c;
channel = c;
}
std::list<Message> MockSensor::buildMessages() {
std::list<Message> messages;
float data = readData();
MeasurementData MockData{data, channel, {}, "MOCK"};
messages.emplace_back(MockData, sensorInformation, Time::getInstance().getEpochSeconds());
return messages;
std::list<Message> messages;
float data = readData() + static_cast<float>(DeepSleep::bootCount);
MeasurementData MockData{data, channel, {}, "MOCK"};
messages.emplace_back(MockData,
sensorInformation,
Time::getInstance().getEpochSeconds());
return messages;
}
SensorInformation MockSensor::getSensorInformation() const {
return sensorInformation;
return sensorInformation;
}
......@@ -7,18 +7,20 @@
#include "Pinout.hpp"
#include "esp_log.h"
#include <Wire.h>
#include <f_deep_sleep.hpp>
class MockSensor : public ForteSensor<float> {
public:
void setup() override;
float readData() override;
void setChannel(int channel);
std::list<Message> buildMessages() override;
[[nodiscard]] SensorInformation getSensorInformation() const override;
private:
const SensorInformation sensorInformation{"MOCK", SensorProtocol::Mock};
int channel;
class MockSensor: public ForteSensor<float> {
public:
void setup() override;
float readData() override;
void setChannel(int channel);
std::list<Message> buildMessages() override;
[[nodiscard]] SensorInformation getSensorInformation() const override;
private:
const SensorInformation sensorInformation{"MOCK", SensorProtocol::Mock};
int channel;
};
#endif
\ No newline at end of file
......@@ -5,8 +5,8 @@
#ifndef HOST_CENTRAL_MAST_UTILITIES_H
#define HOST_CENTRAL_MAST_UTILITIES_H
#include "../../../client/libs/espnow/src/CompressedDataPackage.hpp"
#include "ArduinoJson.h"
#include "CompressedDataPackage.hpp"
#include "SD.h"
#include "SDCardException.h"
#include "SDSetupException.h"
......@@ -35,6 +35,7 @@ String getMacAddressAsString(const uint8_t *mac);
String documentToLineProtocolString(const DynamicJsonDocument &doc);
DynamicJsonDocument parseReceivedJsonData(char *data);
String documentToServerReadableString(const DynamicJsonDocument &doc);
String compressedDataPackageToServerReadableString(CompressedDataPackage compressedDataPackage, const String& clientMacAddress);
String compressedDataPackageToServerReadableString(CompressedDataPackage compressedDataPackage,
const String &clientMacAddress);
#endif // HOST_CENTRAL_MAST_UTILITIES_H
......@@ -16,6 +16,8 @@ monitor_speed = 115200
lib_ldf_mode = deep
monitor_port = /dev/ttyACM0
upload_port = /dev/ttyACM0
lib_extra_dirs =
../../shared-libs
build_flags =
-I include
-DCORE_DEBUG_LEVEL=5
......
......@@ -5,7 +5,7 @@
#define TINY_GSM_MODEM_SIM7000
#include "../../../client/libs/espnow/src/CompressedDataPackage.hpp"
#include "CompressedDataPackage.hpp"
#include "ConnectionManager.h"
#include "MessageType.h"
#include "SDCardLogger.h"
......@@ -56,15 +56,13 @@ void on_data_sent(const uint8_t *mac_addr, esp_now_send_status_t status) {
// go to sleep
}
/**
* @brief ESPNOW callback function that is called when data is received
* @param mac
* @param incomingData
* @param len length of the incoming data in bytes
*/
void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) {
esp_log_write(ESP_LOG_INFO, TAG_ESPNOW, "Message recieved\n");
// copy received data to a char array
CompressedDataPackage compressedDataPackage{};
memcpy(&compressedDataPackage, incomingData, sizeof(CompressedDataPackage));
// esp_log_write(ESP_LOG_DEBUG, TAG_ESPNOW, "Raw received Data: %s\n", data);
// log received data
esp_log_write(ESP_LOG_DEBUG, TAG_ESPNOW, "Received Data: %s\n", compressedDataPackage.toString().c_str());
if (!esp_now_is_peer_exist(mac)) {
esp_now_peer_info_t client = {};
......@@ -78,30 +76,45 @@ void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) {
}
}
// TODO: Respond to the client. Maybe do that before parsing anything
response response = {};
response.type = dataAck;
esp_read_mac(response.mac, ESP_MAC_WIFI_STA);
response.time = rtc.getEpoch();
esp_err_t success = esp_now_send(mac, (uint8_t *)&response, sizeof(response));
esp_log_write(ESP_LOG_INFO, TAG_ESPNOW, "Message recieved\n");
esp_log_write(ESP_LOG_DEBUG, TAG_ESPNOW, (success == ESP_OK) ? "Response sent\n" : "Failed to respond\n");
// copy received data to a char array
std::array<CompressedDataPackage, 4> compressedDataPackage{};
memcpy(&compressedDataPackage, incomingData, sizeof(std::array<CompressedDataPackage, 4>));
// esp_log_write(ESP_LOG_DEBUG, TAG_ESPNOW, "Raw received Data: %s\n", data);
String macAddress = getMacAddressAsString(mac);
auto doc = compressedDataPackageToServerReadableString(compressedDataPackage, macAddress);
for (auto &data : compressedDataPackage) {
// serialize json document again
std::string dataString{};
// ignore padding messages
if (data.timestamp == NULL_TIMESTAMP && data.errorType == NULL_MESSAGE) {
continue;
}
try {
SDUtilities::saveStringToSDCard(dataString);
} catch (const std::exception &e) {
esp_log_write(ESP_LOG_ERROR, TAG_ESPNOW, "Failed to save data to SD card: %s", e.what());
}
// log received data
esp_log_write(ESP_LOG_INFO, TAG_ESPNOW, "Received Data: %s\n", data.toString().c_str());
auto doc = compressedDataPackageToServerReadableString(data, macAddress);
xSemaphoreTake(xMutex, portMAX_DELAY);
queue.emplace(dataString.c_str());
xSemaphoreGive(xMutex);
// serialize json document again
std::string dataString{};
try {
SDUtilities::saveStringToSDCard(dataString);
} catch (const std::exception &e) {
esp_log_write(ESP_LOG_ERROR, TAG_ESPNOW, "Failed to save data to SD card: %s", e.what());
}
xSemaphoreTake(xMutex, portMAX_DELAY);
queue.emplace(dataString.c_str());
xSemaphoreGive(xMutex);
}
}
[[noreturn]] void esp_loop() {
......
......@@ -10,11 +10,15 @@
#include <string>
#include <utility>
constexpr double ERROR_VALUE = -999.99;
constexpr unsigned long NULL_TIMESTAMP = 0; // null timestamp
enum ErrorTypes : short {
SENSOR_NOT_FOUND,
SENSOR_NOT_CONNECTED,
NO_DATA,
DATA_OK,
NULL_MESSAGE, // message that is sent as padding, should be thrown away
};
struct CompressedDataPackage {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment