From 91b0fcb2576104a02aafaafa7d0ac4a024da0011 Mon Sep 17 00:00:00 2001 From: puzzled-pancake <78745145+puzzled-pancake@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:39:48 +1300 Subject: [PATCH 1/2] Added comments on ANZ added 1w limit Added comments https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf As noted 1w limit on both --- src/mesh/RadioInterface.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index d280377b4..c5046d6bb 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -48,9 +48,10 @@ const RegionInfo regions[] = { RDEF(JP, 920.8f, 927.8f, 100, 0, 16, true, false), /* - ??? + https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf + https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf */ - RDEF(ANZ, 915.0f, 928.0f, 100, 0, 0, true, false), + RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false), /* https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf From ca4c1c9d7ce7f662a26ac1b3e1e2e14dd54b324a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 28 Feb 2022 21:19:38 +0000 Subject: [PATCH 2/2] Moved button thread to its own file (#1260) * Moved button thread to its own file * Move some debug code blocks into their own files * Shutdown refactoring * Removed GENIEBLOCKS --- src/ButtonThread.h | 220 +++++++++++++++++++++++++++++ src/debug/axpDebug.h | 19 +++ src/debug/i2cScan.h | 44 ++++++ src/main.cpp | 326 +------------------------------------------ src/main.h | 1 + src/shutdown.h | 41 ++++++ 6 files changed, 329 insertions(+), 322 deletions(-) create mode 100644 src/ButtonThread.h create mode 100644 src/debug/axpDebug.h create mode 100644 src/debug/i2cScan.h create mode 100644 src/shutdown.h diff --git a/src/ButtonThread.h b/src/ButtonThread.h new file mode 100644 index 000000000..1b86f0924 --- /dev/null +++ b/src/ButtonThread.h @@ -0,0 +1,220 @@ +#include "configuration.h" +#include "concurrency/OSThread.h" +#include "PowerFSM.h" +#include "RadioLibInterface.h" +#include "graphics/Screen.h" +#include "power.h" +#include "buzz.h" +#include + +#ifndef NO_ESP32 +#include "nimble/BluetoothUtil.h" +#endif + +namespace concurrency +{ +/** + * Watch a GPIO and if we get an IRQ, wake the main thread. + * Use to add wake on button press + */ +void wakeOnIrq(int irq, int mode) +{ + attachInterrupt( + irq, + [] { + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + FALLING); +} + +class ButtonThread : public concurrency::OSThread +{ +// Prepare for button presses +#ifdef BUTTON_PIN + OneButton userButton; +#endif +#ifdef BUTTON_PIN_ALT + OneButton userButtonAlt; +#endif +#ifdef BUTTON_PIN_TOUCH + OneButton userButtonTouch; +#endif + static bool shutdown_on_long_stop; + + public: + static uint32_t longPressTime; + + // callback returns the period for the next callback invocation (or 0 if we should no longer be called) + ButtonThread() : OSThread("Button") + { +#ifdef BUTTON_PIN + userButton = OneButton(BUTTON_PIN, true, true); +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + pinMode(BUTTON_PIN, INPUT_PULLUP_SENSE); +#endif + userButton.attachClick(userButtonPressed); + userButton.attachDuringLongPress(userButtonPressedLong); + userButton.attachDoubleClick(userButtonDoublePressed); + userButton.attachMultiClick(userButtonMultiPressed); + userButton.attachLongPressStart(userButtonPressedLongStart); + userButton.attachLongPressStop(userButtonPressedLongStop); + wakeOnIrq(BUTTON_PIN, FALLING); +#endif +#ifdef BUTTON_PIN_ALT + userButtonAlt = OneButton(BUTTON_PIN_ALT, true, true); +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + pinMode(BUTTON_PIN_ALT, INPUT_PULLUP_SENSE); +#endif + userButtonAlt.attachClick(userButtonPressed); + userButtonAlt.attachDuringLongPress(userButtonPressedLong); + userButtonAlt.attachDoubleClick(userButtonDoublePressed); + userButtonAlt.attachLongPressStart(userButtonPressedLongStart); + userButtonAlt.attachLongPressStop(userButtonPressedLongStop); + wakeOnIrq(BUTTON_PIN_ALT, FALLING); +#endif + +#ifdef BUTTON_PIN_TOUCH + userButtonTouch = OneButton(BUTTON_PIN_TOUCH, true, true); +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did + pinMode(BUTTON_PIN_TOUCH, INPUT_PULLUP_SENSE); +#endif + userButtonTouch.attachClick(touchPressed); + userButtonTouch.attachDuringLongPress(touchPressedLong); + userButtonTouch.attachDoubleClick(touchDoublePressed); + userButtonTouch.attachLongPressStart(touchPressedLongStart); + userButtonTouch.attachLongPressStop(touchPressedLongStop); + wakeOnIrq(BUTTON_PIN_TOUCH, FALLING); +#endif + + } + + protected: + /// If the button is pressed we suppress CPU sleep until release + int32_t runOnce() override + { + canSleep = true; // Assume we should not keep the board awake + +#ifdef BUTTON_PIN + userButton.tick(); + canSleep &= userButton.isIdle(); +#endif +#ifdef BUTTON_PIN_ALT + userButtonAlt.tick(); + canSleep &= userButtonAlt.isIdle(); +#endif +#ifdef BUTTON_PIN_TOUCH + userButtonTouch.tick(); + canSleep &= userButtonTouch.isIdle(); +#endif + // if (!canSleep) DEBUG_MSG("Supressing sleep!\n"); + // else DEBUG_MSG("sleep ok\n"); + + return 5; + } + + private: + static void touchPressed() + { + screen->forceDisplay(); + DEBUG_MSG("touch press!\n"); + } + static void touchDoublePressed() + { + DEBUG_MSG("touch double press!\n"); + } + static void touchPressedLong() + { + DEBUG_MSG("touch press long!\n"); + } + static void touchDoublePressedLong() + { + DEBUG_MSG("touch double pressed!\n"); + } + static void touchPressedLongStart() + { + DEBUG_MSG("touch long press start!\n"); + } + static void touchPressedLongStop() + { + DEBUG_MSG("touch long press stop!\n"); + } + + + static void userButtonPressed() + { + // DEBUG_MSG("press!\n"); + powerFSM.trigger(EVENT_PRESS); + } + static void userButtonPressedLong() + { + // DEBUG_MSG("Long press!\n"); +#ifndef NRF52_SERIES + screen->adjustBrightness(); +#endif + // If user button is held down for 5 seconds, shutdown the device. + if (millis() - longPressTime > 5 * 1000) { +#ifdef TBEAM_V10 + if (axp192_found == true) { + setLed(false); + power->shutdown(); + } +#elif NRF52_SERIES + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediatedly. + if (!shutdown_on_long_stop) { + screen->startShutdownScreen(); + DEBUG_MSG("Shutdown from long press"); + playBeep(); + ledOff(PIN_LED1); + ledOff(PIN_LED2); + shutdown_on_long_stop = true; + } +#endif + } else { + // DEBUG_MSG("Long press %u\n", (millis() - longPressTime)); + } + } + + static void userButtonDoublePressed() + { +#ifndef NO_ESP32 + disablePin(); +#elif defined(HAS_EINK) + digitalWrite(PIN_EINK_EN,digitalRead(PIN_EINK_EN) == LOW); +#endif + } + + static void userButtonMultiPressed() + { +#ifndef NO_ESP32 + clearNVS(); +#endif +#ifdef NRF52_SERIES + clearBonds(); +#endif + } + + + static void userButtonPressedLongStart() + { + DEBUG_MSG("Long press start!\n"); + longPressTime = millis(); + } + + static void userButtonPressedLongStop() + { + DEBUG_MSG("Long press stop!\n"); + longPressTime = 0; + if (shutdown_on_long_stop) { + playShutdownMelody(); + delay(3000); + power->shutdown(); + } + } +}; + +} \ No newline at end of file diff --git a/src/debug/axpDebug.h b/src/debug/axpDebug.h new file mode 100644 index 000000000..28e3ee26e --- /dev/null +++ b/src/debug/axpDebug.h @@ -0,0 +1,19 @@ +#if 0 +// Turn off for now +uint32_t axpDebugRead() +{ + axp.debugCharging(); + DEBUG_MSG("vbus current %f\n", axp.getVbusCurrent()); + DEBUG_MSG("charge current %f\n", axp.getBattChargeCurrent()); + DEBUG_MSG("bat voltage %f\n", axp.getBattVoltage()); + DEBUG_MSG("batt pct %d\n", axp.getBattPercentage()); + DEBUG_MSG("is battery connected %d\n", axp.isBatteryConnect()); + DEBUG_MSG("is USB connected %d\n", axp.isVBUSPlug()); + DEBUG_MSG("is charging %d\n", axp.isChargeing()); + + return 30 * 1000; +} + +Periodic axpDebugOutput(axpDebugRead); +axpDebugOutput.setup(); +#endif \ No newline at end of file diff --git a/src/debug/i2cScan.h b/src/debug/i2cScan.h new file mode 100644 index 000000000..4752bf0bf --- /dev/null +++ b/src/debug/i2cScan.h @@ -0,0 +1,44 @@ +#include "../configuration.h" +#include "../main.h" +#include + +#ifndef NO_WIRE +void scanI2Cdevice(void) +{ + byte err, addr; + int nDevices = 0; + for (addr = 1; addr < 127; addr++) { + Wire.beginTransmission(addr); + err = Wire.endTransmission(); + if (err == 0) { + DEBUG_MSG("I2C device found at address 0x%x\n", addr); + + nDevices++; + + if (addr == SSD1306_ADDRESS) { + screen_found = addr; + DEBUG_MSG("ssd1306 display found\n"); + } + if (addr == ST7567_ADDRESS) { + screen_found = addr; + DEBUG_MSG("st7567 display found\n"); + } +#ifdef AXP192_SLAVE_ADDRESS + if (addr == AXP192_SLAVE_ADDRESS) { + axp192_found = true; + DEBUG_MSG("axp192 PMU found\n"); + } +#endif + } else if (err == 4) { + DEBUG_MSG("Unknow error at address 0x%x\n", addr); + } + } + + if (nDevices == 0) + DEBUG_MSG("No I2C devices found\n"); + else + DEBUG_MSG("done\n"); +} +#else +void scanI2Cdevice(void) {} +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index f025d3dd2..400ddff63 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,8 +20,10 @@ #include "main.h" #include "modules/Modules.h" #include "sleep.h" +#include "shutdown.h" #include "target_specific.h" -#include +#include "debug/i2cScan.h" +#include "debug/axpDebug.h" #include // #include @@ -41,6 +43,7 @@ #include "SX1262Interface.h" #include "SX1268Interface.h" #include "LLCC68Interface.h" +#include "ButtonThread.h" using namespace concurrency; @@ -64,50 +67,6 @@ bool axp192_found; Router *router = NULL; // Users of router don't care what sort of subclass implements that API -// ----------------------------------------------------------------------------- -// Application -// ----------------------------------------------------------------------------- -#ifndef NO_WIRE -void scanI2Cdevice(void) -{ - byte err, addr; - int nDevices = 0; - for (addr = 1; addr < 127; addr++) { - Wire.beginTransmission(addr); - err = Wire.endTransmission(); - if (err == 0) { - DEBUG_MSG("I2C device found at address 0x%x\n", addr); - - nDevices++; - - if (addr == SSD1306_ADDRESS) { - screen_found = addr; - DEBUG_MSG("ssd1306 display found\n"); - } - if (addr == ST7567_ADDRESS) { - screen_found = addr; - DEBUG_MSG("st7567 display found\n"); - } -#ifdef AXP192_SLAVE_ADDRESS - if (addr == AXP192_SLAVE_ADDRESS) { - axp192_found = true; - DEBUG_MSG("axp192 PMU found\n"); - } -#endif - } else if (err == 4) { - DEBUG_MSG("Unknow error at address 0x%x\n", addr); - } - } - - if (nDevices == 0) - DEBUG_MSG("No I2C devices found\n"); - else - DEBUG_MSG("done\n"); -} -#else -void scanI2Cdevice(void) {} -#endif - const char *getDeviceName() { uint8_t dmac[6]; @@ -161,210 +120,6 @@ class PowerFSMThread : public OSThread } }; -/** - * Watch a GPIO and if we get an IRQ, wake the main thread. - * Use to add wake on button press - */ -void wakeOnIrq(int irq, int mode) -{ - attachInterrupt( - irq, - [] { - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - FALLING); -} - -class ButtonThread : public OSThread -{ -// Prepare for button presses -#ifdef BUTTON_PIN - OneButton userButton; -#endif -#ifdef BUTTON_PIN_ALT - OneButton userButtonAlt; -#endif -#ifdef BUTTON_PIN_TOUCH - OneButton userButtonTouch; -#endif - static bool shutdown_on_long_stop; - - public: - static uint32_t longPressTime; - - // callback returns the period for the next callback invocation (or 0 if we should no longer be called) - ButtonThread() : OSThread("Button") - { -#ifdef BUTTON_PIN - userButton = OneButton(BUTTON_PIN, true, true); -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did - pinMode(BUTTON_PIN, INPUT_PULLUP_SENSE); -#endif - userButton.attachClick(userButtonPressed); - userButton.attachDuringLongPress(userButtonPressedLong); - userButton.attachDoubleClick(userButtonDoublePressed); - userButton.attachMultiClick(userButtonMultiPressed); - userButton.attachLongPressStart(userButtonPressedLongStart); - userButton.attachLongPressStop(userButtonPressedLongStop); - wakeOnIrq(BUTTON_PIN, FALLING); -#endif -#ifdef BUTTON_PIN_ALT - userButtonAlt = OneButton(BUTTON_PIN_ALT, true, true); -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did - pinMode(BUTTON_PIN_ALT, INPUT_PULLUP_SENSE); -#endif - userButtonAlt.attachClick(userButtonPressed); - userButtonAlt.attachDuringLongPress(userButtonPressedLong); - userButtonAlt.attachDoubleClick(userButtonDoublePressed); - userButtonAlt.attachLongPressStart(userButtonPressedLongStart); - userButtonAlt.attachLongPressStop(userButtonPressedLongStop); - wakeOnIrq(BUTTON_PIN_ALT, FALLING); -#endif - -#ifdef BUTTON_PIN_TOUCH - userButtonTouch = OneButton(BUTTON_PIN_TOUCH, true, true); -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did - pinMode(BUTTON_PIN_TOUCH, INPUT_PULLUP_SENSE); -#endif - userButtonTouch.attachClick(touchPressed); - userButtonTouch.attachDuringLongPress(touchPressedLong); - userButtonTouch.attachDoubleClick(touchDoublePressed); - userButtonTouch.attachLongPressStart(touchPressedLongStart); - userButtonTouch.attachLongPressStop(touchPressedLongStop); - wakeOnIrq(BUTTON_PIN_TOUCH, FALLING); -#endif - - } - - protected: - /// If the button is pressed we suppress CPU sleep until release - int32_t runOnce() override - { - canSleep = true; // Assume we should not keep the board awake - -#ifdef BUTTON_PIN - userButton.tick(); - canSleep &= userButton.isIdle(); -#endif -#ifdef BUTTON_PIN_ALT - userButtonAlt.tick(); - canSleep &= userButtonAlt.isIdle(); -#endif -#ifdef BUTTON_PIN_TOUCH - userButtonTouch.tick(); - canSleep &= userButtonTouch.isIdle(); -#endif - // if (!canSleep) DEBUG_MSG("Supressing sleep!\n"); - // else DEBUG_MSG("sleep ok\n"); - - return 5; - } - - private: - static void touchPressed() - { - screen->forceDisplay(); - DEBUG_MSG("touch press!\n"); - } - static void touchDoublePressed() - { - DEBUG_MSG("touch double press!\n"); - } - static void touchPressedLong() - { - DEBUG_MSG("touch press long!\n"); - } - static void touchDoublePressedLong() - { - DEBUG_MSG("touch double pressed!\n"); - } - static void touchPressedLongStart() - { - DEBUG_MSG("touch long press start!\n"); - } - static void touchPressedLongStop() - { - DEBUG_MSG("touch long press stop!\n"); - } - - - static void userButtonPressed() - { - // DEBUG_MSG("press!\n"); - powerFSM.trigger(EVENT_PRESS); - } - static void userButtonPressedLong() - { - // DEBUG_MSG("Long press!\n"); -#ifndef NRF52_SERIES - screen->adjustBrightness(); -#endif - // If user button is held down for 5 seconds, shutdown the device. - if (millis() - longPressTime > 5 * 1000) { -#ifdef TBEAM_V10 - if (axp192_found == true) { - setLed(false); - power->shutdown(); - } -#elif NRF52_SERIES - // Do actual shutdown when button released, otherwise the button release - // may wake the board immediatedly. - if (!shutdown_on_long_stop) { - screen->startShutdownScreen(); - DEBUG_MSG("Shutdown from long press"); - playBeep(); - ledOff(PIN_LED1); - ledOff(PIN_LED2); - shutdown_on_long_stop = true; - } -#endif - } else { - // DEBUG_MSG("Long press %u\n", (millis() - longPressTime)); - } - } - - static void userButtonDoublePressed() - { -#ifndef NO_ESP32 - disablePin(); -#elif defined(HAS_EINK) - digitalWrite(PIN_EINK_EN,digitalRead(PIN_EINK_EN) == LOW); -#endif - } - - static void userButtonMultiPressed() - { -#ifndef NO_ESP32 - clearNVS(); -#endif -#ifdef NRF52_SERIES - clearBonds(); -#endif - } - - - static void userButtonPressedLongStart() - { - DEBUG_MSG("Long press start!\n"); - longPressTime = millis(); - } - - static void userButtonPressedLongStop() - { - DEBUG_MSG("Long press stop!\n"); - longPressTime = 0; - if (shutdown_on_long_stop) { - playShutdownMelody(); - delay(3000); - power->shutdown(); - } - } -}; - bool ButtonThread::shutdown_on_long_stop = false; static Periodic *ledPeriodic; @@ -510,22 +265,6 @@ void setup() readFromRTC(); // read the main CPU RTC at first (in case we can't get GPS time) -#ifdef GENIEBLOCKS - Im intentionally breaking your build so you see this note.Feel free to revert if not correct.I think you can - remove this GPS_RESET_N code by instead defining PIN_GPS_RESET and - use the shared code in GPS.cpp instead.- geeksville - - // gps setup - pinMode(GPS_RESET_N, OUTPUT); - pinMode(GPS_EXTINT, OUTPUT); - digitalWrite(GPS_RESET_N, HIGH); - digitalWrite(GPS_EXTINT, LOW); - // battery setup - // If we want to read battery level, we need to set BATTERY_EN_PIN pin to low. - // ToDo: For low power consumption after read battery level, set that pin to high. - pinMode(BATTERY_EN_PIN, OUTPUT); - digitalWrite(BATTERY_EN_PIN, LOW); -#endif gps = createGps(); if (gps) @@ -680,66 +419,9 @@ void setup() setCPUFast(false); // 80MHz is fine for our slow peripherals } -#if 0 -// Turn off for now - -uint32_t axpDebugRead() -{ - axp.debugCharging(); - DEBUG_MSG("vbus current %f\n", axp.getVbusCurrent()); - DEBUG_MSG("charge current %f\n", axp.getBattChargeCurrent()); - DEBUG_MSG("bat voltage %f\n", axp.getBattVoltage()); - DEBUG_MSG("batt pct %d\n", axp.getBattPercentage()); - DEBUG_MSG("is battery connected %d\n", axp.isBatteryConnect()); - DEBUG_MSG("is USB connected %d\n", axp.isVBUSPlug()); - DEBUG_MSG("is charging %d\n", axp.isChargeing()); - - return 30 * 1000; -} - -Periodic axpDebugOutput(axpDebugRead); -axpDebugOutput.setup(); -#endif - uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) -void powerCommandsCheck() -{ - if (rebootAtMsec && millis() > rebootAtMsec) { -#ifndef NO_ESP32 - DEBUG_MSG("Rebooting for update\n"); - ESP.restart(); -#else - DEBUG_MSG("FIXME implement reboot for this platform"); -#endif - } - -#if NRF52_SERIES - if (shutdownAtMsec) { - screen->startShutdownScreen(); - playBeep(); - ledOff(PIN_LED1); - ledOff(PIN_LED2); - } -#endif - - if (shutdownAtMsec && millis() > shutdownAtMsec) { - DEBUG_MSG("Shutting down from admin command\n"); -#ifdef TBEAM_V10 - if (axp192_found == true) { - setLed(false); - power->shutdown(); - } -#elif NRF52_SERIES - playShutdownMelody(); - power->shutdown(); -#else - DEBUG_MSG("FIXME implement shutdown for this platform"); -#endif - } -} - // If a thread does something that might need for it to be rescheduled ASAP it can set this flag // This will supress the current delay and instead try to run ASAP. bool runASAP; diff --git a/src/main.h b/src/main.h index 2d93caebb..c03652368 100644 --- a/src/main.h +++ b/src/main.h @@ -5,6 +5,7 @@ #include "PowerStatus.h" #include "graphics/Screen.h" +extern uint8_t screen_found; extern bool axp192_found; extern bool isCharging; extern bool isUSBPowered; diff --git a/src/shutdown.h b/src/shutdown.h new file mode 100644 index 000000000..10c93bde2 --- /dev/null +++ b/src/shutdown.h @@ -0,0 +1,41 @@ +#include "configuration.h" +#include "graphics/Screen.h" +#include "power.h" +#include "buzz.h" +#include "main.h" + +void powerCommandsCheck() +{ + if (rebootAtMsec && millis() > rebootAtMsec) { +#ifndef NO_ESP32 + DEBUG_MSG("Rebooting for update\n"); + ESP.restart(); +#else + DEBUG_MSG("FIXME implement reboot for this platform"); +#endif + } + +#if NRF52_SERIES + if (shutdownAtMsec) { + screen->startShutdownScreen(); + playBeep(); + ledOff(PIN_LED1); + ledOff(PIN_LED2); + } +#endif + + if (shutdownAtMsec && millis() > shutdownAtMsec) { + DEBUG_MSG("Shutting down from admin command\n"); +#ifdef TBEAM_V10 + if (axp192_found == true) { + setLed(false); + power->shutdown(); + } +#elif NRF52_SERIES + playShutdownMelody(); + power->shutdown(); +#else + DEBUG_MSG("FIXME implement shutdown for this platform"); +#endif + } +}