diff --git a/.clang-format b/.clang-format index c314114..17ada6d 100644 --- a/.clang-format +++ b/.clang-format @@ -1,7 +1,8 @@ --- Language: Cpp AccessModifierOffset: -2 -AlignAfterOpenBracket: Align +AlignAfterOpenBracket: BlockIndent +BracedInitializerIndentWidth: 4 AlignArrayOfStructures: None AlignConsecutiveAssignments: Enabled: false @@ -65,29 +66,28 @@ AlignConsecutiveTableGenDefinitionColons: AlignFunctionDeclarations: false AlignFunctionPointers: false PadOperators: false -AlignEscapedNewlines: Right -AlignOperands: Align +AlignEscapedNewlines: Left +AlignOperands: DontAlign AlignTrailingComments: - Kind: Always - OverEmptyLines: 0 + Kind: Always + OverEmptyLines: 2 AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true -AllowBreakBeforeNoexceptSpecifier: Never -AllowShortBlocksOnASingleLine: Never +AllowBreakBeforeNoexceptSpecifier: Always +AllowShortBlocksOnASingleLine: Empty AllowShortCaseExpressionOnASingleLine: true -AllowShortCaseLabelsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true AllowShortCompoundRequirementOnASingleLine: true -AllowShortEnumsOnASingleLine: true -AllowShortFunctionsOnASingleLine: All -AllowShortIfStatementsOnASingleLine: Never +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: AllIfsAndElse AllowShortLambdasOnASingleLine: All -AllowShortLoopsOnASingleLine: false +AllowShortLoopsOnASingleLine: true AllowShortNamespacesOnASingleLine: false -AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakBeforeMultilineStrings: false AttributeMacros: - __capability -BinPackArguments: true +BinPackArguments: false BinPackParameters: BinPack BitFieldColonSpacing: Both BraceWrapping: @@ -99,7 +99,7 @@ BraceWrapping: AfterFunction: false AfterNamespace: false AfterObjCDeclaration: false - AfterStruct: false + AfterStruct: true AfterUnion: false BeforeCatch: false BeforeElse: false @@ -109,23 +109,23 @@ BraceWrapping: SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true -BreakAdjacentStringLiterals: true +BreakAdjacentStringLiterals: false BreakAfterAttributes: Leave BreakAfterJavaFieldAnnotations: false -BreakAfterReturnType: None -BreakArrays: true +BreakAfterReturnType: Automatic +BreakArrays: false BreakBeforeBinaryOperators: None BreakBeforeConceptDeclarations: Always -BreakBeforeBraces: Attach +BreakBeforeBraces: Custom BreakBeforeInlineASMColon: OnlyMultiline BreakBeforeTernaryOperators: true BreakBinaryOperations: Never BreakConstructorInitializers: BeforeColon BreakFunctionDefinitionParameters: false BreakInheritanceList: BeforeColon -BreakStringLiterals: true +BreakStringLiterals: false BreakTemplateDeclarations: MultiLine -ColumnLimit: 80 +ColumnLimit: 120 CommentPragmas: '^ IWYU pragma:' CompactNamespaces: false ConstructorInitializerIndentWidth: 4 @@ -143,7 +143,7 @@ ForEachMacros: - BOOST_FOREACH IfMacros: - KJ_IF_MAYBE -IncludeBlocks: Preserve +IncludeBlocks: Regroup IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' Priority: 2 @@ -161,16 +161,16 @@ IncludeIsMainRegex: '(Test)?$' IncludeIsMainSourceRegex: '' IndentAccessModifiers: false IndentCaseBlocks: false -IndentCaseLabels: false +IndentCaseLabels: true IndentExportBlock: true IndentExternBlock: AfterExternBlock IndentGotoLabels: true IndentPPDirectives: None IndentRequiresClause: true -IndentWidth: 2 +IndentWidth: 4 IndentWrappedFunctionNames: false InsertBraces: false -InsertNewlineAtEOF: false +InsertNewlineAtEOF: true InsertTrailingCommas: None IntegerLiteralSeparator: Binary: 0 @@ -183,10 +183,10 @@ JavaScriptQuotes: Leave JavaScriptWrapImports: true KeepEmptyLines: AtEndOfFile: false - AtStartOfBlock: true - AtStartOfFile: true + AtStartOfBlock: false + AtStartOfFile: false KeepFormFeed: false -LambdaBodyIndentation: Signature +LambdaBodyIndentation: OuterScope LineEnding: DeriveLF MacroBlockBegin: '' MacroBlockEnd: '' @@ -217,7 +217,7 @@ QualifierAlignment: Leave ReferenceAlignment: Pointer ReflowComments: Always RemoveBracesLLVM: false -RemoveEmptyLinesInUnwrappedLines: false +RemoveEmptyLinesInUnwrappedLines: true RemoveParentheses: Leave RemoveSemicolon: false RequiresClausePosition: OwnLine @@ -228,8 +228,8 @@ SkipMacroDefinitionBody: false SortIncludes: CaseSensitive SortJavaStaticImport: Before SortUsingDeclarations: LexicographicNumeric -SpaceAfterCStyleCast: false -SpaceAfterLogicalNot: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: true SpaceAfterTemplateKeyword: true SpaceAroundPointerQualifiers: Default SpaceBeforeAssignmentOperators: true @@ -255,7 +255,7 @@ SpaceBeforeSquareBrackets: false SpaceInEmptyBlock: false SpacesBeforeTrailingComments: 1 SpacesInAngles: Never -SpacesInContainerLiterals: true +SpacesInContainerLiterals: false SpacesInLineCommentPrefix: Minimum: 1 Maximum: -1 @@ -274,7 +274,7 @@ StatementMacros: - Q_UNUSED - QT_REQUIRE_VERSION TableGenBreakInsideDAGArg: DontBreak -TabWidth: 8 +TabWidth: 4 UseTab: Never VerilogBreakBetweenInstancePorts: true WhitespaceSensitiveMacros: @@ -285,4 +285,3 @@ WhitespaceSensitiveMacros: - STRINGIZE WrapNamespaceBodyWithEmptyLines: Leave ... - diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml new file mode 100644 index 0000000..a5bc7bc --- /dev/null +++ b/.github/workflows/format-check.yml @@ -0,0 +1,31 @@ +name: Format Check + +on: + push: + branches: + - main + pull_request: + +jobs: + format-check: + name: Check Code Formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v4 + + - name: Install Dependencies + run: npm ci + + - name: Install Clang Format + uses: jidicula/clang-format-action@v4.15.0 + with: + clang-format-version: '20' + check-path: 'src' + + - name: Run Prettier + run: ./node_modules/.bin/prettier --check 'src/**/*.{js,json,css,html}' platformio.ini package.json README.md vite.config.mjs tailwind.config.js diff --git a/package.json b/package.json index b06e637..e41546b 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "scripts": { "build": "npm run format && npm run assets && npm run pio", "format": "npm run format:cpp && npm run format:web", - "format:web": "prettier --write 'src/**/*.{js,json,css,html}' platformio.ini package.json readme.md vite.config.mjs tailwind.config.js", - "format:cpp": "command -v dlang-format >/dev/null 2>&1 && find src -iname '*.h' -o -iname '*.cpp' | xargs clang-format -i || echo 'clang-format not found, skipping format:cpp'", + "format:web": "prettier --write 'src/**/*.{js,json,css,html}' platformio.ini package.json README.md vite.config.mjs tailwind.config.js", + "format:cpp": "command -v clang-format >/dev/null 2>&1 && find src -iname '*.h' -o -iname '*.cpp' -o -iname '*.ino' | xargs clang-format -i || echo 'clang-format not found, skipping format:cpp'", "assets": "vite build --emptyOutDir", "pio": "npm run pio:firmware && sleep 2 && npm run pio:filesystem", "pio:firmware": "pio run -t upload", diff --git a/src/JsonSetting.cpp b/src/JsonSetting.cpp index bebaf21..0a7f116 100644 --- a/src/JsonSetting.cpp +++ b/src/JsonSetting.cpp @@ -1,34 +1,32 @@ #include "JsonSetting.h" String JsonSetting::intVectorToString(const std::vector &vec) { - String result; - for (size_t i = 0; i < vec.size(); ++i) { - result += String(vec[i]); - if (i < vec.size() - 1) { - result += ","; + String result; + for (size_t i = 0; i < vec.size(); ++i) { + result += String(vec[i]); + if (i < vec.size() - 1) { + result += ","; + } } - } - return result; + return result; } bool JsonSetting::validate(String str) { - switch (type) { - case JsonSettingType::JST_INT_VECTOR: - return validateIntVector(str); - default: - return true; - } + switch (type) { + case JsonSettingType::JST_INT_VECTOR: return validateIntVector(str); + default: return true; + } } bool JsonSetting::validateIntVector(String str) { - for (size_t i = 0; i < str.length(); ++i) { - if (str[i] == ',' || str[i] == '-') { - continue; + for (size_t i = 0; i < str.length(); ++i) { + if (str[i] == ',' || str[i] == '-') { + continue; + } + if (str[i] < '0' || str[i] > '9') { + lastValidationError = "Non-integer value found"; + return false; + } } - if (str[i] < '0' || str[i] > '9') { - lastValidationError = "Non-integer value found"; - return false; - } - } - return true; + return true; } diff --git a/src/JsonSetting.h b/src/JsonSetting.h index 7f89792..e9cec47 100644 --- a/src/JsonSetting.h +++ b/src/JsonSetting.h @@ -1,45 +1,42 @@ -#ifndef JsonSetting_h -#define JsonSetting_h +#pragma once -#include "JsonSetting.h" #include #include -typedef enum { JST_STR, JST_INT, JST_FLOAT, JST_INT_VECTOR } JsonSettingType; +typedef enum { + JST_STR, + JST_INT, + JST_FLOAT, + JST_INT_VECTOR +} JsonSettingType; class JsonSetting { -public: - JsonSetting(String strDefault) - : type(JsonSettingType::JST_STR), strDefault(strDefault) {} - JsonSetting(int intDefault) - : type(JsonSettingType::JST_INT), intDefault(intDefault) {} - JsonSetting(float floatDefault) - : type(JsonSettingType::JST_FLOAT), floatDefault(floatDefault) { - strDefault = String(floatDefault); - } - JsonSetting(std::vector intVectorDefault) - : type(JsonSettingType::JST_INT_VECTOR), - intVectorDefault(intVectorDefault) { - strDefault = intVectorToString(intVectorDefault); - } - - bool validate(String str); - String getLastValidationError() { return lastValidationError; } - -private: - JsonSettingType type; - - String strDefault; - int intDefault; - float floatDefault; - std::vector intVectorDefault; - - String intVectorToString(const std::vector &vec); - - String lastValidationError; - bool validateIntVector(String str); - - friend class JsonSettings; + public: + JsonSetting(String strDefault) : type(JsonSettingType::JST_STR), strDefault(strDefault) {} + JsonSetting(int intDefault) : type(JsonSettingType::JST_INT), intDefault(intDefault) {} + JsonSetting(float floatDefault) : type(JsonSettingType::JST_FLOAT), floatDefault(floatDefault) { + strDefault = String(floatDefault); + } + JsonSetting(std::vector intVectorDefault) + : type(JsonSettingType::JST_INT_VECTOR), intVectorDefault(intVectorDefault) { + strDefault = intVectorToString(intVectorDefault); + } + + bool validate(String str); + String getLastValidationError() { return lastValidationError; } + + private: + JsonSettingType type; + + String strDefault; + int intDefault; + float floatDefault; + std::vector intVectorDefault; + + String intVectorToString(const std::vector &vec); + + String lastValidationError; + bool validateIntVector(String str); + + friend class JsonSettings; }; - -#endif diff --git a/src/JsonSettings.cpp b/src/JsonSettings.cpp index bde4ae5..fe41a7a 100644 --- a/src/JsonSettings.cpp +++ b/src/JsonSettings.cpp @@ -1,154 +1,141 @@ #include "JsonSettings.h" -#include "ArduinoJson.h" + +#include #include String JsonSettings::getString(const char *key) { - preferences.begin(name, true); - String value = preferences.getString(key, this->find(key).strDefault); - preferences.end(); - return value; + preferences.begin(name, true); + String value = preferences.getString(key, this->find(key).strDefault); + preferences.end(); + return value; } int JsonSettings::getInt(const char *key) { - preferences.begin(name, true); - int value = preferences.getInt(key, this->find(key).intDefault); - preferences.end(); - return value; + preferences.begin(name, true); + int value = preferences.getInt(key, this->find(key).intDefault); + preferences.end(); + return value; } float JsonSettings::getFloat(const char *key) { - preferences.begin(name, true); - float value = preferences.getFloat(key, this->find(key).floatDefault); - preferences.end(); - return value; + preferences.begin(name, true); + float value = preferences.getFloat(key, this->find(key).floatDefault); + preferences.end(); + return value; } std::vector JsonSettings::getIntVector(const char *key) { - preferences.begin(name, true); - String value = preferences.getString(key, this->find(key).strDefault); - preferences.end(); - - std::vector intVector; - std::istringstream stream(value.c_str()); - std::string token; - while (std::getline(stream, token, ',')) { - try { - intVector.push_back(std::stoi(token)); - } catch (const std::invalid_argument &) { - throw std::runtime_error("Invalid CSV: Non-integer value found"); - } catch (const std::out_of_range &) { - throw std::runtime_error("Invalid CSV: Integer value out of range"); + preferences.begin(name, true); + String value = preferences.getString(key, this->find(key).strDefault); + preferences.end(); + + std::vector intVector; + std::istringstream stream(value.c_str()); + std::string token; + while (std::getline(stream, token, ',')) { + try { + intVector.push_back(std::stoi(token)); + } catch (const std::invalid_argument &) { + throw std::runtime_error("Invalid CSV: Non-integer value found"); + } catch (const std::out_of_range &) { + throw std::runtime_error("Invalid CSV: Integer value out of range"); + } } - } - return intVector; + return intVector; } void JsonSettings::putString(const char *key, String value) { - preferences.begin(name, false); - preferences.putString(key, value); - preferences.end(); + preferences.begin(name, false); + preferences.putString(key, value); + preferences.end(); } void JsonSettings::putInt(const char *key, int value) { - preferences.begin(name, false); - preferences.putInt(key, value); - preferences.end(); + preferences.begin(name, false); + preferences.putInt(key, value); + preferences.end(); } void JsonSettings::putFloat(const char *key, float value) { - preferences.begin(name, false); - preferences.putFloat(key, value); - preferences.end(); + preferences.begin(name, false); + preferences.putFloat(key, value); + preferences.end(); } void JsonSettings::putIntVector(const char *key, std::vector value) { - std::ostringstream stream; - for (size_t i = 0; i < value.size(); ++i) { - stream << value[i]; - if (i < value.size() - 1) { - stream << ","; + std::ostringstream stream; + for (size_t i = 0; i < value.size(); ++i) { + stream << value[i]; + if (i < value.size() - 1) { + stream << ","; + } } - } - putString(key, stream.str().c_str()); + putString(key, stream.str().c_str()); } JsonDocument JsonSettings::toJson() { - JsonDocument settings; - - preferences.begin(name, true); - - for (const auto &pair : map) { - const String &key = pair.first; - const JsonSetting &setting = pair.second; - - switch (setting.type) { - case JsonSettingType::JST_STR: - case JsonSettingType::JST_INT_VECTOR: - settings[key] = preferences.getString(key.c_str(), setting.strDefault); - break; - case JsonSettingType::JST_INT: - settings[key] = preferences.getInt(key.c_str(), setting.intDefault); - break; - case JsonSettingType::JST_FLOAT: - settings[key] = preferences.getFloat(key.c_str(), setting.floatDefault); - break; + JsonDocument settings; + + preferences.begin(name, true); + + for (const auto &pair : map) { + const String &key = pair.first; + const JsonSetting &setting = pair.second; + + switch (setting.type) { + case JsonSettingType::JST_STR: + case JsonSettingType::JST_INT_VECTOR: + settings[key] = preferences.getString(key.c_str(), setting.strDefault); + break; + case JsonSettingType::JST_INT: settings[key] = preferences.getInt(key.c_str(), setting.intDefault); break; + case JsonSettingType::JST_FLOAT: + settings[key] = preferences.getFloat(key.c_str(), setting.floatDefault); + break; + } } - } - preferences.end(); - return settings; + preferences.end(); + return settings; } bool JsonSettings::fromJson(JsonDocument settings) { - preferences.begin(name, false); - - for (JsonPair kv : settings.as()) { - const char *key = kv.key().c_str(); - JsonSetting setting = this->find(key); - - if (!setting.validate(kv.value().as())) { - lastValidationError = setting.getLastValidationError(); - lastValidationKey = String(key); - return false; + preferences.begin(name, false); + + for (JsonPair kv : settings.as()) { + const char *key = kv.key().c_str(); + JsonSetting setting = this->find(key); + + if (! setting.validate(kv.value().as())) { + lastValidationError = setting.getLastValidationError(); + lastValidationKey = String(key); + return false; + } + + switch (setting.type) { + case JsonSettingType::JST_INT_VECTOR: + case JsonSettingType::JST_STR: preferences.putString(key, kv.value().as()); break; + case JsonSettingType::JST_INT: preferences.putInt(key, kv.value().as()); break; + case JsonSettingType::JST_FLOAT: preferences.putFloat(key, kv.value().as()); break; + } } - switch (setting.type) { - case JsonSettingType::JST_INT_VECTOR: - case JsonSettingType::JST_STR: - Serial.println("Setting key: " + String(key) + - " to value: " + kv.value().as()); - preferences.putString(key, kv.value().as()); - break; - case JsonSettingType::JST_INT: - Serial.println("Setting key: " + String(key) + - " to value: " + String(kv.value().as())); - preferences.putInt(key, kv.value().as()); - break; - case JsonSettingType::JST_FLOAT: - Serial.println("Setting key: " + String(key) + - " to value: " + String(kv.value().as())); - preferences.putFloat(key, kv.value().as()); - break; - } - } - - preferences.end(); + preferences.end(); - return true; + return true; } bool JsonSettings::reset() { - preferences.begin("config", false); - preferences.clear(); - preferences.end(); + preferences.begin("config", false); + preferences.clear(); + preferences.end(); - return fromJson(toJson()); + return fromJson(toJson()); } JsonSetting JsonSettings::find(const char *key) { - auto it = this->map.find(key); - if (it == this->map.end()) { - throw std::runtime_error("Key not found in settings map"); - } - return it->second; + auto it = this->map.find(key); + if (it == this->map.end()) { + throw std::runtime_error("Key not found in settings map"); + } + return it->second; } diff --git a/src/JsonSettings.h b/src/JsonSettings.h index 1fb6595..2de9f97 100644 --- a/src/JsonSettings.h +++ b/src/JsonSettings.h @@ -1,44 +1,41 @@ -#ifndef JsonSettings_h -#define JsonSettings_h +#pragma once -#include "Arduino.h" #include "JsonSetting.h" + +#include #include #include #include class JsonSettings { -public: - JsonSettings(const char *name, std::map map) - : name(name), map(map) {} + public: + JsonSettings(const char *name, std::map map) : name(name), map(map) {} - String getString(const char *key); - int getInt(const char *key); - float getFloat(const char *key); - std::vector getIntVector(const char *key); + String getString(const char *key); + int getInt(const char *key); + float getFloat(const char *key); + std::vector getIntVector(const char *key); - void putString(const char *key, String value); - void putInt(const char *key, int value); - void putFloat(const char *key, float value); - void putIntVector(const char *key, std::vector value); + void putString(const char *key, String value); + void putInt(const char *key, int value); + void putFloat(const char *key, float value); + void putIntVector(const char *key, std::vector value); - JsonDocument toJson(); - bool fromJson(JsonDocument settings); - bool reset(); + JsonDocument toJson(); + bool fromJson(JsonDocument settings); + bool reset(); - String getLastValidationError() { return lastValidationError; } - String getLastValidationKey() { return lastValidationKey; } + String getLastValidationError() { return lastValidationError; } + String getLastValidationKey() { return lastValidationKey; } -private: - const char *name; - std::map map; + private: + const char *name; + std::map map; - String lastValidationError; - String lastValidationKey; + String lastValidationError; + String lastValidationKey; - JsonSetting find(const char *key); + JsonSetting find(const char *key); - Preferences preferences; + Preferences preferences; }; - -#endif diff --git a/src/SplitFlapDisplay.cpp b/src/SplitFlapDisplay.cpp index f30a8e2..355a4a7 100644 --- a/src/SplitFlapDisplay.cpp +++ b/src/SplitFlapDisplay.cpp @@ -1,341 +1,317 @@ #include "SplitFlapDisplay.h" + #include "JsonSettings.h" #include "SplitFlapModule.h" #include "SplitFlapMqtt.h" -SplitFlapDisplay::SplitFlapDisplay(JsonSettings &settings) - : settings(settings) {} +SplitFlapDisplay::SplitFlapDisplay(JsonSettings &settings) : settings(settings) {} void SplitFlapDisplay::init() { - numModules = settings.getInt("moduleCount"); - stepsPerRot = settings.getInt("stepsPerRot"); - displayOffset = settings.getInt("displayOffset"); - magnetPosition = settings.getInt("magnetPosition"); - maxVel = settings.getFloat("maxVel"); - - std::vector settingAddresses = settings.getIntVector("moduleAddresses"); - for (int i = 0; i < numModules; i++) { - moduleAddresses[i] = (uint8_t)settingAddresses[i]; - } - - std::vector settingOffsets = settings.getIntVector("moduleOffsets"); - for (int i = 0; i < numModules; i++) { - moduleOffsets[i] = settingOffsets[i]; - } - - Serial.print("Module Offsets: "); - for (int i = 0; i < numModules; i++) { - Serial.print(moduleOffsets[i]); - Serial.print(" "); - } - Serial.println(); - - for (uint8_t i = 0; i < numModules; i++) { - modules[i] = - SplitFlapModule(moduleAddresses[i], stepsPerRot, - moduleOffsets[i] + displayOffset, magnetPosition); - } - - SDAPin = settings.getInt("sdaPin"); - SCLPin = settings.getInt("sclPin"); - - Wire.begin(SDAPin, SCLPin); - Wire.setClock(400000); - - for (uint8_t i = 0; i < numModules; i++) { - modules[i].init(); - } + numModules = settings.getInt("moduleCount"); + stepsPerRot = settings.getInt("stepsPerRot"); + displayOffset = settings.getInt("displayOffset"); + magnetPosition = settings.getInt("magnetPosition"); + maxVel = settings.getFloat("maxVel"); + + std::vector settingAddresses = settings.getIntVector("moduleAddresses"); + for (int i = 0; i < numModules; i++) { + moduleAddresses[i] = (uint8_t) settingAddresses[i]; + } + + std::vector settingOffsets = settings.getIntVector("moduleOffsets"); + for (int i = 0; i < numModules; i++) { + moduleOffsets[i] = settingOffsets[i]; + } + + Serial.print("Module Offsets: "); + for (int i = 0; i < numModules; i++) { + Serial.print(moduleOffsets[i]); + Serial.print(" "); + } + Serial.println(); + + for (uint8_t i = 0; i < numModules; i++) { + modules[i] = SplitFlapModule(moduleAddresses[i], stepsPerRot, moduleOffsets[i] + displayOffset, magnetPosition); + } + + SDAPin = settings.getInt("sdaPin"); + SCLPin = settings.getInt("sclPin"); + + Wire.begin(SDAPin, SCLPin); + Wire.setClock(400000); + + for (uint8_t i = 0; i < numModules; i++) { + modules[i].init(); + } } void SplitFlapDisplay::testAll() { - char testChars[37] = {' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', - 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', - '3', '4', '5', '6', '7', '8', '9'}; - int numChars = sizeof(testChars) / sizeof(testChars[0]); - int targetPositions[numModules]; - - int charPos; - for (int i = 0; i < numChars; i++) { - // Serial.print("Target Positions: ["); - // fill array with same char - - for (int j = 0; j < numModules; j++) { - targetPositions[j] = modules[j].getCharPosition(testChars[i]); - // Serial.print(targetPositions[j]); - // Serial.print(" , "); - } - // Serial.println("]"); + char testChars[37] = {' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + int numChars = sizeof(testChars) / sizeof(testChars[0]); + int targetPositions[numModules]; + + int charPos; + for (int i = 0; i < numChars; i++) { + // Serial.print("Target Positions: ["); + // fill array with same char + + for (int j = 0; j < numModules; j++) { + targetPositions[j] = modules[j].getCharPosition(testChars[i]); + // Serial.print(targetPositions[j]); + // Serial.print(" , "); + } + // Serial.println("]"); - moveTo(targetPositions); - delay(500); - } + moveTo(targetPositions); + delay(500); + } } void SplitFlapDisplay::testRandom(float speed) { - char testChars[37] = {' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', - 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', - '3', '4', '5', '6', '7', '8', '9'}; - - int targetPositions[numModules]; - char randChar; - - Serial.print("Target: "); - for (int i = 0; i < numModules; i++) { - randChar = testChars[random(0, 37)]; - targetPositions[i] = modules[i].getCharPosition(randChar); - Serial.print(randChar); - } - Serial.println(" "); - moveTo(targetPositions, speed); -} + char testChars[37] = {' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; -void SplitFlapDisplay::testCount() { + int targetPositions[numModules]; + char randChar; - int count = 0; - int maxCount = pow(10, numModules); - char targetChar; - int targetInteger; + Serial.print("Target: "); + for (int i = 0; i < numModules; i++) { + randChar = testChars[random(0, 37)]; + targetPositions[i] = modules[i].getCharPosition(randChar); + Serial.print(randChar); + } + Serial.println(" "); + moveTo(targetPositions, speed); +} - int targetPositions[numModules]; +void SplitFlapDisplay::testCount() { + int count = 0; + int maxCount = pow(10, numModules); + char targetChar; + int targetInteger; + + int targetPositions[numModules]; + + for (int i = 0; i < maxCount; i++) { + // get each character in the count integer + for (int j = 0; j < numModules; j++) { + targetInteger = (i % (int) pow(10, j + 1)) / (int) pow(10, j); + targetChar = targetInteger + '0'; // convert to char + targetPositions[numModules - j - 1] = modules[j].getCharPosition(targetChar); + } - for (int i = 0; i < maxCount; i++) { - // get each character in the count integer - for (int j = 0; j < numModules; j++) { - targetInteger = (i % (int)pow(10, j + 1)) / (int)pow(10, j); - targetChar = targetInteger + '0'; // convert to char - targetPositions[numModules - j - 1] = - modules[j].getCharPosition(targetChar); + moveTo(targetPositions); + delay(250); } - - moveTo(targetPositions); - delay(250); - } } void SplitFlapDisplay::home(float speed) { - Serial.println("Homing"); - int targetPositions[numModules]; - for (int i = 0; i < numModules; i++) { - targetPositions[i] = - (modules[i].getPosition() - 1 + stepsPerRot) % stepsPerRot; - } - startMotors(); - moveTo(targetPositions, speed, false); - char homeChar = ' '; - int charPosition; - for (int i = 0; i < numModules; i++) { - targetPositions[i] = modules[i].getCharPosition(homeChar); - } - moveTo(targetPositions, speed); + Serial.println("Homing"); + int targetPositions[numModules]; + for (int i = 0; i < numModules; i++) { + targetPositions[i] = (modules[i].getPosition() - 1 + stepsPerRot) % stepsPerRot; + } + startMotors(); + moveTo(targetPositions, speed, false); + char homeChar = ' '; + int charPosition; + for (int i = 0; i < numModules; i++) { + targetPositions[i] = modules[i].getCharPosition(homeChar); + } + moveTo(targetPositions, speed); } -void SplitFlapDisplay::homeToString(String homeString, float speed, - bool centering) { - Serial.println("Homing"); - int targetPositions[numModules]; - for (int i = 0; i < numModules; i++) { - targetPositions[i] = - (modules[i].getPosition() - 1 + stepsPerRot) % stepsPerRot; - } - startMotors(); - moveTo(targetPositions, speed, false); - writeString(homeString, speed, centering); +void SplitFlapDisplay::homeToString(String homeString, float speed, bool centering) { + Serial.println("Homing"); + int targetPositions[numModules]; + for (int i = 0; i < numModules; i++) { + targetPositions[i] = (modules[i].getPosition() - 1 + stepsPerRot) % stepsPerRot; + } + startMotors(); + moveTo(targetPositions, speed, false); + writeString(homeString, speed, centering); } void SplitFlapDisplay::homeToChar(char homeChar, float speed) { - Serial.println("Homing"); - int targetPositions[numModules]; - for (int i = 0; i < numModules; i++) { - targetPositions[i] = - (modules[i].getPosition() - 1 + stepsPerRot) % stepsPerRot; - } - startMotors(); - moveTo(targetPositions, speed, false); - - for (int i = 0; i < numModules; i++) { - targetPositions[i] = modules[i].getCharPosition(homeChar); - } - moveTo(targetPositions, true, speed); + Serial.println("Homing"); + int targetPositions[numModules]; + for (int i = 0; i < numModules; i++) { + targetPositions[i] = (modules[i].getPosition() - 1 + stepsPerRot) % stepsPerRot; + } + startMotors(); + moveTo(targetPositions, speed, false); + + for (int i = 0; i < numModules; i++) { + targetPositions[i] = modules[i].getCharPosition(homeChar); + } + moveTo(targetPositions, true, speed); } void SplitFlapDisplay::writeChar(char inputChar, float speed) { - - int targetPositions[numModules]; - // Iterate through the input string and process each character - for (int i = 0; i < numModules; i++) { - targetPositions[i] = modules[i].getCharPosition(inputChar); - } - moveTo(targetPositions, speed); + int targetPositions[numModules]; + // Iterate through the input string and process each character + for (int i = 0; i < numModules; i++) { + targetPositions[i] = modules[i].getCharPosition(inputChar); + } + moveTo(targetPositions, speed); } -void SplitFlapDisplay::writeString(String inputString, float speed, - bool centering) { +void SplitFlapDisplay::writeString(String inputString, float speed, bool centering) { + String displayString = inputString.substring(0, numModules); - String displayString = inputString.substring(0, numModules); + if (centering) { + int totalPadding = numModules - displayString.length(); + int paddingLeft = totalPadding / 2; + int paddingRight = totalPadding - paddingLeft; - if (centering) { - int totalPadding = numModules - displayString.length(); - int paddingLeft = totalPadding / 2; - int paddingRight = totalPadding - paddingLeft; + // Add padding to the left + String result = ""; + for (int i = 0; i < paddingLeft; i++) { + result += " "; + } - // Add padding to the left - String result = ""; - for (int i = 0; i < paddingLeft; i++) { - result += " "; - } + // Add the original string + result += displayString; - // Add the original string - result += displayString; + // Add padding to the right + for (int i = 0; i < paddingRight; i++) { + result += " "; + } + displayString = result; + } else { // pad blanks to end, if no centering + while (displayString.length() < numModules) { // Pad with spaces + displayString += " "; // Padding with space + } + } - // Add padding to the right - for (int i = 0; i < paddingRight; i++) { - result += " "; + int targetPositions[numModules]; + // Iterate through the input string and process each character + for (int i = 0; i < displayString.length(); i++) { + char currentChar = displayString[i]; + // Serial.println(currentChar); + targetPositions[i] = modules[i].getCharPosition(currentChar); } - displayString = result; - } else { // pad blanks to end, if no centering - while (displayString.length() < numModules) { // Pad with spaces - displayString += " "; // Padding with space + moveTo(targetPositions, speed); + + if (mqtt && mqtt->isConnected()) { + mqtt->publishState(displayString); } - } - - int targetPositions[numModules]; - // Iterate through the input string and process each character - for (int i = 0; i < displayString.length(); i++) { - char currentChar = displayString[i]; - // Serial.println(currentChar); - targetPositions[i] = modules[i].getCharPosition(currentChar); - } - moveTo(targetPositions, speed); - - if (mqtt && mqtt->isConnected()) { - mqtt->publishState(displayString); - } } -void SplitFlapDisplay::moveTo(int targetPositions[], float speed, - bool releaseMotors) { - - // TODO check length of array and return if empty - - speed = constrain(speed, 2, maxVel); - float stepsPerSecond = (speed / 60) * stepsPerRot; - float timePerStep = 1000000 / stepsPerSecond; - - unsigned long currentTime = micros(); - - int checkIntervalUs = - 20 * 1000; // How often to check each modules hall effect sensor, less - // than 20ms causes issues with bouncing - int startStopDelay = 200; // time to wait to let motor realign itself to - // magnetic field on stop and start - - bool resetLatches[numModules] = - {}; // Initialize to false //start with latch on to prevent case where the - // motion starts with the magnet over the sensor - bool needsStepping[numModules] = - {}; // Initialize to false; //modules that still require moving - unsigned long lastStepTimes[numModules] = - {}; // Initialize to false; //track when each module was last stepped - unsigned long lastSensorCheckTime = - currentTime; // track when we last read all the hall effect sensors - - for (int i = 0; i < numModules; i++) { - targetPositions[i] = constrain( - targetPositions[i], 0, - stepsPerRot - 1); // Constrain to avoid errors with incorrect inputs - resetLatches[i] = true; - lastStepTimes[i] = currentTime; - if (modules[i].getPosition() != targetPositions[i]) { - needsStepping[i] = true; - } else { - needsStepping[i] = false; - } - } +void SplitFlapDisplay::moveTo(int targetPositions[], float speed, bool releaseMotors) { + // TODO check length of array and return if empty + + speed = constrain(speed, 2, maxVel); + float stepsPerSecond = (speed / 60) * stepsPerRot; + float timePerStep = 1000000 / stepsPerSecond; - startMotors(); // not sure if this helps or not, likely that it does not based - // on testing - delay(startStopDelay); // give the motor time to align to magnetic field + unsigned long currentTime = micros(); - bool isFinished = checkAllFalse(needsStepping, numModules); - while (!isFinished) { + int checkIntervalUs = 20 * 1000; // How often to check each modules hall effect sensor, less + // than 20ms causes issues with bouncing + int startStopDelay = 200; // time to wait to let motor realign itself to + // magnetic field on stop and start + + bool resetLatches[numModules] = {}; // Initialize to false //start with latch on to prevent case where the + // motion starts with the magnet over the sensor + bool needsStepping[numModules] = {}; // Initialize to false; //modules that still require moving + unsigned long lastStepTimes[numModules] = {}; // Initialize to false; //track when each module was last stepped + unsigned long lastSensorCheckTime = currentTime; // track when we last read all the hall effect sensors - currentTime = micros(); for (int i = 0; i < numModules; i++) { - if (((currentTime - lastStepTimes[i]) > timePerStep) && - needsStepping[i]) { - modules[i].step(); - lastStepTimes[i] = micros(); - if (modules[i].getPosition() == - targetPositions[i]) { // this module is not in the correct position, - // requires stepping - needsStepping[i] = false; + targetPositions[i] = constrain( + targetPositions[i], + 0, + stepsPerRot - 1 + ); // Constrain to avoid errors with incorrect inputs + resetLatches[i] = true; + lastStepTimes[i] = currentTime; + if (modules[i].getPosition() != targetPositions[i]) { + needsStepping[i] = true; + } else { + needsStepping[i] = false; } - } } - if ((currentTime - lastSensorCheckTime) > - checkIntervalUs) { // check hall effect sensor every checkIntervalMs - // check every modules sensor - for (int i = 0; i < numModules; i++) { - if (needsStepping[i] && - (modules[i].readHallEffectSensor() == - true)) { // only check sensors where the module is still moving - if (!resetLatches[i]) { - // UNCOMMENTING THIS WILL PROBBALY MAKE THE MOTORS INACCURATE, DUE - // TO TIME TAKEN TO PRINT - // Serial.print("Module: "); - // Serial.print(i); - // Serial.print(" Magnet Position: "); - // Serial.print(modules[i].getMagnetPosition()); - // Serial.print(" Actual Position: "); - // Serial.print(modules[i].getPosition()); - // Serial.print(" Error: "); - // Serial.println((modules[i].getMagnetPosition() - - // modules[i].getPosition())); - modules[i].magnetDetected(); // update position to the modules - // magnet position - resetLatches[i] = true; - } - } else if (resetLatches[i] == true) { - resetLatches[i] = false; + startMotors(); // not sure if this helps or not, likely that it does not based + // on testing + delay(startStopDelay); // give the motor time to align to magnetic field + + bool isFinished = checkAllFalse(needsStepping, numModules); + while (! isFinished) { + currentTime = micros(); + for (int i = 0; i < numModules; i++) { + if (((currentTime - lastStepTimes[i]) > timePerStep) && needsStepping[i]) { + modules[i].step(); + lastStepTimes[i] = micros(); + if (modules[i].getPosition() == targetPositions[i]) { // this module is not in the correct position, + // requires stepping + needsStepping[i] = false; + } + } + } + + if ((currentTime - lastSensorCheckTime) > checkIntervalUs) { // check hall effect sensor every checkIntervalMs + // check every modules sensor + for (int i = 0; i < numModules; i++) { + if (needsStepping[i] && + (modules[i].readHallEffectSensor() == true + )) { // only check sensors where the module is still moving + if (! resetLatches[i]) { + // UNCOMMENTING THIS WILL PROBBALY MAKE THE MOTORS INACCURATE, DUE + // TO TIME TAKEN TO PRINT + // Serial.print("Module: "); + // Serial.print(i); + // Serial.print(" Magnet Position: "); + // Serial.print(modules[i].getMagnetPosition()); + // Serial.print(" Actual Position: "); + // Serial.print(modules[i].getPosition()); + // Serial.print(" Error: "); + // Serial.println((modules[i].getMagnetPosition() - + // modules[i].getPosition())); + modules[i].magnetDetected(); // update position to the modules + // magnet position + resetLatches[i] = true; + } + } else if (resetLatches[i] == true) { + resetLatches[i] = false; + } + } + isFinished = checkAllFalse(needsStepping, numModules); + lastSensorCheckTime = currentTime; // recall micros because for loop may + // take a moment to execute } - } - isFinished = checkAllFalse(needsStepping, numModules); - lastSensorCheckTime = currentTime; // recall micros because for loop may - // take a moment to execute } - } - if (releaseMotors) { - delay(startStopDelay); // allow all motors time to settle - stopMotors(); - } + if (releaseMotors) { + delay(startStopDelay); // allow all motors time to settle + stopMotors(); + } } bool SplitFlapDisplay::checkAllFalse(bool array[], int size) { - for (int i = 0; i < size; i++) { - if (array[i] == true) { - return false; // As soon as a true value is found, return false + for (int i = 0; i < size; i++) { + if (array[i] == true) { + return false; // As soon as a true value is found, return false + } } - } - return true; // All values were false + return true; // All values were false } void SplitFlapDisplay::startMotors() { // Probably broken somewhere, not sure - // why, haven't looked - for (int i = 0; i < numModules; i++) { - modules[i].start(); - } + // why, haven't looked + for (int i = 0; i < numModules; i++) { + modules[i].start(); + } } void SplitFlapDisplay::stopMotors() { - // Serial.println("Stopping Motors"); - for (int i = 0; i < numModules; i++) { - modules[i].stop(); - } + // Serial.println("Stopping Motors"); + for (int i = 0; i < numModules; i++) { + modules[i].stop(); + } } -void SplitFlapDisplay::setMqtt(SplitFlapMqtt* mqttHandler) { - mqtt = mqttHandler; -} \ No newline at end of file +void SplitFlapDisplay::setMqtt(SplitFlapMqtt *mqttHandler) { + mqtt = mqttHandler; +} diff --git a/src/SplitFlapDisplay.h b/src/SplitFlapDisplay.h index e56e308..f235ca4 100644 --- a/src/SplitFlapDisplay.h +++ b/src/SplitFlapDisplay.h @@ -1,9 +1,8 @@ -// SplitFlapDisplay.h (header file) -#ifndef SplitFlapDisplay_h -#define SplitFlapDisplay_h +#pragma once #include "JsonSettings.h" #include "SplitFlapModule.h" + #include #define MAX_MODULES 8 // for memory allocation, update if more modules @@ -12,58 +11,49 @@ class SplitFlapMqtt; class SplitFlapDisplay { -public: - SplitFlapDisplay(JsonSettings &settings); - - void init(); - void - writeString(String inputString, float speed = MAX_RPM, - bool centering = - true); // Move all modules at once to show a specific string - void writeChar(char inputChar, - float speed = MAX_RPM); // sets all modules to a single char - void moveTo(int targetPositions[], float speed = MAX_RPM, - bool releaseMotors = true); - void home(float speed = MAX_RPM); // move home - void - homeToString(String homeString, float speed = MAX_RPM, - bool centering = true); // moves home and then writes a string - void homeToChar( - char homeChar, - float speed = MAX_RPM); // moves home and then sets all modules to a char - void testAll(); - void testCount(); - void testRandom(float speed = MAX_RPM); - int getNumModules() { return numModules; } - void setMqtt(SplitFlapMqtt* mqttHandler); - -private: - JsonSettings &settings; - - bool checkAllFalse(bool array[], int size); - void stopMotors(); - void startMotors(); - - int numModules; - uint8_t moduleAddresses[MAX_MODULES]; - SplitFlapModule modules[MAX_MODULES]; - int moduleOffsets[MAX_MODULES]; - int displayOffset; - - float maxVel; // Max Velocity In RPM - int stepsPerRot; // number of motor steps per full rotation of character - // drum - int magnetPosition; // position of drum wheel when magnet is detected - int SDAPin; // SDA pin - int SCLPin; // SCL pin - - SplitFlapMqtt* mqtt = nullptr; + public: + SplitFlapDisplay(JsonSettings &settings); + + void init(); + void writeString( + String inputString, float speed = MAX_RPM, + bool centering = true + ); // Move all modules at once to show a specific string + void writeChar(char inputChar, + float speed = MAX_RPM); // sets all modules to a single char + void moveTo(int targetPositions[], float speed = MAX_RPM, bool releaseMotors = true); + void home(float speed = MAX_RPM); // move home + void homeToString( + String homeString, float speed = MAX_RPM, + bool centering = true + ); // moves home and then writes a string + void homeToChar(char homeChar, + float speed = MAX_RPM); // moves home and then sets all modules to a char + void testAll(); + void testCount(); + void testRandom(float speed = MAX_RPM); + int getNumModules() { return numModules; } + void setMqtt(SplitFlapMqtt *mqttHandler); + + private: + JsonSettings &settings; + + bool checkAllFalse(bool array[], int size); + void stopMotors(); + void startMotors(); + + int numModules; + uint8_t moduleAddresses[MAX_MODULES]; + SplitFlapModule modules[MAX_MODULES]; + int moduleOffsets[MAX_MODULES]; + int displayOffset; + + float maxVel; // Max Velocity In RPM + int stepsPerRot; // number of motor steps per full rotation of character + // drum + int magnetPosition; // position of drum wheel when magnet is detected + int SDAPin; // SDA pin + int SCLPin; // SCL pin + + SplitFlapMqtt *mqtt = nullptr; }; - -#endif -// __ -// (QUACK)>(o )___ -// ( ._> / -// `---' -// Morgan Manly -// 28/01/2025 diff --git a/src/SplitFlapDisplay.ino b/src/SplitFlapDisplay.ino index 63d2e92..f8c18f4 100644 --- a/src/SplitFlapDisplay.ino +++ b/src/SplitFlapDisplay.ino @@ -4,13 +4,15 @@ // Thom Koopman 03/30/2025 // Enjoy :) -#include "Arduino.h" +#include "JsonSettings.h" #include "SplitFlapDisplay.h" -#include "SplitFlapWebServer.h" #include "SplitFlapMqtt.h" -#include "JsonSettings.h" +#include "SplitFlapWebServer.h" + +#include #include +// clang-format off JsonSettings settings = JsonSettings("config", { // General Settings {"name", JsonSetting("My Display")}, @@ -26,9 +28,9 @@ JsonSettings settings = JsonSettings("config", { {"mqtt_pass", JsonSetting("")}, // Hardware Settings {"moduleCount", JsonSetting(8)}, - {"moduleAddresses", JsonSetting({0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27})}, + {"moduleAddresses", JsonSetting({0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27})}, {"magnetPosition", JsonSetting(730)}, - {"moduleOffsets", JsonSetting({0,0,0,0,0,0,0,0})}, + {"moduleOffsets", JsonSetting({0, 0, 0, 0, 0, 0, 0, 0})}, {"displayOffset", JsonSetting(0)}, {"sdaPin", JsonSetting(8)}, {"sclPin", JsonSetting(9)}, @@ -37,6 +39,7 @@ JsonSettings settings = JsonSettings("config", { // Operational States {"mode", JsonSetting(0)} }); +// clang-format on WiFiClient wifiClient; SplitFlapDisplay display(settings); @@ -44,172 +47,171 @@ SplitFlapWebServer webServer(settings); SplitFlapMqtt splitflapMqtt(settings, wifiClient); void setup() { - // put your setup code here, to run once: - Serial.begin(SERIAL_SPEED); + // put your setup code here, to run once: + Serial.begin(SERIAL_SPEED); - #ifdef STARTUP_DELAY +#ifdef STARTUP_DELAY delay(STARTUP_DELAY); - #endif +#endif - Serial.println("Init Web Server"); - webServer.init(); + Serial.println("Init Web Server"); + webServer.init(); - if (! webServer.connectToWifi()) { - webServer.startAccessPoint(); - webServer.startMDNS(); - webServer.startWebServer(); + if (! webServer.connectToWifi()) { + webServer.startAccessPoint(); + webServer.startMDNS(); + webServer.startWebServer(); - display.init(); - display.homeToString(""); + display.init(); + display.homeToString(""); - if (display.getNumModules() == 8){ - display.writeString("Wifi Err"); - } - else{ - display.writeChar('X'); + if (display.getNumModules() == 8) { + display.writeString("Wifi Err"); + } else { + display.writeChar('X'); + } + } else { + webServer.startMDNS(); + webServer.startWebServer(); + + display.init(); + splitflapMqtt.setup(); + splitflapMqtt.setDisplay(&display); + display.setMqtt(&splitflapMqtt); + display.homeToString(""); + + display.writeString("OK"); + delay(250); + display.writeString(""); } - } - else{ - webServer.startMDNS(); - webServer.startWebServer(); - - display.init(); - splitflapMqtt.setup(); - splitflapMqtt.setDisplay(&display); - display.setMqtt(&splitflapMqtt); - display.homeToString(""); - - display.writeString("OK"); - delay(250); - display.writeString(""); - } } void loop() { - splitflapMqtt.loop(); - - // check what mode the display is in, this value is updated by the web server - switch (webServer.getMode()) { - case 0: singleInputMode(); break; - case 1: multiInputMode(); break; - case 2: dateMode(); break; - case 3: timeMode(); break; - case 4: break; - case 5: randomTest(); break; - default: break; - } + splitflapMqtt.loop(); + + // check what mode the display is in, this value is updated by the web server + switch (webServer.getMode()) { + case 0: singleInputMode(); break; + case 1: multiInputMode(); break; + case 2: dateMode(); break; + case 3: timeMode(); break; + case 4: break; + case 5: randomTest(); break; + default: break; + } - checkConnection(); + checkConnection(); - reconnectIfNeeded(); + reconnectIfNeeded(); - yield(); + yield(); } -void singleInputMode(){ - String userInput = webServer.getInputString(); - if (userInput!=webServer.getWrittenString()){ - display.writeString(userInput,MAX_RPM,webServer.getCentering()); - webServer.setWrittenString(userInput); - } +void singleInputMode() { + String userInput = webServer.getInputString(); + if (userInput != webServer.getWrittenString()) { + display.writeString(userInput, MAX_RPM, webServer.getCentering()); + webServer.setWrittenString(userInput); + } } void multiInputMode() { - if (millis() - webServer.getLastSwitchMultiTime() > webServer.getMultiWordDelay()) { - //get user input, extract correct word from index using webserver counter, and display - String userInput = webServer.getMultiInputString(); - String currWord = extractFromCSV(userInput,webServer.getMultiWordCurrentIndex()); - if (currWord!=webServer.getWrittenString()){ - display.writeString(currWord,MAX_RPM,webServer.getCentering()); - webServer.setWrittenString(currWord); + if (millis() - webServer.getLastSwitchMultiTime() > webServer.getMultiWordDelay()) { + // get user input, extract correct word from index using webserver counter, and display + String userInput = webServer.getMultiInputString(); + String currWord = extractFromCSV(userInput, webServer.getMultiWordCurrentIndex()); + if (currWord != webServer.getWrittenString()) { + display.writeString(currWord, MAX_RPM, webServer.getCentering()); + webServer.setWrittenString(currWord); + } + webServer.setLastSwitchMultiTime(millis()); + webServer.setMultiWordCurrentIndex((webServer.getMultiWordCurrentIndex() + 1) % (webServer.getNumMultiWords())); } - webServer.setLastSwitchMultiTime(millis()); - webServer.setMultiWordCurrentIndex((webServer.getMultiWordCurrentIndex()+1)%(webServer.getNumMultiWords())); - } } void dateMode() { - if (millis() - webServer.getLastCheckDateTime() > webServer.getDateCheckInterval()) { - webServer.setLastCheckDateTime(millis()); - String currentDay = webServer.getCurrentDay(); - String dayPrefix = webServer.getDayPrefix(3); - - String outputString = " "; - switch (display.getNumModules()) { - case 2: outputString = currentDay; break; - case 3: outputString = dayPrefix; break; - case 4: outputString = " " + currentDay + " "; break; - case 5: outputString = dayPrefix + currentDay; break; - case 6: outputString = dayPrefix + " " + currentDay; break; - case 7: outputString = dayPrefix + " " + currentDay; break; - case 8: outputString = dayPrefix + currentDay + webServer.getMonthPrefix(3); break; - default: break; - } - if (outputString != webServer.getWrittenString()) { - display.writeString(outputString, MAX_RPM, webServer.getCentering()); - webServer.setWrittenString(outputString); + if (millis() - webServer.getLastCheckDateTime() > webServer.getDateCheckInterval()) { + webServer.setLastCheckDateTime(millis()); + String currentDay = webServer.getCurrentDay(); + String dayPrefix = webServer.getDayPrefix(3); + + String outputString = " "; + switch (display.getNumModules()) { + case 2: outputString = currentDay; break; + case 3: outputString = dayPrefix; break; + case 4: outputString = " " + currentDay + " "; break; + case 5: outputString = dayPrefix + currentDay; break; + case 6: outputString = dayPrefix + " " + currentDay; break; + case 7: outputString = dayPrefix + " " + currentDay; break; + case 8: outputString = dayPrefix + currentDay + webServer.getMonthPrefix(3); break; + default: break; + } + if (outputString != webServer.getWrittenString()) { + display.writeString(outputString, MAX_RPM, webServer.getCentering()); + webServer.setWrittenString(outputString); + } } - } } void timeMode() { - if (millis() - webServer.getLastCheckDateTime() > webServer.getDateCheckInterval()) { - webServer.setLastCheckDateTime(millis()); - String currentHour = webServer.getCurrentHour(); - String currentMinute = webServer.getCurrentMinute(); - String outputString = " "; - - switch (display.getNumModules()) { - case 2: outputString = currentMinute; break; - case 3: outputString = " " + currentMinute; break; - case 4: outputString = currentHour + "" + currentMinute; break; - case 5: outputString = currentHour + " " + currentMinute; break; - case 6: outputString = " " + currentHour + " " + currentMinute; break; - case 7: outputString = " " + currentHour + " " + currentMinute + " "; break; - case 8: outputString = " " + currentHour + currentMinute + " "; break; - default: break; - } + if (millis() - webServer.getLastCheckDateTime() > webServer.getDateCheckInterval()) { + webServer.setLastCheckDateTime(millis()); + String currentHour = webServer.getCurrentHour(); + String currentMinute = webServer.getCurrentMinute(); + String outputString = " "; + + switch (display.getNumModules()) { + case 2: outputString = currentMinute; break; + case 3: outputString = " " + currentMinute; break; + case 4: outputString = currentHour + "" + currentMinute; break; + case 5: outputString = currentHour + " " + currentMinute; break; + case 6: outputString = " " + currentHour + " " + currentMinute; break; + case 7: outputString = " " + currentHour + " " + currentMinute + " "; break; + case 8: outputString = " " + currentHour + currentMinute + " "; break; + default: break; + } - if (outputString != webServer.getWrittenString()) { - display.writeString(outputString, MAX_RPM, webServer.getCentering()); - webServer.setWrittenString(outputString); + if (outputString != webServer.getWrittenString()) { + display.writeString(outputString, MAX_RPM, webServer.getCentering()); + webServer.setWrittenString(outputString); + } } - } } -void randomTest(){ - display.testRandom(); - delay(2500); +void randomTest() { + display.testRandom(); + delay(2500); } void checkConnection() { - if (millis()-webServer.getLastCheckWifiTime() > webServer.getWifiCheckInterval()){ //check wifi to see if disconnected - webServer.checkWiFi(); - webServer.setLastCheckWifiTime(millis()); - } + if (millis() - webServer.getLastCheckWifiTime() > + webServer.getWifiCheckInterval()) { // check wifi to see if disconnected + webServer.checkWiFi(); + webServer.setLastCheckWifiTime(millis()); + } } -void reconnectIfNeeded(){ - if (webServer.getAttemptReconnect()) { //check if the device should attempt reconnection to wifi - webServer.setAttemptReconnect(false); - display.writeString(""); - if (! webServer.connectToWifi()) { - webServer.startAccessPoint(); - webServer.endMDNS(); - webServer.startMDNS(); - display.writeChar('X'); - } else { - webServer.endMDNS(); - webServer.startMDNS(); - display.writeString("OK"); - webServer.setWrittenString("OK"); - delay(500); - display.writeString(""); - webServer.setWrittenString(""); - } +void reconnectIfNeeded() { + if (webServer.getAttemptReconnect()) { // check if the device should attempt reconnection to wifi + webServer.setAttemptReconnect(false); + display.writeString(""); + if (! webServer.connectToWifi()) { + webServer.startAccessPoint(); + webServer.endMDNS(); + webServer.startMDNS(); + display.writeChar('X'); + } else { + webServer.endMDNS(); + webServer.startMDNS(); + display.writeString("OK"); + webServer.setWrittenString("OK"); + delay(500); + display.writeString(""); + webServer.setWrittenString(""); + } - splitflapMqtt.setup(); - } + splitflapMqtt.setup(); + } } String extractFromCSV(String str, int index) { @@ -218,21 +220,15 @@ String extractFromCSV(String str, int index) { int commaCount = 0; for (int i = 0; i < str.length(); i++) { - if (str[i] == ','){ - commaCount++; - if (commaCount == index){ - startIndex = i+1; //skip past the comma - } - else if (commaCount == index+1){ - endIndex = i; + if (str[i] == ',') { + commaCount++; + if (commaCount == index) { + startIndex = i + 1; // skip past the comma + } else if (commaCount == index + 1) { + endIndex = i; + } } - } } return str.substring(startIndex, endIndex); } - -// __ -// (QUACK)>(o )___ -// ( ._> / -// `---' diff --git a/src/SplitFlapModule.cpp b/src/SplitFlapModule.cpp index 72399c3..22b4473 100644 --- a/src/SplitFlapModule.cpp +++ b/src/SplitFlapModule.cpp @@ -1,16 +1,11 @@ -// SplitFlapModule_h.cpp (implementation -// file)c:\Users\morga\Documents\Arduino\libraries\PCF8575_library\PCF8575_library.h #include "SplitFlapModule.h" -#include // Required for tolower() // Array of characters, in order, the first item is located on the magnet on the // character drum -const char SplitFlapModule::chars[37] = { - ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', - 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', - 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; -const int SplitFlapModule::numChars = - sizeof(SplitFlapModule::chars) / sizeof(SplitFlapModule::chars[0]); +const char SplitFlapModule::chars[37] = {' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; +const int SplitFlapModule::numChars = sizeof(SplitFlapModule::chars) / sizeof(SplitFlapModule::chars[0]); int SplitFlapModule::charPositions[37]; // to be written in init function bool hasErrored = false; @@ -21,136 +16,129 @@ bool hasErrored = false; // pin for Hall effect sensor // Default Constructor -SplitFlapModule::SplitFlapModule() - : address(0), position(0), stepNumber(0), stepsPerRot(0) { // default values - magnetPosition = 710; // sort of guessing +SplitFlapModule::SplitFlapModule() : address(0), position(0), stepNumber(0), stepsPerRot(0) { // default values + magnetPosition = 710; // sort of guessing } // Constructor implementation -SplitFlapModule::SplitFlapModule(uint8_t I2Caddress, int stepsPerFullRotation, - int stepOffset, int magnetPos) - : address(I2Caddress), position(0), stepNumber(0), - stepsPerRot(stepsPerFullRotation) { - magnetPosition = magnetPos + stepOffset; +SplitFlapModule::SplitFlapModule(uint8_t I2Caddress, int stepsPerFullRotation, int stepOffset, int magnetPos) + : address(I2Caddress), position(0), stepNumber(0), stepsPerRot(stepsPerFullRotation) { + magnetPosition = magnetPos + stepOffset; } void SplitFlapModule::writeIO(uint16_t data) { - Wire.beginTransmission(address); - Wire.write(data & 0xFF); // Send lower byte - Wire.write((data >> 8) & 0xFF); // Send upper byte - - byte error = Wire.endTransmission(); - - if (error > 0 && !hasErrored) { - hasErrored = true; // Set the error flag - Serial.print("Error writing data to module "); - Serial.print(address); - Serial.print(", error code: "); - Serial.println(error); // Error codes: - // 0 = success - // 1 = data too long to fit in transmit buffer - // 2 = received NACK on transmit of address - // 3 = received NACK on transmit of data - // 4 = other error - } + Wire.beginTransmission(address); + Wire.write(data & 0xFF); // Send lower byte + Wire.write((data >> 8) & 0xFF); // Send upper byte + + byte error = Wire.endTransmission(); + + if (error > 0 && ! hasErrored) { + hasErrored = true; // Set the error flag + Serial.print("Error writing data to module "); + Serial.print(address); + Serial.print(", error code: "); + Serial.println(error); // Error codes: + // 0 = success + // 1 = data too long to fit in transmit buffer + // 2 = received NACK on transmit of address + // 3 = received NACK on transmit of data + // 4 = other error + } } // Init Module, Setup IO Board void SplitFlapModule::init() { - - uint16_t initState = - 0b1111111111100001; // Pin 15 (17) as INPUT, Pins 1-4 as OUTPUT - writeIO(initState); - - stop(); // Write all motor coil inputs LOW - - int initDelay = 100; - - delay(initDelay); - step(); - delay(initDelay); - step(); - delay(initDelay); - step(); - delay(initDelay); - step(); - delay(initDelay); - - stop(); - - // Generate Character Position Array - float stepSize = (float)stepsPerRot / (float)numChars; - float currentPosition = 0; - for (int i = 0; i < numChars; i++) { - charPositions[i] = (int)currentPosition; - currentPosition += stepSize; - } + uint16_t initState = 0b1111111111100001; // Pin 15 (17) as INPUT, Pins 1-4 as OUTPUT + writeIO(initState); + + stop(); // Write all motor coil inputs LOW + + int initDelay = 100; + + delay(initDelay); + step(); + delay(initDelay); + step(); + delay(initDelay); + step(); + delay(initDelay); + step(); + delay(initDelay); + + stop(); + + // Generate Character Position Array + float stepSize = (float) stepsPerRot / (float) numChars; + float currentPosition = 0; + for (int i = 0; i < numChars; i++) { + charPositions[i] = (int) currentPosition; + currentPosition += stepSize; + } } int SplitFlapModule::getCharPosition(char inputChar) { - inputChar = toupper(inputChar); - for (int i = 0; i < numChars; i++) { - if (chars[i] == inputChar) { - return charPositions[i]; + inputChar = toupper(inputChar); + for (int i = 0; i < numChars; i++) { + if (chars[i] == inputChar) { + return charPositions[i]; + } } - } - return 0; // Character not found, return blank + return 0; // Character not found, return blank } void SplitFlapModule::stop() { - - uint16_t stepState = 0b1111111111100001; - writeIO(stepState); + uint16_t stepState = 0b1111111111100001; + writeIO(stepState); } void SplitFlapModule::start() { - stepNumber = (stepNumber + 3) % 4; // effectively take one off stepNumber - step(false); // write the "previous" step high again, in case turned off + stepNumber = (stepNumber + 3) % 4; // effectively take one off stepNumber + step(false); // write the "previous" step high again, in case turned off } void SplitFlapModule::step(bool updatePosition) { - uint16_t stepState; - switch (stepNumber) { - case 0: - stepState = 0b1111111111100111; - writeIO(stepState); - break; - case 1: - stepState = 0b1111111111110011; - writeIO(stepState); - break; - case 2: - stepState = 0b1111111111111001; - writeIO(stepState); - break; - case 3: - stepState = 0b1111111111101101; - writeIO(stepState); - break; - } - if (updatePosition) { - position = (position + 1) % stepsPerRot; - stepNumber = (stepNumber + 1) % 4; - } + uint16_t stepState; + switch (stepNumber) { + case 0: + stepState = 0b1111111111100111; + writeIO(stepState); + break; + case 1: + stepState = 0b1111111111110011; + writeIO(stepState); + break; + case 2: + stepState = 0b1111111111111001; + writeIO(stepState); + break; + case 3: + stepState = 0b1111111111101101; + writeIO(stepState); + break; + } + if (updatePosition) { + position = (position + 1) % stepsPerRot; + stepNumber = (stepNumber + 1) % 4; + } } bool SplitFlapModule::readHallEffectSensor() { + if (hasErrored) { + return false; + } - if (hasErrored) { - return false; - } - - uint8_t requestBytes = 2; - Wire.requestFrom(address, requestBytes); - // Make sure the data is available - if (Wire.available() == 2) { - uint16_t inputState = 0; + uint8_t requestBytes = 2; + Wire.requestFrom(address, requestBytes); + // Make sure the data is available + if (Wire.available() == 2) { + uint16_t inputState = 0; - // Read the two bytes and combine them into a 16-bit value - inputState = Wire.read(); // Read the lower byte - inputState |= (Wire.read() << 8); // Read the upper byte and shift it left + // Read the two bytes and combine them into a 16-bit value + inputState = Wire.read(); // Read the lower byte + inputState |= (Wire.read() << 8); // Read the upper byte and shift it left - return (inputState & (1 << 15)) != 0; // If bit is 15, return HIGH, else LOW - } - return false; + return (inputState & (1 << 15)) != 0; // If bit is 15, return HIGH, else LOW + } + return false; } diff --git a/src/SplitFlapModule.h b/src/SplitFlapModule.h index ba57a79..945aa68 100644 --- a/src/SplitFlapModule.h +++ b/src/SplitFlapModule.h @@ -1,66 +1,52 @@ -// SplitFlapModule.h (header file) +#pragma once -#ifndef SplitFlapModule_h -#define SplitFlapModule_h -#include "Arduino.h" -#include //I2C Communication Between Modules +#include +#include class SplitFlapModule { -public: - // Constructor declarationS - SplitFlapModule(); // default constructor required to allocate memory for - // SplitFlapDisplay class - SplitFlapModule(uint8_t I2Caddress, int stepsPerFullRotation, int stepOffset, - int magnetPos); + public: + // Constructor declarationS + SplitFlapModule(); // default constructor required to allocate memory for + // SplitFlapDisplay class + SplitFlapModule(uint8_t I2Caddress, int stepsPerFullRotation, int stepOffset, int magnetPos); - void init(); + void init(); - void step(bool updatePosition = true); // step motor - void stop(); // write all motor input pins to low - void start(); // re-energize coils to last position, not stepping motor + void step(bool updatePosition = true); // step motor + void stop(); // write all motor input pins to low + void start(); // re-energize coils to last position, not stepping motor - int getMagnetPosition() const { - return magnetPosition; - } // position where magnet is detected - int getCharPosition( - char inputChar); // get integer position given single character - int getPosition() const { return position; } // get integer position + int getMagnetPosition() const { return magnetPosition; } // position where magnet is detected + int getCharPosition(char inputChar); // get integer position given single character + int getPosition() const { return position; } // get integer position - bool readHallEffectSensor(); // return the value read by the hall effect - // sensor - void magnetDetected() { - position = magnetPosition; - } // update position to magnetposition, called when magnet is detected + bool readHallEffectSensor(); // return the value read by the hall effect + // sensor + void magnetDetected() { + position = magnetPosition; + } // update position to magnetposition, called when magnet is detected - bool getHasErrored() const { return hasErrored; } + bool getHasErrored() const { return hasErrored; } -private: - uint8_t address; // i2c address of module - int position; // character drum position - int stepNumber; // current position in the stepping order, to make motor move - int stepsPerRot; // number of steps per rotation - bool hasErrored = false; // flag to indicate if an error has occurred + private: + uint8_t address; // i2c address of module + int position; // character drum position + int stepNumber; // current position in the stepping order, to make motor move + int stepsPerRot; // number of steps per rotation + bool hasErrored = false; // flag to indicate if an error has occurred - void writeIO(uint16_t data); // write to motor in pins + void writeIO(uint16_t data); // write to motor in pins - int magnetPosition; // altered by offsets - static const int motorPins[]; // Array of motor pins - static const int HallEffectPIN; // Hall Effect Sensor Pin (On PCF8575) + int magnetPosition; // altered by offsets + static const int motorPins[]; // Array of motor pins + static const int HallEffectPIN; // Hall Effect Sensor Pin (On PCF8575) - static const char chars[37]; // all characters in order - static int charPositions[37]; // will be generated based on the characters and - // the magnetPosition variable - static const int numChars; // number of characters in module + static const char chars[37]; // all characters in order + static int charPositions[37]; // will be generated based on the characters and + // the magnetPosition variable + static const int numChars; // number of characters in module }; -#endif -// __ -// (QUACK)>(o )___ -// ( ._> / -// `---' -// Morgan Manly -// 28/01/2025 - // //PINs on the PCF8575 Board // #define P00 0 // #define P01 1 diff --git a/src/SplitFlapMqtt.cpp b/src/SplitFlapMqtt.cpp index bd32269..8b04d19 100644 --- a/src/SplitFlapMqtt.cpp +++ b/src/SplitFlapMqtt.cpp @@ -1,28 +1,28 @@ #include "SplitFlapMqtt.h" -SplitFlapMqtt::SplitFlapMqtt(JsonSettings& settings, WiFiClient& wifiClient) - : settings(settings), wifiClient(wifiClient), mqttClient(wifiClient), display(nullptr) {} +SplitFlapMqtt::SplitFlapMqtt(JsonSettings &settings, WiFiClient &wifiClient) + : settings(settings), wifiClient(wifiClient), mqttClient(wifiClient), display(nullptr) {} void SplitFlapMqtt::setup() { mqttServer = settings.getString("mqtt_server"); - mqttPort = settings.getInt("mqtt_port"); - mqttUser = settings.getString("mqtt_user"); - mqttPass = settings.getString("mqtt_pass"); + mqttPort = settings.getInt("mqtt_port"); + mqttUser = settings.getString("mqtt_user"); + mqttPass = settings.getString("mqtt_pass"); String mdns = settings.getString("mdns"); String name = settings.getString("name"); topic_command = "splitflap/" + mdns + "/set"; - topic_state = "splitflap/" + mdns + "/state"; - topic_avail = "splitflap/" + mdns + "/availability"; - topic_config_text = "homeassistant/text/splitflap_text_" + mdns + "/config"; + topic_state = "splitflap/" + mdns + "/state"; + topic_avail = "splitflap/" + mdns + "/availability"; + topic_config_text = "homeassistant/text/splitflap_text_" + mdns + "/config"; topic_config_sensor = "homeassistant/sensor/splitflap_sensor_" + mdns + "/config"; mqttClient.setServer(mqttServer.c_str(), mqttPort); - mqttClient.setCallback([this](char* topic, byte* payload, unsigned int length) { + mqttClient.setCallback([this](char *topic, byte *payload, unsigned int length) { String message; for (unsigned int i = 0; i < length; i++) { - message += (char)payload[i]; + message += (char) payload[i]; } Serial.printf("[MQTT] Message received: %s\n", message.c_str()); if (display) { @@ -35,7 +35,7 @@ void SplitFlapMqtt::setup() { } void SplitFlapMqtt::connectToMqtt() { - if (!mqttClient.connected()) { + if (! mqttClient.connected()) { Serial.println("[MQTT] Attempting to connect..."); String clientId = "SplitFlap-" + settings.getString("mdns"); if (mqttUser.length() > 0) { @@ -49,6 +49,7 @@ void SplitFlapMqtt::connectToMqtt() { String mdns = settings.getString("mdns"); + // clang-format off String payload_text = "{" "\"name\":\"" + settings.getString("name") + "\"," "\"unique_id\":\"splitflap_text_" + mdns + "\"," @@ -78,6 +79,7 @@ void SplitFlapMqtt::connectToMqtt() { "\"sw_version\":\"1.0.0\"" "}" "}"; + // clang-format on mqttClient.subscribe(topic_command.c_str()); mqttClient.publish(topic_avail.c_str(), "online", true); @@ -90,11 +92,11 @@ void SplitFlapMqtt::connectToMqtt() { } } -void SplitFlapMqtt::setDisplay(SplitFlapDisplay* d) { +void SplitFlapMqtt::setDisplay(SplitFlapDisplay *d) { display = d; } -void SplitFlapMqtt::publishState(const String& message) { +void SplitFlapMqtt::publishState(const String &message) { Serial.println("[MQTT] Publishing state: " + message); mqttClient.publish(topic_state.c_str(), message.c_str(), true); } @@ -104,5 +106,5 @@ void SplitFlapMqtt::loop() { } bool SplitFlapMqtt::isConnected() { - return mqttClient.connected(); + return mqttClient.connected(); } diff --git a/src/SplitFlapMqtt.h b/src/SplitFlapMqtt.h index 4cc2529..fbc4d1e 100644 --- a/src/SplitFlapMqtt.h +++ b/src/SplitFlapMqtt.h @@ -2,25 +2,26 @@ #include "JsonSettings.h" #include "SplitFlapDisplay.h" + #include #include class SplitFlapMqtt { -public: - SplitFlapMqtt(JsonSettings& settings, WiFiClient& client); // updated constructor + public: + SplitFlapMqtt(JsonSettings &settings, WiFiClient &client); // updated constructor void setup(); - void loop(); // needed for PubSubClient3 - void publishState(const String& message); - void setDisplay(SplitFlapDisplay* display); + void loop(); // needed for PubSubClient3 + void publishState(const String &message); + void setDisplay(SplitFlapDisplay *display); bool isConnected(); -private: - PubSubClient mqttClient; // PubSubClient instead of AsyncMqttClient - WiFiClient& wifiClient; // store reference to WiFiClient + private: + PubSubClient mqttClient; // PubSubClient instead of AsyncMqttClient + WiFiClient &wifiClient; // store reference to WiFiClient - JsonSettings& settings; - SplitFlapDisplay* display; + JsonSettings &settings; + SplitFlapDisplay *display; void connectToMqtt(); @@ -37,4 +38,4 @@ class SplitFlapMqtt { unsigned long lastAttempt = 0; int retryCount = 0; -}; \ No newline at end of file +}; diff --git a/src/SplitFlapWebServer.cpp b/src/SplitFlapWebServer.cpp index eed343c..160267e 100644 --- a/src/SplitFlapWebServer.cpp +++ b/src/SplitFlapWebServer.cpp @@ -1,6 +1,7 @@ #include "SplitFlapWebServer.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" + +#include +#include #define AP_SSID "Split Flap Display" @@ -12,292 +13,284 @@ #define WIFI_PASS "" #endif -// Constructor SplitFlapWebServer::SplitFlapWebServer(JsonSettings &settings) - : settings(settings), server(80), multiWordDelay(1000), - attemptReconnect(false), multiWordCurrentIndex(0), numMultiWords(0), - wifiCheckInterval(1000), connectionMode(0), checkDateInterval(250), - centering(1) { - lastSwitchMultiTime = millis(); + : settings(settings), server(80), multiWordDelay(1000), attemptReconnect(false), multiWordCurrentIndex(0), + numMultiWords(0), wifiCheckInterval(1000), connectionMode(0), checkDateInterval(250), centering(1) { + lastSwitchMultiTime = millis(); } void SplitFlapWebServer::init() { - if (!LittleFS.begin()) { - Serial.println("An Error has occurred while mounting LittleFS"); - return; - } + if (! LittleFS.begin()) { + Serial.println("An Error has occurred while mounting LittleFS"); + return; + } - setTimezone(); + setTimezone(); } void SplitFlapWebServer::setTimezone() { - const char *sntpServer = "pool.ntp.org"; - const char *defaultTz = "UTC0"; - String timezoneSetting = settings.getString("timezone"); - String posixTimezone = defaultTz; - - File file = LittleFS.open("/timezones.json", "r"); - if (!file) { - Serial.println("Failed to open timezones.json; defaulting to UTC"); - configTzTime(defaultTz, sntpServer); - return; - } - - size_t size = file.size(); - std::unique_ptr buffer(new char[size]); - file.readBytes(buffer.get(), size); - file.close(); - - JsonDocument timezones; - DeserializationError error = deserializeJson(timezones, buffer.get()); - - if (error) { - Serial.println("Failed to parse timezones.json: " + String(error.c_str())); - configTzTime(defaultTz, sntpServer); - return; - } - - for (JsonPair kv : timezones.as()) { - String keyStr = kv.key().c_str(); - String valueStr = kv.value().as(); - - if (keyStr == timezoneSetting) { - posixTimezone = valueStr; - break; + const char *sntpServer = "pool.ntp.org"; + const char *defaultTz = "UTC0"; + String timezoneSetting = settings.getString("timezone"); + String posixTimezone = defaultTz; + + File file = LittleFS.open("/timezones.json", "r"); + if (! file) { + Serial.println("Failed to open timezones.json; defaulting to UTC"); + configTzTime(defaultTz, sntpServer); + return; + } + + size_t size = file.size(); + std::unique_ptr buffer(new char[size]); + file.readBytes(buffer.get(), size); + file.close(); + + JsonDocument timezones; + DeserializationError error = deserializeJson(timezones, buffer.get()); + + if (error) { + Serial.println("Failed to parse timezones.json: " + String(error.c_str())); + configTzTime(defaultTz, sntpServer); + return; } - } - Serial.println("POSIX Timezone set to: " + posixTimezone); - configTzTime(posixTimezone.c_str(), sntpServer); + for (JsonPair kv : timezones.as()) { + String keyStr = kv.key().c_str(); + String valueStr = kv.value().as(); + + if (keyStr == timezoneSetting) { + posixTimezone = valueStr; + break; + } + } + + Serial.println("POSIX Timezone set to: " + posixTimezone); + configTzTime(posixTimezone.c_str(), sntpServer); } // Totally didn't use AI to make these functions // Function to get current minute as a string String SplitFlapWebServer::getCurrentMinute() { - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return ""; - } - char minuteStr[3]; // Max "59" + null terminator - sprintf(minuteStr, "%02d", timeinfo.tm_min); // Format as two-digit string - return String(minuteStr); + struct tm timeinfo; + if (! getLocalTime(&timeinfo)) { + return ""; + } + char minuteStr[3]; // Max "59" + null terminator + sprintf(minuteStr, "%02d", timeinfo.tm_min); // Format as two-digit string + return String(minuteStr); } // Function to get current hour as a string String SplitFlapWebServer::getCurrentHour() { - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return ""; - } - char hourStr[3]; // Max "59" + null terminator - sprintf(hourStr, "%02d", timeinfo.tm_hour); // Format as two-digit string - return String(hourStr); + struct tm timeinfo; + if (! getLocalTime(&timeinfo)) { + return ""; + } + char hourStr[3]; // Max "59" + null terminator + sprintf(hourStr, "%02d", timeinfo.tm_hour); // Format as two-digit string + return String(hourStr); } // Function to get the first n characters of the day String SplitFlapWebServer::getDayPrefix(int n) { - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Err"; // Return error if time not available - } + struct tm timeinfo; + if (! getLocalTime(&timeinfo)) { + return "Err"; // Return error if time not available + } - // Get full weekday name - char fullDay[10]; // Buffer for full day name - strftime(fullDay, sizeof(fullDay), "%A", &timeinfo); + // Get full weekday name + char fullDay[10]; // Buffer for full day name + strftime(fullDay, sizeof(fullDay), "%A", &timeinfo); - // Extract first n characters - char dayPrefix[n + 1]; - strncpy(dayPrefix, fullDay, n); - dayPrefix[n] = '\0'; // Null-terminate the string + // Extract first n characters + char dayPrefix[n + 1]; + strncpy(dayPrefix, fullDay, n); + dayPrefix[n] = '\0'; // Null-terminate the string - return String(dayPrefix); + return String(dayPrefix); } // Function to get the first n characters of the month String SplitFlapWebServer::getMonthPrefix(int n) { - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Err"; // Return error if time not available - } + struct tm timeinfo; + if (! getLocalTime(&timeinfo)) { + return "Err"; // Return error if time not available + } - // Get full month name - char fullMonth[10]; // Buffer for full month name - strftime(fullMonth, sizeof(fullMonth), "%B", &timeinfo); + // Get full month name + char fullMonth[10]; // Buffer for full month name + strftime(fullMonth, sizeof(fullMonth), "%B", &timeinfo); - // Extract first n characters - char monthPrefix[n + 1]; - strncpy(monthPrefix, fullMonth, n); - monthPrefix[n] = '\0'; // Null-terminate the string + // Extract first n characters + char monthPrefix[n + 1]; + strncpy(monthPrefix, fullMonth, n); + monthPrefix[n] = '\0'; // Null-terminate the string - return String(monthPrefix); + return String(monthPrefix); } String SplitFlapWebServer::getCurrentDay() { - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Err"; // Return error if time is not available - } + struct tm timeinfo; + if (! getLocalTime(&timeinfo)) { + return "Err"; // Return error if time is not available + } - char dayStr[3]; // Buffer for the day number (max "31" + null terminator) - sprintf(dayStr, "%02d", timeinfo.tm_mday); // Format as two-digit string + char dayStr[3]; // Buffer for the day number (max "31" + null terminator) + sprintf(dayStr, "%02d", timeinfo.tm_mday); // Format as two-digit string - return String(dayStr); + return String(dayStr); } void SplitFlapWebServer::setMode(int targetMode) { - settings.putInt("mode", targetMode); + settings.putInt("mode", targetMode); } -int SplitFlapWebServer::getMode() { return settings.getInt("mode"); } +int SplitFlapWebServer::getMode() { + return settings.getInt("mode"); +} void SplitFlapWebServer::checkWiFi() { - if (connectionMode == 1) { - if (WiFi.status() != WL_CONNECTED) { - Serial.println("Wi-Fi lost! Forcing reconnect..."); - WiFi.disconnect(); - WiFi.reconnect(); + if (connectionMode == 1) { + if (WiFi.status() != WL_CONNECTED) { + Serial.println("Wi-Fi lost! Forcing reconnect..."); + WiFi.disconnect(); + WiFi.reconnect(); + } } - } } bool SplitFlapWebServer::loadWiFiCredentials() { - // Allow WIFI_SSID and WIFI_PASS to be overridden by compile-time definitions - String ssid = String(WIFI_SSID).isEmpty() ? settings.getString("ssid") - : String(WIFI_SSID); - String password = String(WIFI_PASS).isEmpty() ? settings.getString("password") - : String(WIFI_PASS); - - if (ssid != "" && password != "") { - Serial.println("Wi-Fi credentials loaded successfully."); - Serial.print("Connecting to Network: "); - Serial.println(ssid); - WiFi.mode(WIFI_STA); + // Allow WIFI_SSID and WIFI_PASS to be overridden by compile-time definitions + String ssid = String(WIFI_SSID).isEmpty() ? settings.getString("ssid") : String(WIFI_SSID); + String password = String(WIFI_PASS).isEmpty() ? settings.getString("password") : String(WIFI_PASS); + + if (ssid != "" && password != "") { + Serial.println("Wi-Fi credentials loaded successfully."); + Serial.print("Connecting to Network: "); + Serial.println(ssid); + WiFi.mode(WIFI_STA); #ifdef WIFI_TX_POWER - delay(100); - WiFi.setTxPower((wifi_power_t)WIFI_TX_POWER); + delay(100); + WiFi.setTxPower((wifi_power_t) WIFI_TX_POWER); #endif - WiFi.begin(ssid.c_str(), password.c_str()); - return true; // Return true if credentials exist - } - return false; // Return false if no credentials were found + WiFi.begin(ssid.c_str(), password.c_str()); + return true; // Return true if credentials exist + } + return false; // Return false if no credentials were found } bool SplitFlapWebServer::connectToWifi() { - if (loadWiFiCredentials()) { - - unsigned long startAttemptTime = millis(); - const unsigned long timeout = 20000; // 20 seconds - unsigned long lastPrintTime = startAttemptTime; - - while (WiFi.status() != WL_CONNECTED) { - if (millis() - startAttemptTime >= timeout) { - Serial.println("_"); - Serial.println("Wi-Fi connection failed! Timeout reached."); - return false; // Return false if unable to connect in 30 seconds - } - if ((millis() - lastPrintTime) > 1000) { - Serial.print("."); - lastPrintTime = millis(); - } - yield(); - } + if (loadWiFiCredentials()) { + unsigned long startAttemptTime = millis(); + const unsigned long timeout = 20000; // 20 seconds + unsigned long lastPrintTime = startAttemptTime; + + while (WiFi.status() != WL_CONNECTED) { + if (millis() - startAttemptTime >= timeout) { + Serial.println("_"); + Serial.println("Wi-Fi connection failed! Timeout reached."); + return false; // Return false if unable to connect in 30 seconds + } + if ((millis() - lastPrintTime) > 1000) { + Serial.print("."); + lastPrintTime = millis(); + } + yield(); + } - // connected succesfully - connectionMode = 1; - WiFi.softAPdisconnect(); // Turns off SoftAP mode only after connected to - // actual network - WiFi.setAutoReconnect(true); - WiFi.persistent(true); // Saves Wi-Fi settings to flash memory - WiFi.setSleep(false); - Serial.println("Connected to Wi-Fi!"); - Serial.println("IP Address: http://" + WiFi.localIP().toString()); - return true; - } - return false; + // connected succesfully + connectionMode = 1; + WiFi.softAPdisconnect(); // Turns off SoftAP mode only after connected to + // actual network + WiFi.setAutoReconnect(true); + WiFi.persistent(true); // Saves Wi-Fi settings to flash memory + WiFi.setSleep(false); + Serial.println("Connected to Wi-Fi!"); + Serial.println("IP Address: http://" + WiFi.localIP().toString()); + return true; + } + return false; } void SplitFlapWebServer::startAccessPoint() { - connectionMode = 0; - const char *apSSID = AP_SSID; - WiFi.softAP(apSSID); + connectionMode = 0; + const char *apSSID = AP_SSID; + WiFi.softAP(apSSID); #ifdef WIFI_TX_POWER - delay(100); - WiFi.setTxPower((wifi_power_t)WIFI_TX_POWER); + delay(100); + WiFi.setTxPower((wifi_power_t) WIFI_TX_POWER); #endif - Serial.println("AP Mode Started!"); - Serial.println("Connect to: " + String(apSSID)); - Serial.println("AP IP Address: http://" + WiFi.softAPIP().toString()); + Serial.println("AP Mode Started!"); + Serial.println("Connect to: " + String(apSSID)); + Serial.println("AP IP Address: http://" + WiFi.softAPIP().toString()); } void fourOhFour(AsyncWebServerRequest *request) { - Serial.println("Request: " + request->url()); - Serial.println("Method: " + String(request->methodToString())); - request->send(404); + Serial.println("Request: " + request->url()); + Serial.println("Method: " + String(request->methodToString())); + request->send(404); } void SplitFlapWebServer::endMDNS() { - MDNS.end(); - Serial.println("mDNS responder stopped"); + MDNS.end(); + Serial.println("mDNS responder stopped"); } void SplitFlapWebServer::startMDNS() { - if (!MDNS.begin(settings.getString("mdns").c_str())) { - Serial.println("Error setting up MDNS responder!"); - while (1) { - delay(1000); + if (! MDNS.begin(settings.getString("mdns").c_str())) { + Serial.println("Error setting up MDNS responder!"); + while (1) { + delay(1000); + } } - } - Serial.println("mDNS: http://" + settings.getString("mdns") + ".local"); + Serial.println("mDNS: http://" + settings.getString("mdns") + ".local"); } void SplitFlapWebServer::startWebServer() { - server.on("/", HTTP_GET, [this](AsyncWebServerRequest *request) { - request->redirect("/index.html"); - }); - - File root = LittleFS.open("/"); - if (!root || !root.isDirectory()) { - Serial.println("Failed to open directory or not a directory"); - return; - } - - File file = root.openNextFile(); - while (file) { - if (String(file.name()).endsWith(".gz")) { - const char *filename = file.name(); - String tempFilename = (String("/") + String(filename)); - tempFilename.replace(".gz", ""); - filename = tempFilename.c_str(); - - server.serveStatic(filename, LittleFS, filename, "max-age=600"); + server.on("/", HTTP_GET, [this](AsyncWebServerRequest *request) { request->redirect("/index.html"); }); + + File root = LittleFS.open("/"); + if (! root || ! root.isDirectory()) { + Serial.println("Failed to open directory or not a directory"); + return; } - file = root.openNextFile(); - } - server.on("/settings", HTTP_GET, [this](AsyncWebServerRequest *request) { - request->send(200, "application/json", settings.toJson().as()); - }); + File file = root.openNextFile(); + while (file) { + if (String(file.name()).endsWith(".gz")) { + const char *filename = file.name(); + String tempFilename = (String("/") + String(filename)); + tempFilename.replace(".gz", ""); + filename = tempFilename.c_str(); - server.on( - "/settings/reset", HTTP_POST, [this](AsyncWebServerRequest *request) { + server.serveStatic(filename, LittleFS, filename, "max-age=600"); + } + file = root.openNextFile(); + } + + server.on("/settings", HTTP_GET, [this](AsyncWebServerRequest *request) { + request->send(200, "application/json", settings.toJson().as()); + }); + + server.on("/settings/reset", HTTP_POST, [this](AsyncWebServerRequest *request) { settings.reset(); JsonDocument response; - response["message"] = "Settings reset successfully! Reconnect to the " + - String(AP_SSID) + " network"; + response["message"] = "Settings reset successfully! Reconnect to the " + String(AP_SSID) + " network"; response["persistent"] = true; request->send(200, "application/json", response.as()); this->attemptReconnect = true; - }); + }); - server.addHandler(new AsyncCallbackJsonWebHandler( - "/settings", [this](AsyncWebServerRequest *request, JsonVariant &json) { + server.addHandler(new AsyncCallbackJsonWebHandler( + "/settings", + [this](AsyncWebServerRequest *request, JsonVariant &json) { if (request->method() != HTTP_POST) { - return request->send(405, "application/json", - "{\"error\":\"Method Not Allowed\"}"); + return request->send(405, "application/json", "{\"error\":\"Method Not Allowed\"}"); } Serial.println("Received settings update request"); @@ -308,45 +301,36 @@ void SplitFlapWebServer::startWebServer() { response["message"] = "Settings saved successfully!"; // TODO Refactor this it's gross - if ((json["ssid"].is() && - json["ssid"].as() != settings.getString("ssid")) || - (json["password"].is() && - json["password"].as() != settings.getString("password"))) { - reconnect = true; - response["message"] = "Settings updated successfully, Network " - "settings have changed, reconnect to the " + - json["ssid"].as() + " network"; + if ((json["ssid"].is() && json["ssid"].as() != settings.getString("ssid")) || + (json["password"].is() && json["password"].as() != settings.getString("password"))) { + reconnect = true; + response["message"] = "Settings updated successfully, Network " "settings have changed, reconnect to the " + + json["ssid"].as() + " network"; } - if (json["mdns"].is() && - json["mdns"].as() != settings.getString("mdns")) { - reconnect = true; - response["message"] = - "Settings updated successfully, mDNS name has changed, " - "automatically redirecting to http://" + - json["mdns"].as() + ".local..."; - response["redirect"] = - "http://" + json["mdns"].as() + ".local/settings.html"; + if (json["mdns"].is() && json["mdns"].as() != settings.getString("mdns")) { + reconnect = true; + response["message"] = + "Settings updated successfully, mDNS name has changed, " "automatically redirecting to http://" + + json["mdns"].as() + ".local..."; + response["redirect"] = "http://" + json["mdns"].as() + ".local/settings.html"; } - if ((json["mqtt_server"].is() && - json["mqtt_server"].as() != settings.getString("mqtt_server")) || - (json["mqtt_port"].is() && - json["mqtt_port"].as() != settings.getInt("mqtt_port")) || - (json["mqtt_user"].is() && - json["mqtt_user"].as() != settings.getString("mqtt_user")) || - (json["mqtt_pass"].is() && - json["mqtt_pass"].as() != settings.getString("mqtt_pass"))) { - response["message"] = "Mqtt settings have changed, reconnecting..."; - reconnect = true; + if ((json["mqtt_server"].is() && json["mqtt_server"].as() != settings.getString("mqtt_server") + ) || + (json["mqtt_port"].is() && json["mqtt_port"].as() != settings.getInt("mqtt_port")) || + (json["mqtt_user"].is() && json["mqtt_user"].as() != settings.getString("mqtt_user")) || + (json["mqtt_pass"].is() && json["mqtt_pass"].as() != settings.getString("mqtt_pass"))) { + response["message"] = "Mqtt settings have changed, reconnecting..."; + reconnect = true; } - if (!settings.fromJson(json)) { - response["message"] = "Failed to save settings"; - response["type"] = "error"; - response["errors"]["key"] = settings.getLastValidationKey(); - response["errors"]["message"] = settings.getLastValidationError(); - return request->send(400, "application/json", response.as()); + if (! settings.fromJson(json)) { + response["message"] = "Failed to save settings"; + response["type"] = "error"; + response["errors"]["key"] = settings.getLastValidationKey(); + response["errors"]["message"] = settings.getLastValidationError(); + return request->send(400, "application/json", response.as()); } response["type"] = "success"; @@ -355,13 +339,13 @@ void SplitFlapWebServer::startWebServer() { request->send(200, "application/json", response.as()); this->attemptReconnect = reconnect; - })); + } + )); - server.addHandler(new AsyncCallbackJsonWebHandler( - "/text", [this](AsyncWebServerRequest *request, JsonVariant &json) { + server + .addHandler(new AsyncCallbackJsonWebHandler("/text", [this](AsyncWebServerRequest *request, JsonVariant &json) { if (request->method() != HTTP_POST) { - return request->send(405, "application/json", - "{\"error\":\"Method Not Allowed\"}"); + return request->send(405, "application/json", "{\"error\":\"Method Not Allowed\"}"); } Serial.println("Received text update request"); @@ -371,26 +355,26 @@ void SplitFlapWebServer::startWebServer() { // {"mode":"multiple","words":["asdf","asdfasdf","fffff"],"delay":"14","center":true} JsonDocument response; - if (!json["mode"].is()) { - response["message"] = "Invalid mode type"; + if (! json["mode"].is()) { + response["message"] = "Invalid mode type"; } - if (!json["words"].is()) { - response["message"] = "Invalid words array"; + if (! json["words"].is()) { + response["message"] = "Invalid words array"; } float delay = json["delay"].as(); if (delay < 1) { - response["message"] = "Invalid delay type / value"; + response["message"] = "Invalid delay type / value"; } - if (!json["center"].is()) { - response["message"] = "Invalid center type"; + if (! json["center"].is()) { + response["message"] = "Invalid center type"; } if (response["message"].is()) { - response["type"] = "error"; - return request->send(400, "application/json", response.as()); + response["type"] = "error"; + return request->send(400, "application/json", response.as()); } this->setMultiDelay(delay * 1000); @@ -400,81 +384,81 @@ void SplitFlapWebServer::startWebServer() { Serial.println("centering: " + String(centering ? "true" : "false")); if (json["mode"] == "single") { - String word = decodeURIComponent(json["words"][0].as()); - Serial.println("Single Word: " + word); - this->setInputString(word); - this->setMode(0); // change mode last once all variables updated + String word = decodeURIComponent(json["words"][0].as()); + Serial.println("Single Word: " + word); + this->setInputString(word); + this->setMode(0); // change mode last once all variables updated } if (json["mode"] == "multiple") { - JsonArray wordsArray = json["words"].as(); - String words = ""; - for (JsonVariant v : wordsArray) { - words += decodeURIComponent(v.as()) + ","; - } - if (words.length() > 0) { - words.remove(words.length() - 1); - } - - this->setMultiInputString(words); - this->numMultiWords = wordsArray.size(); - Serial.println("Multiple Words: " + words); - Serial.println("Number of Words: " + String(this->numMultiWords)); - - this->setMode(1); + JsonArray wordsArray = json["words"].as(); + String words = ""; + for (JsonVariant v : wordsArray) { + words += decodeURIComponent(v.as()) + ","; + } + if (words.length() > 0) { + words.remove(words.length() - 1); + } + + this->setMultiInputString(words); + this->numMultiWords = wordsArray.size(); + Serial.println("Multiple Words: " + words); + Serial.println("Number of Words: " + String(this->numMultiWords)); + + this->setMode(1); } response["message"] = "Text updated successfully!"; response["type"] = "success"; request->send(200, "application/json", response.as()); - })); + })); - server.onNotFound(fourOhFour); + server.onNotFound(fourOhFour); - server.begin(); + server.begin(); } String SplitFlapWebServer::decodeURIComponent(String encodedString) { - String decodedString = encodedString; - // Replace common URL-encoded characters with their actual symbols - decodedString.replace("%20", " "); // space - decodedString.replace("%21", "!"); // exclamation mark - decodedString.replace("%22", "\""); // double quote - decodedString.replace("%23", "#"); // hash - decodedString.replace("%24", "$"); // dollar sign - decodedString.replace("%25", "%"); // percent - decodedString.replace("%26", "&"); // ampersand - decodedString.replace("%27", "'"); // single quote - decodedString.replace("%28", "("); // left parenthesis - decodedString.replace("%29", ")"); // right parenthesis - decodedString.replace("%2A", "*"); // asterisk - decodedString.replace("%2B", "+"); // plus - decodedString.replace("%2C", ","); // comma - decodedString.replace("%2D", "-"); // hyphen - decodedString.replace("%2E", "."); // period - decodedString.replace("%2F", "/"); // forward slash - decodedString.replace("%3A", ":"); // colon - decodedString.replace("%3B", ";"); // semicolon - decodedString.replace("%3C", "<"); // less than - decodedString.replace("%3D", "="); // equal sign - decodedString.replace("%3E", ">"); // greater than - decodedString.replace("%3F", "?"); // question mark - decodedString.replace("%40", "@"); // at symbol - decodedString.replace("%5B", "["); // left bracket - decodedString.replace("%5C", "\\"); // backslash - decodedString.replace("%5D", "]"); // right bracket - decodedString.replace("%5E", "^"); // caret - decodedString.replace("%5F", "_"); // underscore - decodedString.replace("%60", "`"); // grave accent - decodedString.replace("%7B", "{"); // left brace - decodedString.replace("%7C", "|"); // vertical bar - decodedString.replace("%7D", "}"); // right brace - decodedString.replace("%7E", "~"); // tilde - - // Handle percent-encoded values for characters beyond basic ASCII (e.g., - // extended Unicode) - decodedString.replace("%", ""); - - return decodedString; + String decodedString = encodedString; + // Replace common URL-encoded characters with their actual symbols + decodedString.replace("%20", " "); // space + decodedString.replace("%21", "!"); // exclamation mark + decodedString.replace("%22", "\""); // double quote + decodedString.replace("%23", "#"); // hash + decodedString.replace("%24", "$"); // dollar sign + decodedString.replace("%25", "%"); // percent + decodedString.replace("%26", "&"); // ampersand + decodedString.replace("%27", "'"); // single quote + decodedString.replace("%28", "("); // left parenthesis + decodedString.replace("%29", ")"); // right parenthesis + decodedString.replace("%2A", "*"); // asterisk + decodedString.replace("%2B", "+"); // plus + decodedString.replace("%2C", ","); // comma + decodedString.replace("%2D", "-"); // hyphen + decodedString.replace("%2E", "."); // period + decodedString.replace("%2F", "/"); // forward slash + decodedString.replace("%3A", ":"); // colon + decodedString.replace("%3B", ";"); // semicolon + decodedString.replace("%3C", "<"); // less than + decodedString.replace("%3D", "="); // equal sign + decodedString.replace("%3E", ">"); // greater than + decodedString.replace("%3F", "?"); // question mark + decodedString.replace("%40", "@"); // at symbol + decodedString.replace("%5B", "["); // left bracket + decodedString.replace("%5C", "\\"); // backslash + decodedString.replace("%5D", "]"); // right bracket + decodedString.replace("%5E", "^"); // caret + decodedString.replace("%5F", "_"); // underscore + decodedString.replace("%60", "`"); // grave accent + decodedString.replace("%7B", "{"); // left brace + decodedString.replace("%7C", "|"); // vertical bar + decodedString.replace("%7D", "}"); // right brace + decodedString.replace("%7E", "~"); // tilde + + // Handle percent-encoded values for characters beyond basic ASCII (e.g., + // extended Unicode) + decodedString.replace("%", ""); + + return decodedString; } diff --git a/src/SplitFlapWebServer.h b/src/SplitFlapWebServer.h index 258d738..babfbac 100644 --- a/src/SplitFlapWebServer.h +++ b/src/SplitFlapWebServer.h @@ -1,104 +1,92 @@ -// SplitFlapWebServer.h (header file) +#pragma once -#ifndef SplitFlapWebServer_h -#define SplitFlapWebServer_h -#include "Arduino.h" #include "JsonSettings.h" -#include "LittleFS.h" // use `pio run -t uploadfs` to upload file system -#include "time.h" //for network time protocol -#include //for settings json -#include //by ESP32Async, Requires AsyncTCP by ESP32Async + +#include +#include +#include #include +#include #include +#include class SplitFlapWebServer { -public: - SplitFlapWebServer(JsonSettings &settings); - void init(); - void setTimezone(); - - // Wifi Connectivity - bool loadWiFiCredentials(); - bool connectToWifi(); - bool getAttemptReconnect() const { return attemptReconnect; } - void setAttemptReconnect(bool input) { attemptReconnect = input; } - void startWebServer(); - void endMDNS(); - void startMDNS(); - void startAccessPoint(); - void checkWiFi(); - unsigned long getLastCheckWifiTime() { return lastCheckWifiTime; } - void setLastCheckWifiTime(unsigned long input) { lastCheckWifiTime = input; } - int getWifiCheckInterval() { return wifiCheckInterval; } - - // Mode - int getMode(); - - // Mode 0 - Single String - String getInputString() const { return inputString; } - String getWrittenString() const { return writtenString; } - void setWrittenString(String input) { writtenString = input; } - - // Mode 1, Multi Input - String getMultiInputString() const { return multiInputString; } - int getMultiWordDelay() const { return multiWordDelay; } - unsigned long getLastSwitchMultiTime() { return lastSwitchMultiTime; } - void setLastSwitchMultiTime(unsigned long input) { - lastSwitchMultiTime = input; - } - int getMultiWordCurrentIndex() { return multiWordCurrentIndex; } - void setMultiWordCurrentIndex(int input) { multiWordCurrentIndex = input; } - int getNumMultiWords() const { return numMultiWords; } - - // Mode 2, Date - // Function to get current minute as a string - String getCurrentMinute(); - String getCurrentHour(); - String getDayPrefix(int n); - String getMonthPrefix(int n); - String getCurrentDay(); - unsigned long getLastCheckDateTime() { return lastCheckDateTime; } - void setLastCheckDateTime(unsigned long input) { lastCheckDateTime = input; } - int getDateCheckInterval() { return checkDateInterval; } - - int getCentering() { return centering; } - -private: - JsonSettings &settings; - - String decodeURIComponent(String encodedString); - void setInputString(String input) { inputString = input; } - void setMultiInputString(String input) { multiInputString = input; } - - void setMode(int targetMode); - void setMultiDelay(int input) { multiWordDelay = input; } - - unsigned long lastCheckDateTime; - int checkDateInterval; - - int connectionMode; // 0 is AP mode, 1 is Internet Mode - int centering; // whether to center text from custom imput - - int numMultiWords; - unsigned long lastSwitchMultiTime; - int multiWordDelay; - int multiWordCurrentIndex; - String multiInputString; // latest multi input from user - - String inputString; // latest single input from user - String - writtenString; // string for whatever is currently written to the display - - bool attemptReconnect; - unsigned long lastCheckWifiTime; - int wifiCheckInterval; - AsyncWebServer server; // Declare server as a class member + public: + SplitFlapWebServer(JsonSettings &settings); + void init(); + void setTimezone(); + + // Wifi Connectivity + bool loadWiFiCredentials(); + bool connectToWifi(); + bool getAttemptReconnect() const { return attemptReconnect; } + void setAttemptReconnect(bool input) { attemptReconnect = input; } + void startWebServer(); + void endMDNS(); + void startMDNS(); + void startAccessPoint(); + void checkWiFi(); + unsigned long getLastCheckWifiTime() { return lastCheckWifiTime; } + void setLastCheckWifiTime(unsigned long input) { lastCheckWifiTime = input; } + int getWifiCheckInterval() { return wifiCheckInterval; } + + // Mode + int getMode(); + + // Mode 0 - Single String + String getInputString() const { return inputString; } + String getWrittenString() const { return writtenString; } + void setWrittenString(String input) { writtenString = input; } + + // Mode 1, Multi Input + String getMultiInputString() const { return multiInputString; } + int getMultiWordDelay() const { return multiWordDelay; } + unsigned long getLastSwitchMultiTime() { return lastSwitchMultiTime; } + void setLastSwitchMultiTime(unsigned long input) { lastSwitchMultiTime = input; } + int getMultiWordCurrentIndex() { return multiWordCurrentIndex; } + void setMultiWordCurrentIndex(int input) { multiWordCurrentIndex = input; } + int getNumMultiWords() const { return numMultiWords; } + + // Mode 2, Date + // Function to get current minute as a string + String getCurrentMinute(); + String getCurrentHour(); + String getDayPrefix(int n); + String getMonthPrefix(int n); + String getCurrentDay(); + unsigned long getLastCheckDateTime() { return lastCheckDateTime; } + void setLastCheckDateTime(unsigned long input) { lastCheckDateTime = input; } + int getDateCheckInterval() { return checkDateInterval; } + + int getCentering() { return centering; } + + private: + JsonSettings &settings; + + String decodeURIComponent(String encodedString); + void setInputString(String input) { inputString = input; } + void setMultiInputString(String input) { multiInputString = input; } + + void setMode(int targetMode); + void setMultiDelay(int input) { multiWordDelay = input; } + + unsigned long lastCheckDateTime; + int checkDateInterval; + + int connectionMode; // 0 is AP mode, 1 is Internet Mode + int centering; // whether to center text from custom imput + + int numMultiWords; + unsigned long lastSwitchMultiTime; + int multiWordDelay; + int multiWordCurrentIndex; + String multiInputString; // latest multi input from user + + String inputString; // latest single input from user + String writtenString; // string for whatever is currently written to the display + + bool attemptReconnect; + unsigned long lastCheckWifiTime; + int wifiCheckInterval; + AsyncWebServer server; // Declare server as a class member }; - -#endif -// __ -// (QUACK)>(o )___ -// ( ._> / -// `---' -// Morgan Manly -// 07/02/2025