Skip to content
Snippets Groups Projects
Commit ce2210f3 authored by Zoe Pfister's avatar Zoe Pfister :speech_balloon:
Browse files

WIP: Refactor

Move SD card specific utilities into their own namespace
Try remounting SD after a number of failed attempts (see Definitions.h)
Early exit continuous SD access if the SD has not been found before.
parent 45925cc7
No related branches found
No related tags found
3 merge requests!39Merge Develop into Main,!25Minor additions to host,!24Refactor of Host ESP32
...@@ -9,6 +9,7 @@ AllowShortFunctionsOnASingleLine: Inline ...@@ -9,6 +9,7 @@ AllowShortFunctionsOnASingleLine: Inline
AlwaysBreakTemplateDeclarations: Yes AlwaysBreakTemplateDeclarations: Yes
BreakBeforeBinaryOperators: NonAssignment BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Custom BreakBeforeBraces: Custom
NamespaceIndentation: All
BraceWrapping: BraceWrapping:
AfterFunction: false AfterFunction: false
... ...
...@@ -14,7 +14,7 @@ void ResendManager::init() { ...@@ -14,7 +14,7 @@ void ResendManager::init() {
} }
uint ResendManager::getLastResendFileId() const { // get the next file id to be resend uint ResendManager::getLastResendFileId() const { // get the next file id to be resend
auto filesInDirectory = getFilesInDirectory(resendDirectoryPath); auto filesInDirectory = SDUtilities::getFilesInDirectory(resendDirectoryPath);
// convert the file names to uint // convert the file names to uint
std::list<uint> fileUintIDs; std::list<uint> fileUintIDs;
...@@ -38,24 +38,24 @@ uint ResendManager::getLastResendFileId() const { // get the next file id to be ...@@ -38,24 +38,24 @@ uint ResendManager::getLastResendFileId() const { // get the next file id to be
} }
void ResendManager::createResendDirectory() const { // create directory if it doesn't exist void ResendManager::createResendDirectory() const { // create directory if it doesn't exist
createDirectory("/resend"); SDUtilities::createDirectory("/resend");
} }
void ResendManager::storeForResend(const String &messageToBeSend) { void ResendManager::storeForResend(const String &messageToBeSend) {
// create file // create file
String filename = String(nextResendFileId); String filename = String(nextResendFileId);
writeFile(messageToBeSend, resendDirectoryPath + "/" + filename); SDUtilities::writeFile(messageToBeSend, resendDirectoryPath + "/" + filename);
ResendManager::incrementCount(); ResendManager::incrementCount();
} }
std::optional<ResendPointType> ResendManager::loadNextToBeResendMessage() { std::optional<ResendPointType> ResendManager::loadNextToBeResendMessage() {
// get first file in resend directory // get first file in resend directory
auto filename = getFirstFileNameInDirectory(resendDirectoryPath.c_str()); auto filename = SDUtilities::getFirstFileNameInDirectory(resendDirectoryPath.c_str());
if (filename.has_value()) { if (filename.has_value()) {
// read file // read file
auto message = readFile(resendDirectoryPath + "/" + filename.value().c_str()); auto message = SDUtilities::readFile(resendDirectoryPath + "/" + filename.value().c_str());
return ResendPointType{message, filename.value()}; return ResendPointType{message, filename.value()};
} }
return std::nullopt; return std::nullopt;
......
...@@ -24,13 +24,16 @@ ...@@ -24,13 +24,16 @@
#define LED_PIN 12 #define LED_PIN 12
// See all AT commands, if wanted // See all AT commands, if wanted
#define DUMP_AT_COMMANDS //#define DUMP_AT_COMMANDS
#define TAG_ESPNOW "ESPNOW" #define TAG_ESPNOW "ESPNOW"
#define TAG_MAIN "MAIN" #define TAG_MAIN "MAIN"
#define TAG_SD "SD" #define TAG_SD "SD"
#define TAG_GSM "GSM" #define TAG_GSM "GSM"
#define TAG_TIMEMANAGER "TIMEMANAGER" #define TAG_TIMEMANAGER "TIMEMANAGER"
#define TAG_SDUTILITIES "SDUTILITIES"
#define SD_CARD_FAIL_COUNTER 100
// global definition of RTC. Initialised in main // global definition of RTC. Initialised in main
extern ESP32Time rtc; extern ESP32Time rtc;
......
...@@ -5,42 +5,55 @@ ...@@ -5,42 +5,55 @@
#include "SDCardLogger.h" #include "SDCardLogger.h"
namespace SDCardLogger { namespace SDCardLogger {
char log_print_buffer[512];
bool printToSerial = false; char log_print_buffer[512];
void printDebugToSerial(bool printToSerialAsWell) { bool printToSerial = false;
printToSerial = printToSerialAsWell;
} void printDebugToSerial(bool printToSerialAsWell) {
int vprintf_into_sd(const char *szFormat, va_list args) { printToSerial = printToSerialAsWell;
String logstring = "[" + rtc.getDateTime() + "] "; }
logstring += szFormat; int vprintf_into_sd(const char *szFormat, va_list args) {
// write evaluated format string into buffer String logstring = "[" + rtc.getDateTime() + "] ";
int ret = vsnprintf(log_print_buffer, sizeof(log_print_buffer), logstring.c_str(), args); logstring += szFormat;
String date = rtc.getDate(); if (!SDUtilities::isSDAvailable()) {
String filename = "/log_" + date + ".txt"; if (printToSerial) {
logstring += " (SD card not available)\n";
// output is now in buffer. write to file. vprintf(logstring.c_str(), args);
if (ret >= 0) { }
if (!SD.exists(filename)) { return 1;
File writeLog = SD.open(filename, FILE_WRITE);
if (!writeLog)
Serial.println("Couldn't open " + filename + " for writing");
delay(50);
writeLog.close();
} }
// write evaluated format string into buffer
int ret = vsnprintf(log_print_buffer, sizeof(log_print_buffer), logstring.c_str(), args);
String date = rtc.getDate();
String filename = "/log_" + date + ".txt";
// output is now in buffer. write to file.
if (ret >= 0) {
if (!SD.exists(filename)) {
File writeLog = SD.open(filename, FILE_WRITE);
if (!writeLog) {
Serial.println("Couldn't open " + filename + " for writing");
SDUtilities::setSDAvailable(false);
return 1;
}
delay(50);
writeLog.close();
}
File logFile = SD.open(filename, FILE_APPEND); File logFile = SD.open(filename, FILE_APPEND);
// debug output // debug output
if (printToSerial) { if (printToSerial) {
vprintf(logstring.c_str(), args); vprintf(logstring.c_str(), args);
}
logFile.write((uint8_t *)log_print_buffer, (size_t)ret);
// to be safe in case of crashes: flush the output
logFile.flush();
logFile.close();
} }
logFile.write((uint8_t *)log_print_buffer, (size_t)ret); return ret;
// to be safe in case of crashes: flush the output
logFile.flush();
logFile.close();
} }
return ret;
}
} // namespace SDCardLogger } // namespace SDCardLogger
...@@ -7,10 +7,12 @@ ...@@ -7,10 +7,12 @@
#include "Definitions.h" #include "Definitions.h"
#include "SD.h" #include "SD.h"
#include "Utilities.h"
namespace SDCardLogger { namespace SDCardLogger {
void printDebugToSerial(bool printToSerialAsWell); void printDebugToSerial(bool printToSerialAsWell);
int vprintf_into_sd(const char *szFormat, va_list args); int vprintf_into_sd(const char *szFormat, va_list args);
void sdNotAvailable(bool sdNotAvailable);
}; // namespace SDCardLogger }; // namespace SDCardLogger
#endif // HOST_CENTRAL_MAST_SDCARDLOGGER_H #endif // HOST_CENTRAL_MAST_SDCARDLOGGER_H
...@@ -4,162 +4,238 @@ ...@@ -4,162 +4,238 @@
#include "Utilities.h" #include "Utilities.h"
void setupSDCard() { namespace SDUtilities {
SPI.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS)) { bool SDAvailable = true;
esp_log_write(ESP_LOG_ERROR, TAG_SD, "Card MOUNT FAIL\n");
// TODO: Error handling void setSDAvailable(bool sdAvailable) {
} else { SDAvailable = sdAvailable;
uint32_t cardSize = SD.cardSize() / (1024 * 1024);
String sdcardSizeString = "SDCard Size: " + String(cardSize) + "MB";
esp_log_write(ESP_LOG_DEBUG, TAG_SD, "%s\n", sdcardSizeString.c_str());
} }
}
void saveStringToSDCard(const std::string &dataString); // Used to try to remount SD card if it fails more than x times (100)
std::list<String> getFilesInDirectory(const String &dirname) { int sdFailCounter = 0;
std::list<String> files;
File dir = openDirectory(dirname); bool isSDAvailable() {
while (true) { return SDAvailable;
File nextFile = dir.openNextFile(); }
if (!nextFile) {
break; void tryRemountingSD() {
try {
setupSDCard(SD_MISO, SD_MOSI, SD_SCLK, SD_CS);
setSDAvailable(true);
} catch (SDSetupException &e) {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "Couldn't remount SD card: %s\n", e.what());
}
sdFailCounter = 0;
}
bool checkSDAvailability(const String &errorMessage) {
if (!isSDAvailable()) {
sdFailCounter++;
if (sdFailCounter % SD_CARD_FAIL_COUNTER == 0) {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "SD card not available. Trying to remount...\n");
tryRemountingSD();
}
String message = errorMessage + " (SD card not available, failed " + sdFailCounter + " times)\n";
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "%s", message.c_str());
// if re-mount was successful, this will be true
return isSDAvailable();
}
return true;
};
std::list<String> getFilesInDirectory(const String &dirname) {
if (!checkSDAvailability("Couldn't get files in directory " + dirname)) {
return {};
} }
if (!nextFile.isDirectory()) { std::list<String> files;
files.emplace_back(nextFile.name()); File dir = openDirectory(dirname);
while (true) {
File nextFile = dir.openNextFile();
if (!nextFile) {
break;
}
if (!nextFile.isDirectory()) {
files.emplace_back(nextFile.name());
}
nextFile.close();
} }
nextFile.close(); return files;
} }
return files;
}
std::optional<String> getLastFileInDirectory(const String &dirname) { std::optional<String> getLastFileInDirectory(const String &dirname) {
File root = openDirectory(dirname); if (!checkSDAvailability("Couldn't get last file in directory " + dirname)) {
root.rewindDirectory(); return std::nullopt;
}
File root = openDirectory(dirname);
root.rewindDirectory();
File file = root.openNextFile(); File file = root.openNextFile();
while (file) { while (file) {
File nextFile = root.openNextFile(); File nextFile = root.openNextFile();
if (!nextFile) { if (!nextFile) {
break; break;
}
file = nextFile;
}
// log
if (file) {
esp_log_write(ESP_LOG_INFO, TAG_SDUTILITIES, "Last file name: %s\n", file.name());
return file.name();
} else {
esp_log_write(ESP_LOG_INFO, TAG_SDUTILITIES, "No file found\n");
return std::nullopt;
} }
file = nextFile;
}
// log
if (file) {
esp_log_write(ESP_LOG_INFO, "getLastFileInDirectory", "Last file name: %s\n", file.name());
return file.name();
} else {
esp_log_write(ESP_LOG_INFO, "getLastFileInDirectory", "No file found\n");
return std::nullopt;
}
}
File openDirectory(const String &dirname) {
File root = SD.open(dirname);
if (!root) {
esp_log_write(ESP_LOG_ERROR, "openDirectory", "Failed to open directory\n");
throw;
} }
if (!root.isDirectory()) { File openDirectory(const String &dirname) {
esp_log_write(ESP_LOG_ERROR, "openDirectory", "Not a directory\n");
throw; if (!checkSDAvailability("Couldn't open directory " + dirname)) {
throw;
}
File root = SD.open(dirname);
if (!root) {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "Failed to open directory\n");
throw;
}
if (!root.isDirectory()) {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "Not a directory\n");
throw;
}
return root;
} }
return root;
}
std::optional<String> getFirstFileNameInDirectory(const String &dirname) {
File root = openDirectory(dirname); std::optional<String> getFirstFileNameInDirectory(const String &dirname) {
root.rewindDirectory();
File file = root.openNextFile(); if (!checkSDAvailability("Couldn't get first file in directory " + dirname)) {
if (file) { return std::nullopt;
esp_log_write(ESP_LOG_INFO, "getFirstFileNameInDirectory", "file found: %s\n", file.name()); }
return file.name();
} else { File root = openDirectory(dirname);
esp_log_write(ESP_LOG_INFO, "getFirstFileNameInDirectory", "no file found\n"); root.rewindDirectory();
return std::nullopt;
File file = root.openNextFile();
if (file) {
esp_log_write(ESP_LOG_INFO, TAG_SDUTILITIES, "file found: %s\n", file.name());
return file.name();
} else {
esp_log_write(ESP_LOG_INFO, TAG_SDUTILITIES, "no file found\n");
return std::nullopt;
}
} }
}
File openForWrite(const String &filePath) { File openForWrite(const String &filePath) {
File file = SD.open(filePath, FILE_WRITE);
if (!file) { if (!checkSDAvailability("Couldn't open file " + filePath + " for writing")) {
esp_log_write(ESP_LOG_ERROR, "SD", "Failed to open file for writing\n"); throw;
throw; }
File file = SD.open(filePath, FILE_WRITE);
if (!file) {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "Failed to open file for writing\n");
throw;
}
return file;
} }
return file;
}
File openForRead(const String &filePath) { File openForRead(const String &filePath) {
File file = SD.open(filePath, FILE_READ);
if (!file) { if (!checkSDAvailability("Couldn't open file " + filePath + " for reading")) {
esp_log_write(ESP_LOG_ERROR, "SD", "Failed to open file for reading\n"); throw;
throw; }
File file = SD.open(filePath, FILE_READ);
if (!file) {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "Failed to open file for reading\n");
throw;
}
return file;
} }
return file;
}
void writeFile(const String &messageToBeSend, const String &filePath) { void writeFile(const String &messageToBeSend, const String &filePath) {
File file = openForWrite(filePath);
if (file.print(messageToBeSend)) { if (!checkSDAvailability("Couldn't write to file " + filePath)) {
esp_log_write(ESP_LOG_INFO, "SD", "File written %s\n", filePath.c_str()); throw;
} else { }
esp_log_write(ESP_LOG_ERROR, "SD", "Write failed %s\n", filePath.c_str());
throw; File file = openForWrite(filePath);
if (file.print(messageToBeSend)) {
esp_log_write(ESP_LOG_INFO, TAG_SDUTILITIES, "File written %s\n", filePath.c_str());
} else {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "Write failed %s\n", filePath.c_str());
throw;
}
file.close();
} }
file.close();
}
String readFile(const String &filePath) {
File file = openForRead(filePath);
String ret; String readFile(const String &filePath) {
if (!checkSDAvailability("Couldn't read file " + filePath)) {
throw;
}
File file = openForRead(filePath);
String ret;
while (file.available()) { while (file.available()) {
ret += (char)file.read(); ret += (char)file.read();
}
file.close();
return ret;
} }
file.close(); void createDirectory(const String &dirname) {
return ret;
} if (!checkSDAvailability("Couldn't create directory " + dirname)) {
throw;
}
void createDirectory(const String &dirname) { if (!SD.exists(dirname)) {
if (!SD.exists(dirname)) { SD.mkdir(dirname);
SD.mkdir(dirname); esp_log_write(ESP_LOG_INFO, TAG_SDUTILITIES, "Created directory: %s\n", dirname.c_str());
esp_log_write(ESP_LOG_INFO, "createDirectory", "Created directory: %s\n", dirname.c_str()); } else {
} else { esp_log_write(ESP_LOG_WARN, TAG_SDUTILITIES, "Directory already exists\n");
esp_log_write(ESP_LOG_WARN, "createDirectory", "Directory already exists\n"); }
} }
}
void setupSDCard(int MISO, int MOSI, int SCLK, int CS) { void setupSDCard(int MISO, int MOSI, int SCLK, int CS) {
SPI.begin(SCLK, MISO, MOSI, CS); SPI.begin(SCLK, MISO, MOSI, CS);
if (!SD.begin(CS)) { if (!SD.begin(CS)) {
esp_log_write(ESP_LOG_ERROR, "Utilities", "Card MOUNT FAIL\n"); esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "Card MOUNT FAIL\n");
throw SDSetupException("Card MOUNT FAIL"); throw SDSetupException("Card MOUNT FAIL");
} else { } else {
uint32_t cardSize = SD.cardSize() / (1024 * 1024); uint32_t cardSize = SD.cardSize() / (1024 * 1024);
String sdcardSizeString = "SDCard Size: " + String(cardSize) + "MB"; String sdcardSizeString = "SDCard Size: " + String(cardSize) + "MB";
esp_log_write(ESP_LOG_DEBUG, "Utilities", "%s\n", sdcardSizeString.c_str()); esp_log_write(ESP_LOG_DEBUG, TAG_SDUTILITIES, "%s\n", sdcardSizeString.c_str());
}
} }
}
void saveStringToSDCard(const std::string &dataString) { void saveStringToSDCard(const std::string &dataString) {
File dataFile = SD.open("/datalog.txt", FILE_APPEND);
// if the file is available, write to it: if (!checkSDAvailability("Couldn't save string to SD card")) {
if (dataFile) { throw SDCardException("Couldn't save string to SD card");
if (dataString.length() > 0) { }
dataFile.println(dataString.c_str());
File dataFile = SD.open("/datalog.txt", FILE_APPEND);
// if the file is available, write to it:
if (dataFile) {
if (dataString.length() > 0) {
dataFile.println(dataString.c_str());
}
dataFile.close();
}
// if the file isn't open, pop up an error:
else {
esp_log_write(ESP_LOG_ERROR, TAG_SDUTILITIES, "error opening datalog.txt\n");
throw SDCardException("error opening datalog.txt");
} }
dataFile.close();
}
// if the file isn't open, pop up an error:
else {
esp_log_write(ESP_LOG_ERROR, "Utilities", "error opening datalog.txt\n");
throw SDCardException("error opening datalog.txt");
} }
} } // namespace SDUtilities
// I don't think this does anything. Copied from the example // I don't think this does anything. Copied from the example
void turnOffLEDs() { // Set LED OFF void turnOffLEDs() { // Set LED OFF
......
...@@ -9,21 +9,26 @@ ...@@ -9,21 +9,26 @@
#include "SD.h" #include "SD.h"
#include "SDCardException.h" #include "SDCardException.h"
#include "SDSetupException.h" #include "SDSetupException.h"
#include "WiFi.h"
#include <Arduino.h> #include <Arduino.h>
#include <Definitions.h> #include <Definitions.h>
#include <WString.h> #include <WString.h>
#include <list> #include <list>
#include "WiFi.h"
File openDirectory(const String &dirname); namespace SDUtilities {
std::list<String> getFilesInDirectory(const String &dirname); File openDirectory(const String &dirname);
std::optional<String> getFirstFileNameInDirectory(const String &dirname); std::list<String> getFilesInDirectory(const String &dirname);
std::optional<String> getLastFileInDirectory(const String &dirname); std::optional<String> getFirstFileNameInDirectory(const String &dirname);
void writeFile(const String &messageToBeSend, const String &filePath); std::optional<String> getLastFileInDirectory(const String &dirname);
String readFile(const String &filePath); void writeFile(const String &messageToBeSend, const String &filePath);
void createDirectory(const String &dirname); String readFile(const String &filePath);
void setupSDCard(int MISO, int MOSI, int SCLK, int CS); void createDirectory(const String &dirname);
void saveStringToSDCard(const std::string &dataString); void setupSDCard(int MISO, int MOSI, int SCLK, int CS);
void saveStringToSDCard(const std::string &dataString);
void setSDAvailable(bool sdAvailable);
bool isSDAvailable();
} // namespace SDUtilities
void turnOffLEDs(); void turnOffLEDs();
String getMacAddressAsString(const uint8_t *mac); String getMacAddressAsString(const uint8_t *mac);
String documentToLineProtocolString(const DynamicJsonDocument &doc); String documentToLineProtocolString(const DynamicJsonDocument &doc);
......
...@@ -76,6 +76,14 @@ void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) { ...@@ -76,6 +76,14 @@ void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) {
DynamicJsonDocument doc = parseReceivedJsonData(data); DynamicJsonDocument doc = parseReceivedJsonData(data);
// 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_DEBUG, TAG_ESPNOW, (success == ESP_OK) ? "Response sent\n" : "Failed to respond\n");
String macAddress = getMacAddressAsString(mac); String macAddress = getMacAddressAsString(mac);
// add timestamp and mac address // add timestamp and mac address
...@@ -88,7 +96,11 @@ void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) { ...@@ -88,7 +96,11 @@ void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) {
std::string dataString{}; std::string dataString{};
serializeJson(doc, dataString); serializeJson(doc, dataString);
saveStringToSDCard(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());
}
String lineData = documentToLineProtocolString(doc); String lineData = documentToLineProtocolString(doc);
...@@ -97,17 +109,11 @@ void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) { ...@@ -97,17 +109,11 @@ void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len) {
xSemaphoreTake(xMutex, portMAX_DELAY); xSemaphoreTake(xMutex, portMAX_DELAY);
queue.push(lineData); queue.push(lineData);
xSemaphoreGive(xMutex); xSemaphoreGive(xMutex);
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_DEBUG, TAG_ESPNOW, (success == ESP_OK) ? "Response sent\n" : "Failed to respond\n");
} }
[[noreturn]] void esp_loop(){ [[noreturn]] void esp_loop() {
while (true){} while (true) {
}
} }
[[noreturn]] void ESPNOWReceiveTask(void *parameter) { [[noreturn]] void ESPNOWReceiveTask(void *parameter) {
...@@ -150,7 +156,12 @@ void setup() { ...@@ -150,7 +156,12 @@ void setup() {
// Set console baud rate // Set console baud rate
Serial.begin(115200); Serial.begin(115200);
delay(10); delay(10);
setupSDCard(SD_MISO, SD_MOSI, SD_SCLK, SD_CS); try {
SDUtilities::setupSDCard(SD_MISO, SD_MOSI, SD_SCLK, SD_CS);
} catch (const SDSetupException &e) {
SDUtilities::setSDAvailable(false);
esp_log_write(ESP_LOG_ERROR, TAG_MAIN, "SD Card setup failed: %s\n", e.what());
}
SDCardLogger::printDebugToSerial(true); SDCardLogger::printDebugToSerial(true);
...@@ -255,5 +266,5 @@ void loop() { ...@@ -255,5 +266,5 @@ void loop() {
connectionManager.modemPowerOff(); connectionManager.modemPowerOff();
delay(60000); delay(10000);
} }
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