From 825e48086b98bc4dd67367fc65b093a37247822a Mon Sep 17 00:00:00 2001 From: RnD1 Date: Tue, 14 Apr 2026 05:11:09 +0900 Subject: [PATCH] Req. Arduino Core 3.0.2 --- ConnectWiFi.cpp | 3 +- HermitCrab.h | 39 +- LED0.cpp | 6 +- Libraries/Adafruit_SSD1306.cpp | 965 ++++++++++++++++++ Libraries/Adafruit_SSD1306.h | 167 +++ Libraries/NetworkClient.cpp | 680 ++++++++++++ Libraries/NetworkClient.h | 111 ++ .../NimBLEAdvertisedDevice.cpp | 828 +++++++++++++++ .../NimBLEAdvertisedDevice.h | 197 ++++ .../NimBLE-Arduino_modified/NimBLEScan.cpp | 625 ++++++++++++ .../NimBLE-Arduino_modified/NimBLEScan.h | 146 +++ OTA.cpp | 2 +- README.md | 3 +- Setup.cpp | 185 ++-- Task0.ino | 1 + UI.cpp | 10 +- WiFiHost.cpp | 48 +- WiFiHost.h | 3 + zcd.cpp | 4 +- 19 files changed, 3899 insertions(+), 124 deletions(-) create mode 100644 Libraries/Adafruit_SSD1306.cpp create mode 100644 Libraries/Adafruit_SSD1306.h create mode 100644 Libraries/NetworkClient.cpp create mode 100644 Libraries/NetworkClient.h create mode 100644 Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.cpp create mode 100644 Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.h create mode 100644 Libraries/NimBLE-Arduino_modified/NimBLEScan.cpp create mode 100644 Libraries/NimBLE-Arduino_modified/NimBLEScan.h diff --git a/ConnectWiFi.cpp b/ConnectWiFi.cpp index dbf5453..8480caa 100644 --- a/ConnectWiFi.cpp +++ b/ConnectWiFi.cpp @@ -40,7 +40,8 @@ void checkAndUpdateWiFiCredentials() { } } -IRAM_ATTR void checkWiFi(unsigned long tickMillis) { +IRAM_ATTR +void checkWiFi(unsigned long tickMillis) { static unsigned long lastAttempt = 0; static bool bConnecting = false; diff --git a/HermitCrab.h b/HermitCrab.h index 8bfb40f..691e510 100644 --- a/HermitCrab.h +++ b/HermitCrab.h @@ -20,7 +20,7 @@ #ifndef DEBUG #define DEBUG 1 // Set to 0 to disable debug output #endif -#undef DEBUG + //#define BLE_DEBUG #ifdef BLE_DEBUG @@ -96,28 +96,27 @@ // End of PIN Definition // ======================= + +// Resolution for all Channels #define PWM_RESOLUTION 10 // 10-bit resolution -#define PWM_MOTOR_RESOLUTION 8 - -#define PWM_AP_CHANNEL 9 -#define PWM_SC_CHANNEL 10 -#define PWM_AP_FREQ 1 -#define PWM_SC_FREQ 2 - -#define PWM_MIST_CHANNEL 8 // MIST -#define PWM_MIST_FREQ 1 // MIST - -#define PWM_HEATER1_CHANNEL 2 -#define PWM_HEATER2_CHANNEL 3 -#define PWM_LIGHT_CHANNEL 4 -#define PWM_MOTOR_CHANNEL 5 -#define PWM_FAN_CHANNEL 6 - -#define PWM_1KHZ_FREQ 1000 // 1 kHz frequency -#define PWM_25KHZ_FREQ 25000 // 25KHz - #define PWM_FULL 1023 #define PWM_OFF 0 + +// High Speed Group - Motor and Fan (26KHz) +#define PWM_MOTOR_CHANNEL 0 +#define PWM_FAN_CHANNEL 1 +// Medium Speed Group - LED & Heater (1KHz) +#define PWM_LIGHT_CHANNEL 2 +#define PWM_HEATER1_CHANNEL 3 +#define PWM_HEATER2_CHANNEL 4 +// Low Speed Group - Mist, WifiLED (1Hz) +#define PWM_MIST_CHANNEL 8 +#define PWM_WIFI_LED_CHANNEL 9 + +#define PWM_1HZ_FREQ 1 // 1 Hz +#define PWM_1KHZ_FREQ 1221 // 1 kHz +#define PWM_26KHZ_FREQ 26042 // 26KHz + #define TAG "HC" enum EVENT_TYPE { diff --git a/LED0.cpp b/LED0.cpp index 5bddf47..44a568d 100644 --- a/LED0.cpp +++ b/LED0.cpp @@ -15,7 +15,7 @@ void CLED0::setup(uint8_t _pin, uint16_t _freq, uint16_t _channel) { setDuty(duty); }; -MY_IRAM_ATTR void CLED0::setFreq(uint16_t _freq) { +void CLED0::setFreq(uint16_t _freq) { if (freq != _freq) { if (_freq == 0) { ledcDetach(pin); @@ -31,7 +31,7 @@ MY_IRAM_ATTR void CLED0::setFreq(uint16_t _freq) { } } -MY_IRAM_ATTR void CLED0::setDuty() { +void CLED0::setDuty() { uint16_t _duty; if (bAC) _duty = LED0_DUTY_AC; @@ -41,7 +41,7 @@ MY_IRAM_ATTR void CLED0::setDuty() { ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink } -MY_IRAM_ATTR void CLED0::setDuty(uint16_t _duty) { +void CLED0::setDuty(uint16_t _duty) { if (duty != _duty) { if (bPWMMode) ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink diff --git a/Libraries/Adafruit_SSD1306.cpp b/Libraries/Adafruit_SSD1306.cpp new file mode 100644 index 0000000..93e17fc --- /dev/null +++ b/Libraries/Adafruit_SSD1306.cpp @@ -0,0 +1,965 @@ +/*! + * @file Adafruit_SSD1306.cpp + * + * @mainpage Arduino library for monochrome OLEDs based on SSD1306 drivers. + * + * @section intro_sec Introduction + * + * This is documentation for Adafruit's SSD1306 library for monochrome + * OLED displays: http://www.adafruit.com/category/63_98 + * + * These displays use I2C or SPI to communicate. I2C requires 2 pins + * (SCL+SDA) and optionally a RESET pin. SPI requires 4 pins (MOSI, SCK, + * select, data/command) and optionally a reset pin. Hardware SPI or + * 'bitbang' software SPI are both supported. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * @section dependencies Dependencies + * + * This library depends on Adafruit_GFX + * being present on your system. Please make sure you have installed the latest + * version before using this library. + * + * @section author Author + * + * Written by Limor Fried/Ladyada for Adafruit Industries, with + * contributions from the open source community. + * + * @section license License + * + * BSD license, all text above, and the splash screen included below, + * must be included in any redistribution. + * + */ + +#include +#include "Adafruit_SSD1306.h" +#include + +// SOME DEFINES AND STATIC VARIABLES USED INTERNALLY ----------------------- + +#if defined(I2C_BUFFER_LENGTH) +#define WIRE_MAX min(256, I2C_BUFFER_LENGTH) ///< Particle or similar Wire lib +#else +#define WIRE_MAX 32 ///< Use common Arduino core default +#endif + +#define ssd1306_swap(a, b) \ + (((a) ^= (b)), ((b) ^= (a)), ((a) ^= (b))) ///< No-temp-var swap operation + +#if ARDUINO >= 100 +#define WIRE_WRITE wire->write ///< Wire write function in recent Arduino lib +#else +#define WIRE_WRITE wire->send ///< Wire write function in older Arduino lib +#endif + +#define SSD1306_SELECT digitalWrite(csPin, LOW); ///< Device select +#define SSD1306_DESELECT digitalWrite(csPin, HIGH); ///< Device deselect +#define SSD1306_MODE_COMMAND digitalWrite(dcPin, LOW); ///< Command mode +#define SSD1306_MODE_DATA digitalWrite(dcPin, HIGH); ///< Data mode + +#if (ARDUINO >= 157) && !defined(ARDUINO_STM32_FEATHER) +#define SETWIRECLOCK wire->setClock(wireClk) ///< Set before I2C transfer +#define RESWIRECLOCK wire->setClock(restoreClk) ///< Restore after I2C xfer +#else // setClock() is not present in older Arduino Wire lib (or WICED) +#define SETWIRECLOCK ///< Dummy stand-in define +#define RESWIRECLOCK ///< keeps compiler happy +#endif + +// The definition of 'transaction' is broadened a bit in the context of +// this library -- referring not just to SPI transactions (if supported +// in the version of the SPI library being used), but also chip select +// (if SPI is being used, whether hardware or soft), and also to the +// beginning and end of I2C transfers (the Wire clock may be sped up before +// issuing data to the display, then restored to the default rate afterward +// so other I2C device types still work). All of these are encapsulated +// in the TRANSACTION_* macros. + +// Check first if Wire, then hardware SPI, then soft SPI: +#define TRANSACTION_START SETWIRECLOCK; +#define TRANSACTION_END RESWIRECLOCK; \ + +// CONSTRUCTORS, DESTRUCTOR ------------------------------------------------ + +/*! + @brief Constructor for I2C-interfaced SSD1306 displays. + @param w + Display width in pixels + @param h + Display height in pixels + @param twi + Pointer to an existing TwoWire instance (e.g. &Wire, the + microcontroller's primary I2C bus). + @param rst_pin + Reset pin (using Arduino pin numbering), or -1 if not used + (some displays might be wired to share the microcontroller's + reset pin). + @param clkDuring + Speed (in Hz) for Wire transmissions in SSD1306 library calls. + Defaults to 400000 (400 KHz), a known 'safe' value for most + microcontrollers, and meets the SSD1306 datasheet spec. + Some systems can operate I2C faster (800 KHz for ESP32, 1 MHz + for many other 32-bit MCUs), and some (perhaps not all) + SSD1306's can work with this -- so it's optionally be specified + here and is not a default behavior. (Ignored if using pre-1.5.7 + Arduino software, which operates I2C at a fixed 100 KHz.) + @param clkAfter + Speed (in Hz) for Wire transmissions following SSD1306 library + calls. Defaults to 100000 (100 KHz), the default Arduino Wire + speed. This is done rather than leaving it at the 'during' speed + because other devices on the I2C bus might not be compatible + with the faster rate. (Ignored if using pre-1.5.7 Arduino + software, which operates I2C at a fixed 100 KHz.) + @return Adafruit_SSD1306 object. + @note Call the object's begin() function before use -- buffer + allocation is performed there! +*/ +Adafruit_SSD1306::Adafruit_SSD1306(uint8_t w, uint8_t h, TwoWire *twi, + int8_t rst_pin, uint32_t clkDuring, + uint32_t clkAfter) + : Adafruit_GFX(w, h), spi(NULL), wire(twi ? twi : &Wire), buffer(NULL), + mosiPin(-1), clkPin(-1), dcPin(-1), csPin(-1), rstPin(rst_pin) +#if ARDUINO >= 157 + , + wireClk(clkDuring), restoreClk(clkAfter) +#endif +{ +} + +/*! + @brief Constructor for SPI SSD1306 displays, using software (bitbang) + SPI. + @param w + Display width in pixels + @param h + Display height in pixels + @param mosi_pin + MOSI (master out, slave in) pin (using Arduino pin numbering). + This transfers serial data from microcontroller to display. + @param sclk_pin + SCLK (serial clock) pin (using Arduino pin numbering). + This clocks each bit from MOSI. + @param dc_pin + Data/command pin (using Arduino pin numbering), selects whether + display is receiving commands (low) or data (high). + @param rst_pin + Reset pin (using Arduino pin numbering), or -1 if not used + (some displays might be wired to share the microcontroller's + reset pin). + @param cs_pin + Chip-select pin (using Arduino pin numbering) for sharing the + bus with other devices. Active low. + @return Adafruit_SSD1306 object. + @note Call the object's begin() function before use -- buffer + allocation is performed there! +*/ +Adafruit_SSD1306::Adafruit_SSD1306(uint8_t w, uint8_t h, int8_t mosi_pin, + int8_t sclk_pin, int8_t dc_pin, + int8_t rst_pin, int8_t cs_pin) + : Adafruit_GFX(w, h), spi(NULL), wire(NULL), buffer(NULL), + mosiPin(mosi_pin), clkPin(sclk_pin), dcPin(dc_pin), csPin(cs_pin), + rstPin(rst_pin) {} + +/*! + @brief Constructor for SPI SSD1306 displays, using native hardware SPI. + @param w + Display width in pixels + @param h + Display height in pixels + @param spi_ptr + Pointer to an existing SPIClass instance (e.g. &SPI, the + microcontroller's primary SPI bus). + @param dc_pin + Data/command pin (using Arduino pin numbering), selects whether + display is receiving commands (low) or data (high). + @param rst_pin + Reset pin (using Arduino pin numbering), or -1 if not used + (some displays might be wired to share the microcontroller's + reset pin). + @param cs_pin + Chip-select pin (using Arduino pin numbering) for sharing the + bus with other devices. Active low. + @param bitrate + SPI clock rate for transfers to this display. Default if + unspecified is 8000000UL (8 MHz). + @return Adafruit_SSD1306 object. + @note Call the object's begin() function before use -- buffer + allocation is performed there! +*/ +Adafruit_SSD1306::Adafruit_SSD1306(uint8_t w, uint8_t h, SPIClass *spi_ptr, + int8_t dc_pin, int8_t rst_pin, int8_t cs_pin, + uint32_t bitrate) + : Adafruit_GFX(w, h), spi(spi_ptr ? spi_ptr : &SPI), wire(NULL), + buffer(NULL), mosiPin(-1), clkPin(-1), dcPin(dc_pin), csPin(cs_pin), + rstPin(rst_pin) { +#ifdef SPI_HAS_TRANSACTION + spiSettings = SPISettings(bitrate, MSBFIRST, SPI_MODE0); +#endif +} + +/*! + @brief Issue single command to SSD1306, using I2C or hard/soft SPI as + needed. Because command calls are often grouped, SPI transaction and + selection must be started/ended in calling function for efficiency. This is a + protected function, not exposed (see ssd1306_command() instead). + + @param c + the command character to send to the display. + Refer to ssd1306 data sheet for commands + @return None (void). + @note +*/ +void Adafruit_SSD1306::ssd1306_command1(uint8_t c) { + { // I2C + wire->beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0 + WIRE_WRITE(c); + wire->endTransmission(); + } +} + +/*! + @brief Issue list of commands to SSD1306, same rules as above re: + transactions. This is a protected function, not exposed. + @param c + pointer to list of commands + + @param n + number of commands in the list + + @return None (void). + @note +*/ +void Adafruit_SSD1306::ssd1306_commandList(const uint8_t *c, uint8_t n) { + { // I2C + wire->beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0 + uint16_t bytesOut = 1; + while (n--) { + if (bytesOut >= WIRE_MAX) { + wire->endTransmission(); + wire->beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0 + bytesOut = 1; + } + WIRE_WRITE(pgm_read_byte(c++)); + bytesOut++; + } + wire->endTransmission(); + } +} + +// A public version of ssd1306_command1(), for existing user code that +// might rely on that function. This encapsulates the command transfer +// in a transaction start/end, similar to old library's handling of it. +/*! + @brief Issue a single low-level command directly to the SSD1306 + display, bypassing the library. + @param c + Command to issue (0x00 to 0xFF, see datasheet). + @return None (void). +*/ +void Adafruit_SSD1306::ssd1306_command(uint8_t c) { + TRANSACTION_START + ssd1306_command1(c); + TRANSACTION_END +} + +// ALLOCATE & INIT DISPLAY ------------------------------------------------- + +/*! + @brief Allocate RAM for image buffer, initialize peripherals and pins. + @param vcs + VCC selection. Pass SSD1306_SWITCHCAPVCC to generate the display + voltage (step up) from the 3.3V source, or SSD1306_EXTERNALVCC + otherwise. Most situations with Adafruit SSD1306 breakouts will + want SSD1306_SWITCHCAPVCC. + @param addr + I2C address of corresponding SSD1306 display (or pass 0 to use + default of 0x3C for 128x32 display, 0x3D for all others). + SPI displays (hardware or software) do not use addresses, but + this argument is still required (pass 0 or any value really, + it will simply be ignored). Default if unspecified is 0. + @param reset + If true, and if the reset pin passed to the constructor is + valid, a hard reset will be performed before initializing the + display. If using multiple SSD1306 displays on the same bus, and + if they all share the same reset pin, you should only pass true + on the first display being initialized, false on all others, + else the already-initialized displays would be reset. Default if + unspecified is true. + @param periphBegin + If true, and if a hardware peripheral is being used (I2C or SPI, + but not software SPI), call that peripheral's begin() function, + else (false) it has already been done in one's sketch code. + Cases where false might be used include multiple displays or + other devices sharing a common bus, or situations on some + platforms where a nonstandard begin() function is available + (e.g. a TwoWire interface on non-default pins, as can be done + on the ESP8266 and perhaps others). + @return true on successful allocation/init, false otherwise. + Well-behaved code should check the return value before + proceeding. + @note MUST call this function before any drawing or updates! +*/ +bool Adafruit_SSD1306::begin(uint8_t vcs, uint8_t addr, bool reset, + bool periphBegin) { + + if ((!buffer) && !(buffer = (uint8_t *)malloc(WIDTH * ((HEIGHT + 7) / 8)))) + return false; + + clearDisplay(); + + vccstate = vcs; + + // Setup pin directions + { // Using I2C + // If I2C address is unspecified, use default + // (0x3C for 32-pixel-tall displays, 0x3D for all others). + i2caddr = addr ? addr : ((HEIGHT == 32) ? 0x3C : 0x3D); + // TwoWire begin() function might be already performed by the calling + // function if it has unusual circumstances (e.g. TWI variants that + // can accept different SDA/SCL pins, or if two SSD1306 instances + // with different addresses -- only a single begin() is needed). + if (periphBegin) + wire->begin(); + } + + // Reset SSD1306 if requested and reset pin specified in constructor + if (reset && (rstPin >= 0)) { + pinMode(rstPin, OUTPUT); + digitalWrite(rstPin, HIGH); + delay(1); // VDD goes high at start, pause for 1 ms + digitalWrite(rstPin, LOW); // Bring reset low + delay(10); // Wait 10 ms + digitalWrite(rstPin, HIGH); // Bring out of reset + } + + TRANSACTION_START + + // Init sequence + static const uint8_t init1[] = {SSD1306_DISPLAYOFF, // 0xAE + SSD1306_SETDISPLAYCLOCKDIV, // 0xD5 + 0x80, // the suggested ratio 0x80 + SSD1306_SETMULTIPLEX}; // 0xA8 + ssd1306_commandList(init1, sizeof(init1)); + ssd1306_command1(HEIGHT - 1); + + static const uint8_t init2[] = {SSD1306_SETDISPLAYOFFSET, // 0xD3 + 0x0, // no offset + SSD1306_SETSTARTLINE | 0x0, // line #0 + SSD1306_CHARGEPUMP}; // 0x8D + ssd1306_commandList(init2, sizeof(init2)); + + ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0x14); + + static const uint8_t init3[] = {SSD1306_MEMORYMODE, // 0x20 + 0x00, // 0x0 act like ks0108 + SSD1306_SEGREMAP | 0x1, + SSD1306_COMSCANDEC}; + ssd1306_commandList(init3, sizeof(init3)); + + uint8_t comPins = 0x02; + contrast = 0x8F; + + if ((WIDTH == 128) && (HEIGHT == 32)) { + comPins = 0x02; + contrast = 0x8F; + } else if ((WIDTH == 128) && (HEIGHT == 64)) { + comPins = 0x12; + contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x9F : 0xCF; + } else if ((WIDTH == 96) && (HEIGHT == 16)) { + comPins = 0x2; // ada x12 + contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0xAF; + } else { + // Other screen varieties -- TBD + } + + ssd1306_command1(SSD1306_SETCOMPINS); + ssd1306_command1(comPins); + ssd1306_command1(SSD1306_SETCONTRAST); + ssd1306_command1(contrast); + + ssd1306_command1(SSD1306_SETPRECHARGE); // 0xd9 + ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x22 : 0xF1); + static const uint8_t init5[] = { + SSD1306_SETVCOMDETECT, // 0xDB + 0x40, + SSD1306_DISPLAYALLON_RESUME, // 0xA4 + SSD1306_NORMALDISPLAY, // 0xA6 + SSD1306_DEACTIVATE_SCROLL, + SSD1306_DISPLAYON}; // Main screen turn on + ssd1306_commandList(init5, sizeof(init5)); + + TRANSACTION_END + + return true; // Success +} + +// DRAWING FUNCTIONS ------------------------------------------------------- + +/*! + @brief Set/clear/invert a single pixel. This is also invoked by the + Adafruit_GFX library in generating many higher-level graphics + primitives. + @param x + Column of display -- 0 at left to (screen width - 1) at right. + @param y + Row of display -- 0 at top to (screen height -1) at bottom. + @param color + Pixel color, one of: SSD1306_BLACK, SSD1306_WHITE or + SSD1306_INVERSE. + @return None (void). + @note Changes buffer contents only, no immediate effect on display. + Follow up with a call to display(), or with other graphics + commands as needed by one's own application. +*/ +void Adafruit_SSD1306::drawPixel(int16_t x, int16_t y, uint16_t color) { + if ((x >= 0) && (x < width()) && (y >= 0) && (y < height())) { + // Pixel is in-bounds. Rotate coordinates if needed. + switch (getRotation()) { + case 1: + ssd1306_swap(x, y); + x = WIDTH - x - 1; + break; + case 2: + x = WIDTH - x - 1; + y = HEIGHT - y - 1; + break; + case 3: + ssd1306_swap(x, y); + y = HEIGHT - y - 1; + break; + } + switch (color) { + case SSD1306_WHITE: + buffer[x + (y / 8) * WIDTH] |= (1 << (y & 7)); + break; + case SSD1306_BLACK: + buffer[x + (y / 8) * WIDTH] &= ~(1 << (y & 7)); + break; + case SSD1306_INVERSE: + buffer[x + (y / 8) * WIDTH] ^= (1 << (y & 7)); + break; + } + } +} + +/*! + @brief Clear contents of display buffer (set all pixels to off). + @return None (void). + @note Changes buffer contents only, no immediate effect on display. + Follow up with a call to display(), or with other graphics + commands as needed by one's own application. +*/ +void Adafruit_SSD1306::clearDisplay(void) { + memset(buffer, 0, WIDTH * ((HEIGHT + 7) / 8)); +} + +/*! + @brief Draw a horizontal line. This is also invoked by the Adafruit_GFX + library in generating many higher-level graphics primitives. + @param x + Leftmost column -- 0 at left to (screen width - 1) at right. + @param y + Row of display -- 0 at top to (screen height -1) at bottom. + @param w + Width of line, in pixels. + @param color + Line color, one of: SSD1306_BLACK, SSD1306_WHITE or SSD1306_INVERSE. + @return None (void). + @note Changes buffer contents only, no immediate effect on display. + Follow up with a call to display(), or with other graphics + commands as needed by one's own application. +*/ +void Adafruit_SSD1306::drawFastHLine(int16_t x, int16_t y, int16_t w, + uint16_t color) { + bool bSwap = false; + switch (rotation) { + case 1: + // 90 degree rotation, swap x & y for rotation, then invert x + bSwap = true; + ssd1306_swap(x, y); + x = WIDTH - x - 1; + break; + case 2: + // 180 degree rotation, invert x and y, then shift y around for height. + x = WIDTH - x - 1; + y = HEIGHT - y - 1; + x -= (w - 1); + break; + case 3: + // 270 degree rotation, swap x & y for rotation, + // then invert y and adjust y for w (not to become h) + bSwap = true; + ssd1306_swap(x, y); + y = HEIGHT - y - 1; + y -= (w - 1); + break; + } + + if (bSwap) + drawFastVLineInternal(x, y, w, color); + else + drawFastHLineInternal(x, y, w, color); +} + +/*! + @brief Draw a horizontal line with a width and color. Used by public + methods drawFastHLine,drawFastVLine + @param x + Leftmost column -- 0 at left to (screen width - 1) at right. + @param y + Row of display -- 0 at top to (screen height -1) at bottom. + @param w + Width of line, in pixels. + @param color + Line color, one of: SSD1306_BLACK, SSD1306_WHITE or + SSD1306_INVERSE. + @return None (void). + @note Changes buffer contents only, no immediate effect on display. + Follow up with a call to display(), or with other graphics + commands as needed by one's own application. +*/ +void Adafruit_SSD1306::drawFastHLineInternal(int16_t x, int16_t y, int16_t w, + uint16_t color) { + + if ((y >= 0) && (y < HEIGHT)) { // Y coord in bounds? + if (x < 0) { // Clip left + w += x; + x = 0; + } + if ((x + w) > WIDTH) { // Clip right + w = (WIDTH - x); + } + if (w > 0) { // Proceed only if width is positive + uint8_t *pBuf = &buffer[(y / 8) * WIDTH + x], mask = 1 << (y & 7); + switch (color) { + case SSD1306_WHITE: + while (w--) { + *pBuf++ |= mask; + }; + break; + case SSD1306_BLACK: + mask = ~mask; + while (w--) { + *pBuf++ &= mask; + }; + break; + case SSD1306_INVERSE: + while (w--) { + *pBuf++ ^= mask; + }; + break; + } + } + } +} + +/*! + @brief Draw a vertical line. This is also invoked by the Adafruit_GFX + library in generating many higher-level graphics primitives. + @param x + Column of display -- 0 at left to (screen width -1) at right. + @param y + Topmost row -- 0 at top to (screen height - 1) at bottom. + @param h + Height of line, in pixels. + @param color + Line color, one of: SSD1306_BLACK, SSD1306_WHITE or SSD1306_INVERSE. + @return None (void). + @note Changes buffer contents only, no immediate effect on display. + Follow up with a call to display(), or with other graphics + commands as needed by one's own application. +*/ +void Adafruit_SSD1306::drawFastVLine(int16_t x, int16_t y, int16_t h, + uint16_t color) { + bool bSwap = false; + switch (rotation) { + case 1: + // 90 degree rotation, swap x & y for rotation, + // then invert x and adjust x for h (now to become w) + bSwap = true; + ssd1306_swap(x, y); + x = WIDTH - x - 1; + x -= (h - 1); + break; + case 2: + // 180 degree rotation, invert x and y, then shift y around for height. + x = WIDTH - x - 1; + y = HEIGHT - y - 1; + y -= (h - 1); + break; + case 3: + // 270 degree rotation, swap x & y for rotation, then invert y + bSwap = true; + ssd1306_swap(x, y); + y = HEIGHT - y - 1; + break; + } + + if (bSwap) + drawFastHLineInternal(x, y, h, color); + else + drawFastVLineInternal(x, y, h, color); +} + +/*! + @brief Draw a vertical line with a width and color. Used by public method + drawFastHLine,drawFastVLine + @param x + Leftmost column -- 0 at left to (screen width - 1) at right. + @param __y + Row of display -- 0 at top to (screen height -1) at bottom. + @param __h height of the line in pixels + @param color + Line color, one of: SSD1306_BLACK, SSD1306_WHITE or + SSD1306_INVERSE. + @return None (void). + @note Changes buffer contents only, no immediate effect on display. + Follow up with a call to display(), or with other graphics + commands as needed by one's own application. +*/ +void Adafruit_SSD1306::drawFastVLineInternal(int16_t x, int16_t __y, + int16_t __h, uint16_t color) { + + if ((x >= 0) && (x < WIDTH)) { // X coord in bounds? + if (__y < 0) { // Clip top + __h += __y; + __y = 0; + } + if ((__y + __h) > HEIGHT) { // Clip bottom + __h = (HEIGHT - __y); + } + if (__h > 0) { // Proceed only if height is now positive + // this display doesn't need ints for coordinates, + // use local byte registers for faster juggling + uint8_t y = __y, h = __h; + uint8_t *pBuf = &buffer[(y / 8) * WIDTH + x]; + + // do the first partial byte, if necessary - this requires some masking + uint8_t mod = (y & 7); + if (mod) { + // mask off the high n bits we want to set + mod = 8 - mod; + // note - lookup table results in a nearly 10% performance + // improvement in fill* functions + // uint8_t mask = ~(0xFF >> mod); + static const uint8_t PROGMEM premask[8] = {0x00, 0x80, 0xC0, 0xE0, + 0xF0, 0xF8, 0xFC, 0xFE}; + uint8_t mask = pgm_read_byte(&premask[mod]); + // adjust the mask if we're not going to reach the end of this byte + if (h < mod) + mask &= (0XFF >> (mod - h)); + + switch (color) { + case SSD1306_WHITE: + *pBuf |= mask; + break; + case SSD1306_BLACK: + *pBuf &= ~mask; + break; + case SSD1306_INVERSE: + *pBuf ^= mask; + break; + } + pBuf += WIDTH; + } + + if (h >= mod) { // More to go? + h -= mod; + // Write solid bytes while we can - effectively 8 rows at a time + if (h >= 8) { + if (color == SSD1306_INVERSE) { + // separate copy of the code so we don't impact performance of + // black/white write version with an extra comparison per loop + do { + *pBuf ^= 0xFF; // Invert byte + pBuf += WIDTH; // Advance pointer 8 rows + h -= 8; // Subtract 8 rows from height + } while (h >= 8); + } else { + // store a local value to work with + uint8_t val = (color != SSD1306_BLACK) ? 255 : 0; + do { + *pBuf = val; // Set byte + pBuf += WIDTH; // Advance pointer 8 rows + h -= 8; // Subtract 8 rows from height + } while (h >= 8); + } + } + + if (h) { // Do the final partial byte, if necessary + mod = h & 7; + // this time we want to mask the low bits of the byte, + // vs the high bits we did above + // uint8_t mask = (1 << mod) - 1; + // note - lookup table results in a nearly 10% performance + // improvement in fill* functions + static const uint8_t PROGMEM postmask[8] = {0x00, 0x01, 0x03, 0x07, + 0x0F, 0x1F, 0x3F, 0x7F}; + uint8_t mask = pgm_read_byte(&postmask[mod]); + switch (color) { + case SSD1306_WHITE: + *pBuf |= mask; + break; + case SSD1306_BLACK: + *pBuf &= ~mask; + break; + case SSD1306_INVERSE: + *pBuf ^= mask; + break; + } + } + } + } // endif positive height + } // endif x in bounds +} + +/*! + @brief Return color of a single pixel in display buffer. + @param x + Column of display -- 0 at left to (screen width - 1) at right. + @param y + Row of display -- 0 at top to (screen height -1) at bottom. + @return true if pixel is set (usually SSD1306_WHITE, unless display invert + mode is enabled), false if clear (SSD1306_BLACK). + @note Reads from buffer contents; may not reflect current contents of + screen if display() has not been called. +*/ +bool Adafruit_SSD1306::getPixel(int16_t x, int16_t y) { + if ((x >= 0) && (x < width()) && (y >= 0) && (y < height())) { + // Pixel is in-bounds. Rotate coordinates if needed. + switch (getRotation()) { + case 1: + ssd1306_swap(x, y); + x = WIDTH - x - 1; + break; + case 2: + x = WIDTH - x - 1; + y = HEIGHT - y - 1; + break; + case 3: + ssd1306_swap(x, y); + y = HEIGHT - y - 1; + break; + } + return (buffer[x + (y / 8) * WIDTH] & (1 << (y & 7))); + } + return false; // Pixel out of bounds +} + +/*! + @brief Get base address of display buffer for direct reading or writing. + @return Pointer to an unsigned 8-bit array, column-major, columns padded + to full byte boundary if needed. +*/ +uint8_t *Adafruit_SSD1306::getBuffer(void) { return buffer; } + +// REFRESH DISPLAY --------------------------------------------------------- + +/*! + @brief Push data currently in RAM to SSD1306 display. + @return None (void). + @note Drawing operations are not visible until this function is + called. Call after each graphics command, or after a whole set + of graphics commands, as best needed by one's own application. +*/ +void Adafruit_SSD1306::display(void) { + TRANSACTION_START + static const uint8_t dlist1[] = { + SSD1306_PAGEADDR, + 0, // Page start address + 0xFF, // Page end (not really, but works here) + SSD1306_COLUMNADDR, 0}; // Column start address + ssd1306_commandList(dlist1, sizeof(dlist1)); + ssd1306_command1(WIDTH - 1); // Column end address + +#if defined(ESP8266) + // ESP8266 needs a periodic yield() call to avoid watchdog reset. + // With the limited size of SSD1306 displays, and the fast bitrate + // being used (1 MHz or more), I think one yield() immediately before + // a screen write and one immediately after should cover it. But if + // not, if this becomes a problem, yields() might be added in the + // 32-byte transfer condition below. + yield(); +#endif + uint16_t count = WIDTH * ((HEIGHT + 7) / 8); + uint8_t *ptr = buffer; + if (wire) { // I2C + wire->beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x40); + uint16_t bytesOut = 1; + while (count--) { + if (bytesOut >= WIRE_MAX) { + wire->endTransmission(); + wire->beginTransmission(i2caddr); + WIRE_WRITE((uint8_t)0x40); + bytesOut = 1; + } + WIRE_WRITE(*ptr++); + bytesOut++; + } + wire->endTransmission(); + } else { // SPI + SSD1306_MODE_DATA + while (count--) + SPIwrite(*ptr++); + } + TRANSACTION_END +#if defined(ESP8266) + yield(); +#endif +} + +// SCROLLING FUNCTIONS ----------------------------------------------------- + +/*! + @brief Activate a right-handed scroll for all or part of the display. + @param start + First row. + @param stop + Last row. + @return None (void). +*/ +// To scroll the whole display, run: display.startscrollright(0x00, 0x0F) +void Adafruit_SSD1306::startscrollright(uint8_t start, uint8_t stop) { + TRANSACTION_START + static const uint8_t PROGMEM scrollList1a[] = { + SSD1306_RIGHT_HORIZONTAL_SCROLL, 0X00}; + ssd1306_commandList(scrollList1a, sizeof(scrollList1a)); + ssd1306_command1(start); + ssd1306_command1(0X00); + ssd1306_command1(stop); + static const uint8_t PROGMEM scrollList1b[] = {0X00, 0XFF, + SSD1306_ACTIVATE_SCROLL}; + ssd1306_commandList(scrollList1b, sizeof(scrollList1b)); + TRANSACTION_END +} + +/*! + @brief Activate a left-handed scroll for all or part of the display. + @param start + First row. + @param stop + Last row. + @return None (void). +*/ +// To scroll the whole display, run: display.startscrollleft(0x00, 0x0F) +void Adafruit_SSD1306::startscrollleft(uint8_t start, uint8_t stop) { + TRANSACTION_START + static const uint8_t PROGMEM scrollList2a[] = {SSD1306_LEFT_HORIZONTAL_SCROLL, + 0X00}; + ssd1306_commandList(scrollList2a, sizeof(scrollList2a)); + ssd1306_command1(start); + ssd1306_command1(0X00); + ssd1306_command1(stop); + static const uint8_t PROGMEM scrollList2b[] = {0X00, 0XFF, + SSD1306_ACTIVATE_SCROLL}; + ssd1306_commandList(scrollList2b, sizeof(scrollList2b)); + TRANSACTION_END +} + +/*! + @brief Activate a diagonal scroll for all or part of the display. + @param start + First row. + @param stop + Last row. + @return None (void). +*/ +// display.startscrolldiagright(0x00, 0x0F) +void Adafruit_SSD1306::startscrolldiagright(uint8_t start, uint8_t stop) { + TRANSACTION_START + static const uint8_t PROGMEM scrollList3a[] = { + SSD1306_SET_VERTICAL_SCROLL_AREA, 0X00}; + ssd1306_commandList(scrollList3a, sizeof(scrollList3a)); + ssd1306_command1(HEIGHT); + static const uint8_t PROGMEM scrollList3b[] = { + SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL, 0X00}; + ssd1306_commandList(scrollList3b, sizeof(scrollList3b)); + ssd1306_command1(start); + ssd1306_command1(0X00); + ssd1306_command1(stop); + static const uint8_t PROGMEM scrollList3c[] = {0X01, SSD1306_ACTIVATE_SCROLL}; + ssd1306_commandList(scrollList3c, sizeof(scrollList3c)); + TRANSACTION_END +} + +/*! + @brief Activate alternate diagonal scroll for all or part of the display. + @param start + First row. + @param stop + Last row. + @return None (void). +*/ +// To scroll the whole display, run: display.startscrolldiagleft(0x00, 0x0F) +void Adafruit_SSD1306::startscrolldiagleft(uint8_t start, uint8_t stop) { + TRANSACTION_START + static const uint8_t PROGMEM scrollList4a[] = { + SSD1306_SET_VERTICAL_SCROLL_AREA, 0X00}; + ssd1306_commandList(scrollList4a, sizeof(scrollList4a)); + ssd1306_command1(HEIGHT); + static const uint8_t PROGMEM scrollList4b[] = { + SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL, 0X00}; + ssd1306_commandList(scrollList4b, sizeof(scrollList4b)); + ssd1306_command1(start); + ssd1306_command1(0X00); + ssd1306_command1(stop); + static const uint8_t PROGMEM scrollList4c[] = {0X01, SSD1306_ACTIVATE_SCROLL}; + ssd1306_commandList(scrollList4c, sizeof(scrollList4c)); + TRANSACTION_END +} + +/*! + @brief Cease a previously-begun scrolling action. + @return None (void). +*/ +void Adafruit_SSD1306::stopscroll(void) { + TRANSACTION_START + ssd1306_command1(SSD1306_DEACTIVATE_SCROLL); + TRANSACTION_END +} + +// OTHER HARDWARE SETTINGS ------------------------------------------------- + +/*! + @brief Enable or disable display invert mode (white-on-black vs + black-on-white). + @param i + If true, switch to invert mode (black-on-white), else normal + mode (white-on-black). + @return None (void). + @note This has an immediate effect on the display, no need to call the + display() function -- buffer contents are not changed, rather a + different pixel mode of the display hardware is used. When + enabled, drawing SSD1306_BLACK (value 0) pixels will actually draw + white, SSD1306_WHITE (value 1) will draw black. +*/ +void Adafruit_SSD1306::invertDisplay(bool i) { + TRANSACTION_START + ssd1306_command1(i ? SSD1306_INVERTDISPLAY : SSD1306_NORMALDISPLAY); + TRANSACTION_END +} + +/*! + @brief Dim the display. + @param dim + true to enable lower brightness mode, false for full brightness. + @return None (void). + @note This has an immediate effect on the display, no need to call the + display() function -- buffer contents are not changed. +*/ +void Adafruit_SSD1306::dim(bool dim) { + // the range of contrast to too small to be really useful + // it is useful to dim the display + TRANSACTION_START + ssd1306_command1(SSD1306_SETCONTRAST); + ssd1306_command1(dim ? 0 : contrast); + TRANSACTION_END +} \ No newline at end of file diff --git a/Libraries/Adafruit_SSD1306.h b/Libraries/Adafruit_SSD1306.h new file mode 100644 index 0000000..78f4295 --- /dev/null +++ b/Libraries/Adafruit_SSD1306.h @@ -0,0 +1,167 @@ +/*! + * @file Adafruit_SSD1306.h + * + * This is part of for Adafruit's SSD1306 library for monochrome + * OLED displays: http://www.adafruit.com/category/63_98 + * + * These displays use I2C or SPI to communicate. I2C requires 2 pins + * (SCL+SDA) and optionally a RESET pin. SPI requires 4 pins (MOSI, SCK, + * select, data/command) and optionally a reset pin. Hardware SPI or + * 'bitbang' software SPI are both supported. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Written by Limor Fried/Ladyada for Adafruit Industries, with + * contributions from the open source community. + * + * BSD license, all text above, and the splash screen header file, + * must be included in any redistribution. + * + */ + +#ifndef _Adafruit_SSD1306_H_ +#define _Adafruit_SSD1306_H_ + +// ONE of the following three lines must be #defined: +//#define SSD1306_128_64 ///< DEPRECTAED: old way to specify 128x64 screen +#define SSD1306_128_32 ///< DEPRECATED: old way to specify 128x32 screen +//#define SSD1306_96_16 ///< DEPRECATED: old way to specify 96x16 screen +// This establishes the screen dimensions in old Adafruit_SSD1306 sketches +// (NEW CODE SHOULD IGNORE THIS, USE THE CONSTRUCTORS THAT ACCEPT WIDTH +// AND HEIGHT ARGUMENTS). + +// Uncomment to disable Adafruit splash logo +//#define SSD1306_NO_SPLASH +#include +#include + +/// The following "raw" color names are kept for backwards client compatability +/// They can be disabled by predefining this macro before including the Adafruit +/// header client code will then need to be modified to use the scoped enum +/// values directly +#ifndef NO_ADAFRUIT_SSD1306_COLOR_COMPATIBILITY +#define BLACK SSD1306_BLACK ///< Draw 'off' pixels +#define WHITE SSD1306_WHITE ///< Draw 'on' pixels +#define INVERSE SSD1306_INVERSE ///< Invert pixels +#endif +/// fit into the SSD1306_ naming scheme +#define SSD1306_BLACK 0 ///< Draw 'off' pixels +#define SSD1306_WHITE 1 ///< Draw 'on' pixels +#define SSD1306_INVERSE 2 ///< Invert pixels + +#define SSD1306_MEMORYMODE 0x20 ///< See datasheet +#define SSD1306_COLUMNADDR 0x21 ///< See datasheet +#define SSD1306_PAGEADDR 0x22 ///< See datasheet +#define SSD1306_SETCONTRAST 0x81 ///< See datasheet +#define SSD1306_CHARGEPUMP 0x8D ///< See datasheet +#define SSD1306_SEGREMAP 0xA0 ///< See datasheet +#define SSD1306_DISPLAYALLON_RESUME 0xA4 ///< See datasheet +#define SSD1306_DISPLAYALLON 0xA5 ///< Not currently used +#define SSD1306_NORMALDISPLAY 0xA6 ///< See datasheet +#define SSD1306_INVERTDISPLAY 0xA7 ///< See datasheet +#define SSD1306_SETMULTIPLEX 0xA8 ///< See datasheet +#define SSD1306_DISPLAYOFF 0xAE ///< See datasheet +#define SSD1306_DISPLAYON 0xAF ///< See datasheet +#define SSD1306_COMSCANINC 0xC0 ///< Not currently used +#define SSD1306_COMSCANDEC 0xC8 ///< See datasheet +#define SSD1306_SETDISPLAYOFFSET 0xD3 ///< See datasheet +#define SSD1306_SETDISPLAYCLOCKDIV 0xD5 ///< See datasheet +#define SSD1306_SETPRECHARGE 0xD9 ///< See datasheet +#define SSD1306_SETCOMPINS 0xDA ///< See datasheet +#define SSD1306_SETVCOMDETECT 0xDB ///< See datasheet + +#define SSD1306_SETLOWCOLUMN 0x00 ///< Not currently used +#define SSD1306_SETHIGHCOLUMN 0x10 ///< Not currently used +#define SSD1306_SETSTARTLINE 0x40 ///< See datasheet + +#define SSD1306_EXTERNALVCC 0x01 ///< External display voltage source +#define SSD1306_SWITCHCAPVCC 0x02 ///< Gen. display voltage from 3.3V + +#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 ///< Init rt scroll +#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 ///< Init left scroll +#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 ///< Init diag scroll +#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A ///< Init diag scroll +#define SSD1306_DEACTIVATE_SCROLL 0x2E ///< Stop scroll +#define SSD1306_ACTIVATE_SCROLL 0x2F ///< Start scroll +#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 ///< Set scroll range + +// Deprecated size stuff for backwards compatibility with old sketches +#if defined SSD1306_128_64 +#define SSD1306_LCDWIDTH 128 ///< DEPRECATED: width w/SSD1306_128_64 defined +#define SSD1306_LCDHEIGHT 64 ///< DEPRECATED: height w/SSD1306_128_64 defined +#endif +#if defined SSD1306_128_32 +#define SSD1306_LCDWIDTH 128 ///< DEPRECATED: width w/SSD1306_128_32 defined +#define SSD1306_LCDHEIGHT 32 ///< DEPRECATED: height w/SSD1306_128_32 defined +#endif +#if defined SSD1306_96_16 +#define SSD1306_LCDWIDTH 96 ///< DEPRECATED: width w/SSD1306_96_16 defined +#define SSD1306_LCDHEIGHT 16 ///< DEPRECATED: height w/SSD1306_96_16 defined +#endif + +/*! + @brief Class that stores state and functions for interacting with + SSD1306 OLED displays. +*/ +class Adafruit_SSD1306 : public Adafruit_GFX { +public: + // NEW CONSTRUCTORS -- recommended for new projects + Adafruit_SSD1306(uint8_t w, uint8_t h, TwoWire *twi = &Wire, + int8_t rst_pin = -1, uint32_t clkDuring = 400000UL, + uint32_t clkAfter = 100000UL); + Adafruit_SSD1306(uint8_t w, uint8_t h, int8_t mosi_pin, int8_t sclk_pin, + int8_t dc_pin, int8_t rst_pin, int8_t cs_pin); + Adafruit_SSD1306(uint8_t w, uint8_t h, SPIClass *spi, int8_t dc_pin, + int8_t rst_pin, int8_t cs_pin, uint32_t bitrate = 8000000UL); + + ~Adafruit_SSD1306(void); + + bool begin(uint8_t switchvcc = SSD1306_SWITCHCAPVCC, uint8_t i2caddr = 0, + bool reset = true, bool periphBegin = true); + void display(void); + void clearDisplay(void); + void invertDisplay(bool i); + void dim(bool dim); + void drawPixel(int16_t x, int16_t y, uint16_t color); + virtual void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color); + virtual void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color); + void startscrollright(uint8_t start, uint8_t stop); + void startscrollleft(uint8_t start, uint8_t stop); + void startscrolldiagright(uint8_t start, uint8_t stop); + void startscrolldiagleft(uint8_t start, uint8_t stop); + void stopscroll(void); + void ssd1306_command(uint8_t c); + bool getPixel(int16_t x, int16_t y); + uint8_t *getBuffer(void); + +protected: + void drawFastHLineInternal(int16_t x, int16_t y, int16_t w, uint16_t color); + void drawFastVLineInternal(int16_t x, int16_t y, int16_t h, uint16_t color); + void ssd1306_command1(uint8_t c); + void ssd1306_commandList(const uint8_t *c, uint8_t n); + + SPIClass *spi; ///< Initialized during construction when using SPI. See + ///< SPI.cpp, SPI.h + TwoWire *wire; ///< Initialized during construction when using I2C. See + ///< Wire.cpp, Wire.h + uint8_t *buffer; ///< Buffer data used for display buffer. Allocated when + ///< begin method is called. + int8_t i2caddr; ///< I2C address initialized when begin method is called. + int8_t vccstate; ///< VCC selection, set by begin method. + int8_t page_end; ///< not used + int8_t mosiPin; ///< (Master Out Slave In) set when using SPI set during + ///< construction. + int8_t clkPin; ///< (Clock Pin) set when using SPI set during construction. + int8_t dcPin; ///< (Data Pin) set when using SPI set during construction. + int8_t + csPin; ///< (Chip Select Pin) set when using SPI set during construction. + int8_t rstPin; ///< Display reset pin assignment. Set during construction. + + uint32_t wireClk; ///< Wire speed for SSD1306 transfers + uint32_t restoreClk; ///< Wire speed following SSD1306 transfers + uint8_t contrast; ///< normal contrast setting for this device +}; + +endif // _Adafruit_SSD1306_H_ \ No newline at end of file diff --git a/Libraries/NetworkClient.cpp b/Libraries/NetworkClient.cpp new file mode 100644 index 0000000..0782b74 --- /dev/null +++ b/Libraries/NetworkClient.cpp @@ -0,0 +1,680 @@ +/* + Client.h - Client class for Raspberry Pi + Copyright (c) 2016 Hristo Gochkov All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "NetworkClient.h" +#include "NetworkManager.h" +#include +#include +#include + +#define IN6_IS_ADDR_V4MAPPED(a) ((((__const uint32_t *)(a))[0] == 0) && (((__const uint32_t *)(a))[1] == 0) && (((__const uint32_t *)(a))[2] == htonl(0xffff))) + +#define WIFI_CLIENT_DEF_CONN_TIMEOUT_MS (3000) +#define WIFI_CLIENT_MAX_WRITE_RETRY (10) +#define WIFI_CLIENT_SELECT_TIMEOUT_US (1000000) +#define WIFI_CLIENT_FLUSH_BUFFER_SIZE (1024) + +#undef connect +#undef write +#undef read + +class NetworkClientRxBuffer { +private: + size_t _size; + uint8_t *_buffer; + size_t _pos; + size_t _fill; + int _fd; + bool _failed; + + size_t r_available() { + if (_fd < 0) { + return 0; + } + int count; +#ifdef ESP_IDF_VERSION_MAJOR + int res = lwip_ioctl(_fd, FIONREAD, &count); +#else + int res = lwip_ioctl_r(_fd, FIONREAD, &count); +#endif + if (res < 0) { + _failed = true; + return 0; + } + return count; + } + + size_t fillBuffer() { + if (!_buffer) { + _buffer = (uint8_t *)malloc(_size); + if (!_buffer) { + log_e("Not enough memory to allocate buffer"); + _failed = true; + return 0; + } + } + if (_fill && _pos == _fill) { + _fill = 0; + _pos = 0; + } + if (!_buffer || _size <= _fill || !r_available()) { + return 0; + } + int res = recv(_fd, _buffer + _fill, _size - _fill, MSG_DONTWAIT); + if (res < 0) { + if (errno != EWOULDBLOCK) { + _failed = true; + } + return 0; + } + _fill += res; + return res; + } + +public: + NetworkClientRxBuffer(int fd, size_t size = 1436) : _size(size), _buffer(NULL), _pos(0), _fill(0), _fd(fd), _failed(false) { + //_buffer = (uint8_t *)malloc(_size); + } + + ~NetworkClientRxBuffer() { + free(_buffer); + } + + bool failed() { + return _failed; + } + + int read(uint8_t *dst, size_t len) { + if (!dst || !len || (_pos == _fill && !fillBuffer())) { + return _failed ? -1 : 0; + } + size_t a = _fill - _pos; + if (len <= a || ((len - a) <= (_size - _fill) && fillBuffer() >= (len - a))) { + if (len == 1) { + *dst = _buffer[_pos]; + } else { + memcpy(dst, _buffer + _pos, len); + } + _pos += len; + return len; + } + size_t left = len; + size_t toRead = a; + uint8_t *buf = dst; + memcpy(buf, _buffer + _pos, toRead); + _pos += toRead; + left -= toRead; + buf += toRead; + while (left) { + if (!fillBuffer()) { + return len - left; + } + a = _fill - _pos; + toRead = (a > left) ? left : a; + memcpy(buf, _buffer + _pos, toRead); + _pos += toRead; + left -= toRead; + buf += toRead; + } + return len; + } + + int peek() { + if (_pos == _fill && !fillBuffer()) { + return -1; + } + return _buffer[_pos]; + } + + size_t available() { + return _fill - _pos + r_available(); + } + + void clear() { + if (r_available()) { + _pos = _fill; + while (fillBuffer()) { + _pos = _fill; + } + } + _pos = 0; + _fill = 0; + } +}; + +class NetworkClientSocketHandle { +private: + int sockfd; + +public: + NetworkClientSocketHandle(int fd) : sockfd(fd) {} + + ~NetworkClientSocketHandle() { + close(); + } + + void close() { + if (sockfd >= 0) { + ::close(sockfd); + sockfd = -1; + } + } + + int fd() { + return sockfd; + } +}; + +NetworkClient::NetworkClient() : _rxBuffer(nullptr), _connected(false), _sse(false), _timeout(WIFI_CLIENT_DEF_CONN_TIMEOUT_MS), next(NULL) {} + +NetworkClient::NetworkClient(int fd) : _connected(true), _timeout(WIFI_CLIENT_DEF_CONN_TIMEOUT_MS), next(NULL) { + clientSocketHandle.reset(new NetworkClientSocketHandle(fd)); + _rxBuffer.reset(new NetworkClientRxBuffer(fd)); +} + +NetworkClient::~NetworkClient() {} + +void NetworkClient::stop() { + if (clientSocketHandle) { + clientSocketHandle->close(); + } + clientSocketHandle = NULL; + _rxBuffer = NULL; + _connected = false; + _lastReadTimeout = 0; + _lastWriteTimeout = 0; +} + +int NetworkClient::connect(IPAddress ip, uint16_t port) { + return connect(ip, port, _timeout); +} + +int NetworkClient::connect(IPAddress ip, uint16_t port, int32_t timeout_ms) { + struct sockaddr_storage serveraddr = {}; + _timeout = timeout_ms; + int sockfd = -1; + + if (ip.type() == IPv6) { + struct sockaddr_in6 *tmpaddr = (struct sockaddr_in6 *)&serveraddr; + sockfd = socket(AF_INET6, SOCK_STREAM, 0); + tmpaddr->sin6_family = AF_INET6; + memcpy(tmpaddr->sin6_addr.un.u8_addr, &ip[0], 16); + tmpaddr->sin6_port = htons(port); + tmpaddr->sin6_scope_id = ip.zone(); + } else { + struct sockaddr_in *tmpaddr = (struct sockaddr_in *)&serveraddr; + sockfd = socket(AF_INET, SOCK_STREAM, 0); + tmpaddr->sin_family = AF_INET; + tmpaddr->sin_addr.s_addr = ip; + tmpaddr->sin_port = htons(port); + } + if (sockfd < 0) { + log_e("socket: %d", errno); + return 0; + } + fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK); + + fd_set fdset; + struct timeval tv; + FD_ZERO(&fdset); + FD_SET(sockfd, &fdset); + tv.tv_sec = _timeout / 1000; + tv.tv_usec = (_timeout % 1000) * 1000; + +#ifdef ESP_IDF_VERSION_MAJOR + int res = lwip_connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); +#else + int res = lwip_connect_r(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); +#endif + if (res < 0 && errno != EINPROGRESS) { + log_e("connect on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno)); + close(sockfd); + return 0; + } + + res = select(sockfd + 1, nullptr, &fdset, nullptr, _timeout < 0 ? nullptr : &tv); + if (res < 0) { + log_e("select on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno)); + close(sockfd); + return 0; + } else if (res == 0) { + log_i("select returned due to timeout %d ms for fd %d", _timeout, sockfd); + close(sockfd); + return 0; + } else { + int sockerr; + socklen_t len = (socklen_t)sizeof(int); + res = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &sockerr, &len); + + if (res < 0) { + log_e("getsockopt on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno)); + close(sockfd); + return 0; + } + + if (sockerr != 0) { + log_e("socket error on fd %d, errno: %d, \"%s\"", sockfd, sockerr, strerror(sockerr)); + close(sockfd); + return 0; + } + } + +#define ROE_WIFICLIENT(x, msg) \ + { \ + if (((x) < 0)) { \ + log_e("Setsockopt '" msg "'' on fd %d failed. errno: %d, \"%s\"", sockfd, errno, strerror(errno)); \ + return 0; \ + } \ + } + ROE_WIFICLIENT(setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)), "SO_SNDTIMEO"); + ROE_WIFICLIENT(setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)), "SO_RCVTIMEO"); + + // These are also set in NetworkClientSecure, should be set here too? + //ROE_WIFICLIENT(setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(enable)),"TCP_NODELAY"); + //ROE_WIFICLIENT (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &enable, sizeof(enable)),"SO_KEEPALIVE"); + + fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) & (~O_NONBLOCK)); + clientSocketHandle.reset(new NetworkClientSocketHandle(sockfd)); + _rxBuffer.reset(new NetworkClientRxBuffer(sockfd)); + + _connected = true; + return 1; +} + +int NetworkClient::connect(const char *host, uint16_t port) { + return connect(host, port, _timeout); +} + +int NetworkClient::connect(const char *host, uint16_t port, int32_t timeout_ms) { + IPAddress srv((uint32_t)0); + if (!Network.hostByName(host, srv)) { + return 0; + } + return connect(srv, port, timeout_ms); +} + +int NetworkClient::setSocketOption(int option, char *value, size_t len) { + return setSocketOption(SOL_SOCKET, option, (const void *)value, len); +} + +int NetworkClient::setSocketOption(int level, int option, const void *value, size_t len) { + int res = setsockopt(fd(), level, option, value, len); + if (res < 0) { + log_e("fail on %d, errno: %d, \"%s\"", fd(), errno, strerror(errno)); + } + return res; +} + +int NetworkClient::getSocketOption(int level, int option, const void *value, size_t size) { + int res = getsockopt(fd(), level, option, (char *)value, (socklen_t *)&size); + if (res < 0) { + log_e("fail on fd %d, errno: %d, \"%s\"", fd(), errno, strerror(errno)); + } + return res; +} + +int NetworkClient::setOption(int option, int *value) { + return setSocketOption(IPPROTO_TCP, option, (const void *)value, sizeof(int)); +} + +int NetworkClient::getOption(int option, int *value) { + socklen_t size = sizeof(int); + int res = getsockopt(fd(), IPPROTO_TCP, option, (char *)value, &size); + if (res < 0) { + log_e("fail on fd %d, errno: %d, \"%s\"", fd(), errno, strerror(errno)); + } + return res; +} + +void NetworkClient::setConnectionTimeout(uint32_t milliseconds) { + _timeout = milliseconds; +} + +int NetworkClient::setNoDelay(bool nodelay) { + int flag = nodelay; + return setOption(TCP_NODELAY, &flag); +} + +bool NetworkClient::getNoDelay() { + int flag = 0; + getOption(TCP_NODELAY, &flag); + return flag; +} + +size_t NetworkClient::write(uint8_t data) { + return write(&data, 1); +} + +int NetworkClient::read() { + uint8_t data = 0; + int res = read(&data, 1); + if (res < 0) { + return res; + } + if (res == 0) { // No data available. + return -1; + } + return data; +} + +void NetworkClient::flush() { + clear(); +} + +size_t NetworkClient::write(const uint8_t *buf, size_t size) { + int res = 0; + int retry = WIFI_CLIENT_MAX_WRITE_RETRY; + int socketFileDescriptor = fd(); + size_t totalBytesSent = 0; + size_t bytesRemaining = size; + + if (!_connected || (socketFileDescriptor < 0)) { + return 0; + } + + while (retry) { + //use select to make sure the socket is ready for writing + fd_set set; + struct timeval tv; + FD_ZERO(&set); // empties the set + FD_SET(socketFileDescriptor, &set); // adds FD to the set + tv.tv_sec = 0; + tv.tv_usec = WIFI_CLIENT_SELECT_TIMEOUT_US; + retry--; + + if (_lastWriteTimeout != _timeout) { + if (fd() >= 0) { + struct timeval timeout_tv; + timeout_tv.tv_sec = _timeout / 1000; + timeout_tv.tv_usec = (_timeout % 1000) * 1000; + if (setSocketOption(SO_SNDTIMEO, (char *)&timeout_tv, sizeof(struct timeval)) >= 0) { + _lastWriteTimeout = _timeout; + } + } + } + + if (select(socketFileDescriptor + 1, NULL, &set, NULL, &tv) < 0) { + return 0; + } + + if (FD_ISSET(socketFileDescriptor, &set)) { + res = send(socketFileDescriptor, (void *)buf, bytesRemaining, MSG_DONTWAIT); + if (res > 0) { + totalBytesSent += res; + if (totalBytesSent >= size) { + //completed successfully + retry = 0; + } else { + buf += res; + bytesRemaining -= res; + retry = WIFI_CLIENT_MAX_WRITE_RETRY; + } + } else if (res < 0) { + log_e("fail on fd %d, errno: %d, \"%s\"", fd(), errno, strerror(errno)); + if (errno != EAGAIN) { + //if resource was busy, can try again, otherwise give up + stop(); + res = 0; + retry = 0; + } + } else { + // Try again + } + } + } + return totalBytesSent; +} + +size_t NetworkClient::write_P(PGM_P buf, size_t size) { + return write(buf, size); +} + +size_t NetworkClient::write(Stream &stream) { + uint8_t *buf = (uint8_t *)malloc(1360); + if (!buf) { + return 0; + } + size_t toRead = 0, toWrite = 0, written = 0; + size_t available = stream.available(); + while (available) { + toRead = (available > 1360) ? 1360 : available; + toWrite = stream.readBytes(buf, toRead); + written += write(buf, toWrite); + available = stream.available(); + } + free(buf); + return written; +} + +int NetworkClient::read(uint8_t *buf, size_t size) { + if (_lastReadTimeout != _timeout) { + if (fd() >= 0) { + struct timeval timeout_tv; + timeout_tv.tv_sec = _timeout / 1000; + timeout_tv.tv_usec = (_timeout % 1000) * 1000; + if (setSocketOption(SO_RCVTIMEO, (char *)&timeout_tv, sizeof(struct timeval)) >= 0) { + _lastReadTimeout = _timeout; + } + } + } + + int res = -1; + if (_rxBuffer) { + res = _rxBuffer->read(buf, size); + if (_rxBuffer->failed()) { + log_e("fail on fd %d, errno: %d, \"%s\"", fd(), errno, strerror(errno)); + stop(); + } + } + return res; +} + +size_t NetworkClient::readBytes(char *buffer, size_t length) { + size_t left = length, sofar = 0; + int r = 0, to = millis() + getTimeout(); + while (left) { + r = read((uint8_t *)buffer + sofar, left); + if (r < 0) { + // Error has occurred + break; + } + if (r > 0) { + // We got some data + left -= r; + sofar += r; + to = millis() + getTimeout(); + } else { + // We got no data + if (millis() >= to) { + // We have waited for data enough + log_w("Timeout waiting for data on fd %d", fd()); + break; + } + // Allow other tasks to run + delay(2); + } + } + return sofar; +} + +int NetworkClient::peek() { + int res = -1; + if (fd() >= 0 && _rxBuffer) { + res = _rxBuffer->peek(); + if (_rxBuffer->failed()) { + log_e("fail on fd %d, errno: %d, \"%s\"", fd(), errno, strerror(errno)); + stop(); + } + } + return res; +} + +int NetworkClient::available() { + if (fd() < 0 || !_rxBuffer) { + return 0; + } + int res = _rxBuffer->available(); + if (_rxBuffer->failed()) { + log_e("fail on fd %d, errno: %d, \"%s\"", fd(), errno, strerror(errno)); + stop(); + } + return res; +} + +void NetworkClient::clear() { + if (_rxBuffer != nullptr) { + _rxBuffer->clear(); + } +} + +uint8_t NetworkClient::connected() { + if (fd() == -1 && _connected) { + stop(); + } + if (_connected) { + uint8_t dummy; + int res = recv(fd(), &dummy, 0, MSG_DONTWAIT); + // avoid unused var warning by gcc + (void)res; + // recv only sets errno if res is <= 0 + if (res <= 0) { + switch (errno) { + case EWOULDBLOCK: + case ENOENT: //caused by vfs + _connected = true; + break; + case ENOTCONN: + case EPIPE: + case ECONNRESET: + case ECONNREFUSED: + case ECONNABORTED: + _connected = false; + log_d("Disconnected: RES: %d, ERR: %d", res, errno); + break; + default: + log_i("Unexpected: RES: %d, ERR: %d", res, errno); + _connected = true; + break; + } + } else { + _connected = true; + } + } + return _connected; +} + +IPAddress NetworkClient::remoteIP(int fd) const { + struct sockaddr_storage addr; + socklen_t len = sizeof addr; + getpeername(fd, (struct sockaddr *)&addr, &len); + + // IPv4 socket, old way + if (((struct sockaddr *)&addr)->sa_family == AF_INET) { + struct sockaddr_in *s = (struct sockaddr_in *)&addr; + return IPAddress((uint32_t)(s->sin_addr.s_addr)); + } + + // IPv6, but it might be IPv4 mapped address + if (((struct sockaddr *)&addr)->sa_family == AF_INET6) { + struct sockaddr_in6 *saddr6 = (struct sockaddr_in6 *)&addr; + if (IN6_IS_ADDR_V4MAPPED(saddr6->sin6_addr.un.u32_addr)) { + return IPAddress(IPv4, (uint8_t *)saddr6->sin6_addr.s6_addr + IPADDRESS_V4_BYTES_INDEX); + } else { + return IPAddress(IPv6, (uint8_t *)(saddr6->sin6_addr.s6_addr), saddr6->sin6_scope_id); + } + } + log_e("NetworkClient::remoteIP Not AF_INET or AF_INET6?"); + return (IPAddress(0, 0, 0, 0)); +} + +uint16_t NetworkClient::remotePort(int fd) const { + struct sockaddr_storage addr; + socklen_t len = sizeof addr; + getpeername(fd, (struct sockaddr *)&addr, &len); + struct sockaddr_in *s = (struct sockaddr_in *)&addr; + return ntohs(s->sin_port); +} + +IPAddress NetworkClient::remoteIP() const { + return remoteIP(fd()); +} + +uint16_t NetworkClient::remotePort() const { + return remotePort(fd()); +} + +IPAddress NetworkClient::localIP(int fd) const { + struct sockaddr_storage addr; + socklen_t len = sizeof addr; + getsockname(fd, (struct sockaddr *)&addr, &len); + + // IPv4 socket, old way + if (((struct sockaddr *)&addr)->sa_family == AF_INET) { + struct sockaddr_in *s = (struct sockaddr_in *)&addr; + return IPAddress((uint32_t)(s->sin_addr.s_addr)); + } + + // IPv6, but it might be IPv4 mapped address + if (((struct sockaddr *)&addr)->sa_family == AF_INET6) { + struct sockaddr_in6 *saddr6 = (struct sockaddr_in6 *)&addr; + if (IN6_IS_ADDR_V4MAPPED(saddr6->sin6_addr.un.u32_addr)) { + return IPAddress(IPv4, (uint8_t *)saddr6->sin6_addr.s6_addr + IPADDRESS_V4_BYTES_INDEX); + } else { + return IPAddress(IPv6, (uint8_t *)(saddr6->sin6_addr.s6_addr), saddr6->sin6_scope_id); + } + } + log_e("NetworkClient::localIP Not AF_INET or AF_INET6?"); + return (IPAddress(0, 0, 0, 0)); +} + +uint16_t NetworkClient::localPort(int fd) const { + struct sockaddr_storage addr; + socklen_t len = sizeof addr; + getsockname(fd, (struct sockaddr *)&addr, &len); + struct sockaddr_in *s = (struct sockaddr_in *)&addr; + return ntohs(s->sin_port); +} + +IPAddress NetworkClient::localIP() const { + return localIP(fd()); +} + +uint16_t NetworkClient::localPort() const { + return localPort(fd()); +} + +bool NetworkClient::operator==(const NetworkClient &rhs) { + return clientSocketHandle == rhs.clientSocketHandle && remotePort() == rhs.remotePort() && remoteIP() == rhs.remoteIP(); +} + +int NetworkClient::fd() const { + if (clientSocketHandle == NULL) { + return -1; + } else { + return clientSocketHandle->fd(); + } +} + +void NetworkClient::setSSE(bool sse) { + _sse = sse; +} + +bool NetworkClient::isSSE() { + return _sse; +} diff --git a/Libraries/NetworkClient.h b/Libraries/NetworkClient.h new file mode 100644 index 0000000..bdf857e --- /dev/null +++ b/Libraries/NetworkClient.h @@ -0,0 +1,111 @@ +/* + Client.h - Base class that provides Client + Copyright (c) 2011 Adrian McEwen. All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "Arduino.h" +#include "Client.h" +#include + +class NetworkClientSocketHandle; +class NetworkClientRxBuffer; + +class ESPLwIPClient : public Client { +public: + virtual int connect(IPAddress ip, uint16_t port, int32_t timeout) = 0; + virtual int connect(const char *host, uint16_t port, int32_t timeout) = 0; + virtual void setConnectionTimeout(uint32_t milliseconds) = 0; +}; + +class NetworkClient : public ESPLwIPClient { +protected: + std::shared_ptr clientSocketHandle; + std::shared_ptr _rxBuffer; + bool _connected; + bool _sse; + int _timeout; + int _lastWriteTimeout; + int _lastReadTimeout; + +public: + NetworkClient *next; + NetworkClient(); + NetworkClient(int fd); + ~NetworkClient(); + int connect(IPAddress ip, uint16_t port); + int connect(IPAddress ip, uint16_t port, int32_t timeout_ms); + int connect(const char *host, uint16_t port); + int connect(const char *host, uint16_t port, int32_t timeout_ms); + size_t write(uint8_t data); + size_t write(const uint8_t *buf, size_t size); + size_t write_P(PGM_P buf, size_t size); + size_t write(Stream &stream); + [[deprecated("Use clear() instead.")]] + void flush(); // Print::flush tx + int available(); + int read(); + int read(uint8_t *buf, size_t size); + size_t readBytes(char *buffer, size_t length); + size_t readBytes(uint8_t *buffer, size_t length) { + return readBytes((char *)buffer, length); + } + int peek(); + void clear(); // clear rx + void stop(); + uint8_t connected(); + void setSSE(bool sse); + bool isSSE(); + + operator bool() { + return connected(); + } + bool operator==(const bool value) { + return bool() == value; + } + bool operator!=(const bool value) { + return bool() != value; + } + bool operator==(const NetworkClient &); + bool operator!=(const NetworkClient &rhs) { + return !this->operator==(rhs); + }; + + virtual int fd() const; + + int setSocketOption(int option, char *value, size_t len); + int setSocketOption(int level, int option, const void *value, size_t len); + int getSocketOption(int level, int option, const void *value, size_t size); + int setOption(int option, int *value); + int getOption(int option, int *value); + void setConnectionTimeout(uint32_t milliseconds); + int setNoDelay(bool nodelay); + bool getNoDelay(); + + IPAddress remoteIP() const; + IPAddress remoteIP(int fd) const; + uint16_t remotePort() const; + uint16_t remotePort(int fd) const; + IPAddress localIP() const; + IPAddress localIP(int fd) const; + uint16_t localPort() const; + uint16_t localPort(int fd) const; + + //friend class NetworkServer; + using Print::write; +}; diff --git a/Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.cpp b/Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.cpp new file mode 100644 index 0000000..17501c3 --- /dev/null +++ b/Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.cpp @@ -0,0 +1,828 @@ +/* + * Copyright 2020-2024 Ryan Powell and + * esp-nimble-cpp, NimBLE-Arduino contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "nimconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) + +# include "NimBLEDevice.h" +# include "NimBLEAdvertisedDevice.h" +# include "NimBLEUtils.h" +# include "NimBLELog.h" + +# include + +static const char* LOG_TAG = "NimBLEAdvertisedDevice"; + +/** + * @brief Constructor + * @param [in] event The advertisement event data. + */ +NimBLEAdvertisedDevice::NimBLEAdvertisedDevice(const ble_gap_event* event, uint8_t eventType) +# if CONFIG_BT_NIMBLE_EXT_ADV + : m_address{event->ext_disc.addr}, + m_advType{eventType}, + m_rssi{event->ext_disc.rssi}, + m_callbackSent{0}, + m_advLength{event->ext_disc.length_data}, + m_isLegacyAdv{!!(event->ext_disc.props & BLE_HCI_ADV_LEGACY_MASK)}, + m_sid{event->ext_disc.sid}, + m_primPhy{event->ext_disc.prim_phy}, + m_secPhy{event->ext_disc.sec_phy}, + m_periodicItvl{event->ext_disc.periodic_adv_itvl}, + m_payload(event->ext_disc.data, event->ext_disc.data + event->ext_disc.length_data) { +# else + : m_address{event->disc.addr}, + m_advType{eventType}, + m_rssi{event->disc.rssi}, + m_callbackSent{0}, + m_advLength{event->disc.length_data}, + m_payload(event->disc.data, event->disc.data + event->disc.length_data) { +# endif + m_payload.reserve(256); +} // NimBLEAdvertisedDevice + +/** + * @brief Update the advertisement data. + * @param [in] event The advertisement event data. + */ +void NimBLEAdvertisedDevice::update(const ble_gap_event* event, uint8_t eventType) { +# if CONFIG_BT_NIMBLE_EXT_ADV + const auto& disc = event->ext_disc; + m_isLegacyAdv = disc.props & BLE_HCI_ADV_LEGACY_MASK; +# else + const auto& disc = event->disc; +# endif + + m_rssi = disc.rssi; + if (eventType == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP && isLegacyAdvertisement()) { + m_payload.insert(m_payload.end(), disc.data, disc.data + disc.length_data); + return; + } + m_advLength = disc.length_data; + m_payload.assign(disc.data, disc.data + disc.length_data); + m_callbackSent = 0; // new data, reset callback sent flag +} // update + +/** + * @brief Get the address of the advertising device. + * @return The address of the advertised device. + */ +const NimBLEAddress& NimBLEAdvertisedDevice::getAddress() const { + return m_address; +} // getAddress + +/** + * @brief Get the advertisement type. + * @return The advertising type the device is reporting: + * * BLE_HCI_ADV_TYPE_ADV_IND (0) - indirect advertising + * * BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_HD (1) - direct advertising - high duty cycle + * * BLE_HCI_ADV_TYPE_ADV_SCAN_IND (2) - indirect scan response + * * BLE_HCI_ADV_TYPE_ADV_NONCONN_IND (3) - indirect advertising - not connectable + * * BLE_HCI_ADV_TYPE_ADV_DIRECT_IND_LD (4) - direct advertising - low duty cycle + */ +uint8_t NimBLEAdvertisedDevice::getAdvType() const { + return m_advType; +} // getAdvType + +/** + * @brief Get the advertisement flags. + * @return The advertisement flags, a bitmask of: + * BLE_HS_ADV_F_DISC_LTD (0x01) - limited discoverability + * BLE_HS_ADV_F_DISC_GEN (0x02) - general discoverability + * BLE_HS_ADV_F_BREDR_UNSUP - BR/EDR not supported + */ +uint8_t NimBLEAdvertisedDevice::getAdvFlags() const { + size_t data_loc; + if (findAdvField(BLE_HS_ADV_TYPE_FLAGS, 0, &data_loc) > 0) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length == BLE_HS_ADV_FLAGS_LEN + 1) { + return *field->value; + } + } + + return 0; +} // getAdvFlags + +/** + * @brief Get the appearance. + * + * A %BLE device can declare its own appearance. The appearance is how it would like to be shown to an end user + * typically in the form of an icon. + * + * @return The appearance of the advertised device. + */ +uint16_t NimBLEAdvertisedDevice::getAppearance() const { + size_t data_loc; + if (findAdvField(BLE_HS_ADV_TYPE_APPEARANCE, 0, &data_loc) > 0) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length == BLE_HS_ADV_APPEARANCE_LEN + 1) { + return *field->value | *(field->value + 1) << 8; + } + } + + return 0; +} // getAppearance + +/** + * @brief Get the advertisement interval. + * @return The advertisement interval in 0.625ms units. + */ +uint16_t NimBLEAdvertisedDevice::getAdvInterval() const { + size_t data_loc; + if (findAdvField(BLE_HS_ADV_TYPE_ADV_ITVL, 0, &data_loc) > 0) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length == BLE_HS_ADV_ADV_ITVL_LEN + 1) { + return *field->value | *(field->value + 1) << 8; + } + } + + return 0; +} // getAdvInterval + +/** + * @brief Get the preferred min connection interval. + * @return The preferred min connection interval in 1.25ms units. + */ +uint16_t NimBLEAdvertisedDevice::getMinInterval() const { + size_t data_loc; + if (findAdvField(BLE_HS_ADV_TYPE_SLAVE_ITVL_RANGE, 0, &data_loc) > 0) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length == BLE_HS_ADV_SLAVE_ITVL_RANGE_LEN + 1) { + return *field->value | *(field->value + 1) << 8; + } + } + + return 0; +} // getMinInterval + +/** + * @brief Get the preferred max connection interval. + * @return The preferred max connection interval in 1.25ms units. + */ +uint16_t NimBLEAdvertisedDevice::getMaxInterval() const { + size_t data_loc; + if (findAdvField(BLE_HS_ADV_TYPE_SLAVE_ITVL_RANGE, 0, &data_loc) > 0) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length == BLE_HS_ADV_SLAVE_ITVL_RANGE_LEN + 1) { + return *(field->value + 2) | *(field->value + 3) << 8; + } + } + + return 0; +} // getMaxInterval + +/** + * @brief Get the manufacturer data. + * @param [in] index The index of the of the manufacturer data set to get. + * @return The manufacturer data. + */ +std::string NimBLEAdvertisedDevice::getManufacturerData(uint8_t index) const { + return getPayloadByType(BLE_HS_ADV_TYPE_MFG_DATA, index); +} // getManufacturerData + +/** + * @brief Get the count of manufacturer data sets. + * @return The number of manufacturer data sets. + */ +uint8_t NimBLEAdvertisedDevice::getManufacturerDataCount() const { + return findAdvField(BLE_HS_ADV_TYPE_MFG_DATA); +} // getManufacturerDataCount + +/** + * @brief Get the URI from the advertisement. + * @return The URI data. + */ +std::string NimBLEAdvertisedDevice::getURI() const { + return getPayloadByType(BLE_HS_ADV_TYPE_URI); +} // getURI + +/** + * @brief Get the data from any type available in the advertisement. + * @param [in] type The advertised data type BLE_HS_ADV_TYPE. + * @param [in] index The index of the data type. + * @return The data available under the type `type`. + */ +std::string NimBLEAdvertisedDevice::getPayloadByType(uint16_t type, uint8_t index) const { + size_t data_loc; + if (findAdvField(type, index, &data_loc) > 0) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length > 1) { + return std::string((char*)field->value, field->length - 1); + } + } + + return ""; +} // getPayloadByType + +/** + * @brief Get the advertised name. + * @return The name of the advertised device. + */ +std::string NimBLEAdvertisedDevice::getName() const { + return getPayloadByType(BLE_HS_ADV_TYPE_COMP_NAME); +} // getName + +/** + * @brief Get the RSSI. + * @return The RSSI of the advertised device. + */ +int8_t NimBLEAdvertisedDevice::getRSSI() const { + return m_rssi; +} // getRSSI + +/** + * @brief Get the scan object that created this advertised device. + * @return The scan object. + */ +NimBLEScan* NimBLEAdvertisedDevice::getScan() const { + return NimBLEDevice::getScan(); +} // getScan + +/** + * @brief Get the number of target addresses. + * @return The number of addresses. + */ +uint8_t NimBLEAdvertisedDevice::getTargetAddressCount() const { + uint8_t count = findAdvField(BLE_HS_ADV_TYPE_PUBLIC_TGT_ADDR); + count += findAdvField(BLE_HS_ADV_TYPE_RANDOM_TGT_ADDR); + + return count; +} + +/** + * @brief Get the target address at the index. + * @param [in] index The index of the target address. + * @return The target address. + */ +NimBLEAddress NimBLEAdvertisedDevice::getTargetAddress(uint8_t index) const { + size_t data_loc = ULONG_MAX; + uint8_t count = findAdvField(BLE_HS_ADV_TYPE_PUBLIC_TGT_ADDR, index, &data_loc); + if (count < index + 1) { + index -= count; + count = findAdvField(BLE_HS_ADV_TYPE_RANDOM_TGT_ADDR, index, &data_loc); + } + + if (count > 0 && data_loc != ULONG_MAX) { + index++; + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length < index * BLE_HS_ADV_PUBLIC_TGT_ADDR_ENTRY_LEN) { + // In the case of more than one field of target addresses we need to adjust the index + index -= count - field->length / BLE_HS_ADV_PUBLIC_TGT_ADDR_ENTRY_LEN; + } + if (field->length > index * BLE_HS_ADV_PUBLIC_TGT_ADDR_ENTRY_LEN) { + return NimBLEAddress{field->value + (index - 1) * BLE_HS_ADV_PUBLIC_TGT_ADDR_ENTRY_LEN, field->type}; + } + } + + return NimBLEAddress{}; +} + +/** + * @brief Get the service data. + * @param [in] index The index of the service data requested. + * @return The advertised service data or empty string if no data. + */ +std::string NimBLEAdvertisedDevice::getServiceData(uint8_t index) const { + uint8_t bytes; + size_t data_loc = findServiceData(index, &bytes); + if (data_loc != ULONG_MAX) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length > bytes) { + const char* field_data = reinterpret_cast(field->value + bytes); + return std::string(field_data, field->length - bytes - 1); + } + } + + return ""; +} // getServiceData + +/** + * @brief Get the service data. + * @param [out] len The length of the service data requested. + * @return The advertised service data or empty string if no data. + */ +uint8_t* NimBLEAdvertisedDevice::getServiceData(uint16_t* len) const { + uint8_t bytes; + size_t data_loc = findServiceData(0, &bytes); // Assuming index 0 for the first service data + if (data_loc != ULONG_MAX) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length > bytes) { + *len = field->length - bytes - 1; + return const_cast(field->value + bytes); + } + } + *len = 0; + return nullptr; +} // getServiceData + + +/** + * @brief Get the service data. + * @param [in] uuid The uuid of the service data requested. + * @return The advertised service data or empty string if no data. + */ +std::string NimBLEAdvertisedDevice::getServiceData(const NimBLEUUID& uuid) const { + uint8_t bytes; + uint8_t index = 0; + size_t data_loc = findServiceData(index, &bytes); + size_t pl_size = m_payload.size() - 2; + uint8_t uuid_bytes = uuid.bitSize() / 8; + + while (data_loc < pl_size) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (bytes == uuid_bytes && NimBLEUUID(field->value, bytes) == uuid) { + const char* field_data = reinterpret_cast(field->value + bytes); + return std::string(field_data, field->length - bytes - 1); + } + + index++; + data_loc = findServiceData(index, &bytes); + } + + NIMBLE_LOGI(LOG_TAG, "No service data found"); + return ""; +} // getServiceData + +/** + * @brief Get the UUID of the service data at the index. + * @param [in] index The index of the service data UUID requested. + * @return The advertised service data UUID or an empty UUID if not found. + */ +NimBLEUUID NimBLEAdvertisedDevice::getServiceDataUUID(uint8_t index) const { + uint8_t bytes; + size_t data_loc = findServiceData(index, &bytes); + if (data_loc != ULONG_MAX) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length >= bytes) { + return NimBLEUUID(field->value, bytes); + } + } + + return NimBLEUUID(""); +} // getServiceDataUUID + +/** + * @brief Find the service data at the index. + * @param [in] index The index of the service data to find. + * @param [in] bytes A pointer to storage for the number of the bytes in the UUID. + * @return The index in the vector where the data is located, ULONG_MAX if not found. + */ +size_t NimBLEAdvertisedDevice::findServiceData(uint8_t index, uint8_t* bytes) const { + *bytes = 0; + + size_t data_loc = 0; + uint8_t found = findAdvField(BLE_HS_ADV_TYPE_SVC_DATA_UUID16, index, &data_loc); + if (found > index) { + *bytes = 2; + return data_loc; + } + + index -= found; + found = findAdvField(BLE_HS_ADV_TYPE_SVC_DATA_UUID32, index, &data_loc); + if (found > index) { + *bytes = 4; + return data_loc; + } + + index -= found; + found = findAdvField(BLE_HS_ADV_TYPE_SVC_DATA_UUID128, index, &data_loc); + if (found > index) { + *bytes = 16; + return data_loc; + } + + return ULONG_MAX; +} + +/** + * @brief Get the count of advertised service data UUIDS + * @return The number of service data UUIDS in the vector. + */ +uint8_t NimBLEAdvertisedDevice::getServiceDataCount() const { + uint8_t count = findAdvField(BLE_HS_ADV_TYPE_SVC_DATA_UUID16); + count += findAdvField(BLE_HS_ADV_TYPE_SVC_DATA_UUID32); + count += findAdvField(BLE_HS_ADV_TYPE_SVC_DATA_UUID128); + + return count; +} // getServiceDataCount + +/** + * @brief Get the Service UUID. + * @param [in] index The index of the service UUID requested. + * @return The Service UUID of the advertised service, or an empty UUID if not found. + */ +NimBLEUUID NimBLEAdvertisedDevice::getServiceUUID(uint8_t index) const { + uint8_t type = BLE_HS_ADV_TYPE_INCOMP_UUIDS16; + size_t data_loc = 0; + uint8_t uuid_bytes = 0; + uint8_t count = 0; + + do { + count = findAdvField(type, index, &data_loc); + if (count > index) { + if (type < BLE_HS_ADV_TYPE_INCOMP_UUIDS32) { + uuid_bytes = 2; + } else if (type < BLE_HS_ADV_TYPE_INCOMP_UUIDS128) { + uuid_bytes = 4; + } else { + uuid_bytes = 16; + } + break; + + } else { + type++; + index -= count; + } + + } while (type <= BLE_HS_ADV_TYPE_COMP_UUIDS128); + + if (uuid_bytes > 0) { + index++; + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + // In the case of more than one field of service uuid's we need to adjust + // the index to account for the uuids of the previous fields. + if (field->length < index * uuid_bytes) { + index -= count - field->length / uuid_bytes; + } + + if (field->length > uuid_bytes * index) { + return NimBLEUUID(field->value + uuid_bytes * (index - 1), uuid_bytes); + } + } + + return NimBLEUUID(""); +} // getServiceUUID + +/** + * @brief Get the number of services advertised + * @return The count of services in the advertising packet. + */ +uint8_t NimBLEAdvertisedDevice::getServiceUUIDCount() const { + uint8_t count = findAdvField(BLE_HS_ADV_TYPE_INCOMP_UUIDS16); + count += findAdvField(BLE_HS_ADV_TYPE_COMP_UUIDS16); + count += findAdvField(BLE_HS_ADV_TYPE_INCOMP_UUIDS32); + count += findAdvField(BLE_HS_ADV_TYPE_COMP_UUIDS32); + count += findAdvField(BLE_HS_ADV_TYPE_INCOMP_UUIDS128); + count += findAdvField(BLE_HS_ADV_TYPE_COMP_UUIDS128); + + return count; +} // getServiceUUIDCount + +/** + * @brief Check advertised services for existence of the required UUID + * @param [in] uuid The service uuid to look for in the advertisement. + * @return Return true if service is advertised + */ +bool NimBLEAdvertisedDevice::isAdvertisingService(const NimBLEUUID& uuid) const { + size_t count = getServiceUUIDCount(); + for (size_t i = 0; i < count; i++) { + if (uuid == getServiceUUID(i)) { + return true; + } + } + + return false; +} // isAdvertisingService + +/** + * @brief Get the TX Power. + * @return The TX Power of the advertised device. + */ +int8_t NimBLEAdvertisedDevice::getTXPower() const { + size_t data_loc = 0; + if (findAdvField(BLE_HS_ADV_TYPE_TX_PWR_LVL, 0, &data_loc) > 0) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data_loc]); + if (field->length == BLE_HS_ADV_TX_PWR_LVL_LEN + 1) { + return *(int8_t*)field->value; + } + } + + return -99; +} // getTXPower + +/** + * @brief Does this advertisement have preferred connection parameters? + * @return True if connection parameters are present. + */ +bool NimBLEAdvertisedDevice::haveConnParams() const { + return findAdvField(BLE_HS_ADV_TYPE_SLAVE_ITVL_RANGE) > 0; +} // haveConnParams + +/** + * @brief Does this advertisement have have the advertising interval? + * @return True if the advertisement interval is present. + */ +bool NimBLEAdvertisedDevice::haveAdvInterval() const { + return findAdvField(BLE_HS_ADV_TYPE_ADV_ITVL) > 0; +} // haveAdvInterval + +/** + * @brief Does this advertisement have an appearance value? + * @return True if there is an appearance value present. + */ +bool NimBLEAdvertisedDevice::haveAppearance() const { + return findAdvField(BLE_HS_ADV_TYPE_APPEARANCE) > 0; +} // haveAppearance + +/** + * @brief Does this advertisement have manufacturer data? + * @return True if there is manufacturer data present. + */ +bool NimBLEAdvertisedDevice::haveManufacturerData() const { + return findAdvField(BLE_HS_ADV_TYPE_MFG_DATA) > 0; +} // haveManufacturerData + +/** + * @brief Does this advertisement have a URI? + * @return True if there is a URI present. + */ +bool NimBLEAdvertisedDevice::haveURI() const { + return findAdvField(BLE_HS_ADV_TYPE_URI) > 0; +} // haveURI + +/** + * @brief Does this advertisement have a adv type `type`? + * @return True if there is a `type` present. + */ +bool NimBLEAdvertisedDevice::haveType(uint16_t type) const { + return findAdvField(type) > 0; +} + +/** + * @brief Does the advertisement contain a target address? + * @return True if an address is present. + */ +bool NimBLEAdvertisedDevice::haveTargetAddress() const { + return findAdvField(BLE_HS_ADV_TYPE_PUBLIC_TGT_ADDR) > 0 || findAdvField(BLE_HS_ADV_TYPE_RANDOM_TGT_ADDR) > 0; +} + +/** + * @brief Does this advertisement have a name value? + * @return True if there is a name value present. + */ +bool NimBLEAdvertisedDevice::haveName() const { + return findAdvField(BLE_HS_ADV_TYPE_COMP_NAME) > 0; +} // haveName + +/** + * @brief Does this advertisement have a service data value? + * @return True if there is a service data value present. + */ +bool NimBLEAdvertisedDevice::haveServiceData() const { + return getServiceDataCount() > 0; +} // haveServiceData + +/** + * @brief Does this advertisement have a service UUID value? + * @return True if there is a service UUID value present. + */ +bool NimBLEAdvertisedDevice::haveServiceUUID() const { + return getServiceUUIDCount() > 0; +} // haveServiceUUID + +/** + * @brief Does this advertisement have a transmission power value? + * @return True if there is a transmission power value present. + */ +bool NimBLEAdvertisedDevice::haveTXPower() const { + return findAdvField(BLE_HS_ADV_TYPE_TX_PWR_LVL) > 0; +} // haveTXPower + +# if CONFIG_BT_NIMBLE_EXT_ADV +/** + * @brief Get the set ID of the extended advertisement. + * @return The set ID. + */ +uint8_t NimBLEAdvertisedDevice::getSetId() const { + return m_sid; +} // getSetId + +/** + * @brief Get the primary PHY used by this advertisement. + * @return The PHY type, one of: + * * BLE_HCI_LE_PHY_1M + * * BLE_HCI_LE_PHY_CODED + */ +uint8_t NimBLEAdvertisedDevice::getPrimaryPhy() const { + return m_primPhy; +} // getPrimaryPhy + +/** + * @brief Get the primary PHY used by this advertisement. + * @return The PHY type, one of: + * * BLE_HCI_LE_PHY_1M + * * BLE_HCI_LE_PHY_2M + * * BLE_HCI_LE_PHY_CODED + */ +uint8_t NimBLEAdvertisedDevice::getSecondaryPhy() const { + return m_secPhy; +} // getSecondaryPhy + +/** + * @brief Get the periodic interval of the advertisement. + * @return The periodic advertising interval, 0 if not periodic advertising. + */ +uint16_t NimBLEAdvertisedDevice::getPeriodicInterval() const { + return m_periodicItvl; +} // getPeriodicInterval +# endif + +uint8_t NimBLEAdvertisedDevice::findAdvField(uint8_t type, uint8_t index, size_t* data_loc) const { + size_t length = m_payload.size(); + size_t data = 0; + uint8_t count = 0; + + while (length > 2) { + const ble_hs_adv_field* field = reinterpret_cast(&m_payload[data]); + if (field->length >= length) { + return count; + } + + if (field->type == type || (type == BLE_HS_ADV_TYPE_COMP_NAME && field->type == BLE_HS_ADV_TYPE_INCOMP_NAME)) { + switch (type) { + case BLE_HS_ADV_TYPE_INCOMP_UUIDS16: + case BLE_HS_ADV_TYPE_COMP_UUIDS16: + count += field->length / 2; + break; + + case BLE_HS_ADV_TYPE_INCOMP_UUIDS32: + case BLE_HS_ADV_TYPE_COMP_UUIDS32: + count += field->length / 4; + break; + + case BLE_HS_ADV_TYPE_INCOMP_UUIDS128: + case BLE_HS_ADV_TYPE_COMP_UUIDS128: + count += field->length / 16; + break; + + case BLE_HS_ADV_TYPE_PUBLIC_TGT_ADDR: + case BLE_HS_ADV_TYPE_RANDOM_TGT_ADDR: + count += field->length / 6; + break; + + case BLE_HS_ADV_TYPE_COMP_NAME: + // keep looking for complete name, else use this + if (data_loc != nullptr && field->type == BLE_HS_ADV_TYPE_INCOMP_NAME) { + *data_loc = data; + index++; + } + // fall through + default: + count++; + break; + } + + if (data_loc != nullptr) { + if (count > index) { // assumes index values default to 0 + break; + } + } + } + + length -= 1 + field->length; + data += 1 + field->length; + } + + if (data_loc != nullptr && count > index) { + *data_loc = data; + } + + return count; +} // findAdvField + +/** + * @brief Create a string representation of this device. + * @return A string representation of this device. + */ +std::string NimBLEAdvertisedDevice::toString() const { + std::string res = "Name: " + getName() + ", Address: " + getAddress().toString(); + + if (haveAppearance()) { + char val[6]; + snprintf(val, sizeof(val), "%d", getAppearance()); + res += ", appearance: "; + res += val; + } + + if (haveManufacturerData()) { + auto mfgData = getManufacturerData(); + res += ", manufacturer data: "; + res += NimBLEUtils::dataToHexString(reinterpret_cast(mfgData.data()), mfgData.length()); + } + + if (haveServiceUUID()) { + res += ", serviceUUID: " + getServiceUUID().toString(); + } + + if (haveTXPower()) { + char val[5]; + snprintf(val, sizeof(val), "%d", getTXPower()); + res += ", txPower: "; + res += val; + } + + if (haveServiceData()) { + uint8_t count = getServiceDataCount(); + res += "\nService Data:"; + for (uint8_t i = 0; i < count; i++) { + res += "\nUUID: " + std::string(getServiceDataUUID(i)); + res += ", Data: " + getServiceData(i); + } + } + + return res; + +} // toString + +/** + * @brief Get the length of the advertisement data in the payload. + * @return The number of bytes in the payload that is from the advertisement. + */ +uint8_t NimBLEAdvertisedDevice::getAdvLength() const { + return m_advLength; +} + +/** + * @brief Get the advertised device address type. + * @return The device address type: + * * BLE_ADDR_PUBLIC (0x00) + * * BLE_ADDR_RANDOM (0x01) + * * BLE_ADDR_PUBLIC_ID (0x02) + * * BLE_ADDR_RANDOM_ID (0x03) + */ +uint8_t NimBLEAdvertisedDevice::getAddressType() const { + return m_address.getType(); +} // getAddressType + +/** + * @brief Check if this device is advertising as connectable. + * @return True if the device is connectable. + */ +bool NimBLEAdvertisedDevice::isConnectable() const { +# if CONFIG_BT_NIMBLE_EXT_ADV + if (m_isLegacyAdv) { + return m_advType == BLE_HCI_ADV_RPT_EVTYPE_ADV_IND || m_advType == BLE_HCI_ADV_RPT_EVTYPE_DIR_IND; + } +# endif + return (m_advType & BLE_HCI_ADV_CONN_MASK) || (m_advType & BLE_HCI_ADV_DIRECT_MASK); +} // isConnectable + +/** + * @brief Check if this device is advertising as scannable. + * @return True if the device is scannable. + */ +bool NimBLEAdvertisedDevice::isScannable() const { + return isLegacyAdvertisement() && (m_advType == BLE_HCI_ADV_TYPE_ADV_IND || m_advType == BLE_HCI_ADV_TYPE_ADV_SCAN_IND); +} // isScannable + +/** + * @brief Check if this advertisement is a legacy or extended type + * @return True if legacy (Bluetooth 4.x), false if extended (bluetooth 5.x). + */ +bool NimBLEAdvertisedDevice::isLegacyAdvertisement() const { +# if CONFIG_BT_NIMBLE_EXT_ADV + return m_isLegacyAdv; +# else + return true; +# endif +} // isLegacyAdvertisement + +/** + * @brief Get the payload advertised by the device. + * @return The advertisement payload. + */ +const std::vector& NimBLEAdvertisedDevice::getPayload() const { + return m_payload; +} + +/** + * @brief Get the begin iterator for the payload. + * @return A read only iterator pointing to the first byte in the payload. + */ +const std::vector::const_iterator NimBLEAdvertisedDevice::begin() const { + return m_payload.cbegin(); +} + +/** + * @brief Get the end iterator for the payload. + * @return A read only iterator pointing to one past the last byte of the payload. + */ +const std::vector::const_iterator NimBLEAdvertisedDevice::end() const { + return m_payload.cend(); +} + +#endif /* CONFIG_BT_ENABLED && CONFIG_BT_NIMBLE_ROLE_CENTRAL */ diff --git a/Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.h b/Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.h new file mode 100644 index 0000000..79955ef --- /dev/null +++ b/Libraries/NimBLE-Arduino_modified/NimBLEAdvertisedDevice.h @@ -0,0 +1,197 @@ +/* + * Copyright 2020-2024 Ryan Powell and + * esp-nimble-cpp, NimBLE-Arduino contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NIMBLE_CPP_ADVERTISED_DEVICE_H_ +#define NIMBLE_CPP_ADVERTISED_DEVICE_H_ + +#include "nimconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) + +# include "NimBLEAddress.h" +# include "NimBLEScan.h" +# include "NimBLEUUID.h" + +# if defined(CONFIG_NIMBLE_CPP_IDF) +# include "host/ble_hs_adv.h" +# include "host/ble_gap.h" +# else +# include "nimble/nimble/host/include/host/ble_hs_adv.h" +# include "nimble/nimble/host/include/host/ble_gap.h" +# endif + +# include + +class NimBLEScan; +/** + * @brief A representation of a %BLE advertised device found by a scan. + * + * When we perform a %BLE scan, the result will be a set of devices that are advertising. This + * class provides a model of a detected device. + */ +class NimBLEAdvertisedDevice { + public: + NimBLEAdvertisedDevice() = default; + + uint8_t getAdvType() const; + uint8_t getAdvFlags() const; + uint16_t getAppearance() const; + uint16_t getAdvInterval() const; + uint16_t getMinInterval() const; + uint16_t getMaxInterval() const; + uint8_t getManufacturerDataCount() const; + const NimBLEAddress& getAddress() const; + std::string getManufacturerData(uint8_t index = 0) const; + std::string getURI() const; + std::string getPayloadByType(uint16_t type, uint8_t index = 0) const; + std::string getName() const; + int8_t getRSSI() const; + NimBLEScan* getScan() const; + uint8_t getServiceDataCount() const; + std::string getServiceData(uint8_t index = 0) const; + uint8_t * getServiceData(uint16_t *len) const; + std::string getServiceData(const NimBLEUUID& uuid) const; + NimBLEUUID getServiceDataUUID(uint8_t index = 0) const; + NimBLEUUID getServiceUUID(uint8_t index = 0) const; + uint8_t getServiceUUIDCount() const; + NimBLEAddress getTargetAddress(uint8_t index = 0) const; + uint8_t getTargetAddressCount() const; + int8_t getTXPower() const; + uint8_t getAdvLength() const; + uint8_t getAddressType() const; + bool isAdvertisingService(const NimBLEUUID& uuid) const; + bool haveAppearance() const; + bool haveManufacturerData() const; + bool haveName() const; + bool haveServiceData() const; + bool haveServiceUUID() const; + bool haveTXPower() const; + bool haveConnParams() const; + bool haveAdvInterval() const; + bool haveTargetAddress() const; + bool haveURI() const; + bool haveType(uint16_t type) const; + std::string toString() const; + bool isConnectable() const; + bool isScannable() const; + bool isLegacyAdvertisement() const; +# if CONFIG_BT_NIMBLE_EXT_ADV + uint8_t getSetId() const; + uint8_t getPrimaryPhy() const; + uint8_t getSecondaryPhy() const; + uint16_t getPeriodicInterval() const; +# endif + + const std::vector& getPayload() const; + const std::vector::const_iterator begin() const; + const std::vector::const_iterator end() const; + + /** + * @brief A template to convert the service data to . + * @tparam T The type to convert the data to. + * @param [in] skipSizeCheck If true it will skip checking if the data size is less than sizeof(). + * @return The data converted to or NULL if skipSizeCheck is false and the data is + * less than sizeof(). + * @details Use: getManufacturerData(skipSizeCheck); + */ + template + T getManufacturerData(bool skipSizeCheck = false) const { + std::string data = getManufacturerData(); + if (!skipSizeCheck && data.size() < sizeof(T)) return T(); + const char* pData = data.data(); + return *((T*)pData); + } + + /** + * @brief A template to convert the service data to . + * @tparam T The type to convert the data to. + * @param [in] index The vector index of the service data requested. + * @param [in] skipSizeCheck If true it will skip checking if the data size is less than sizeof(). + * @return The data converted to or NULL if skipSizeCheck is false and the data is + * less than sizeof(). + * @details Use: getServiceData(skipSizeCheck); + */ + template + T getServiceData(uint8_t index = 0, bool skipSizeCheck = false) const { + std::string data = getServiceData(index); + if (!skipSizeCheck && data.size() < sizeof(T)) return T(); + const char* pData = data.data(); + return *((T*)pData); + } + + /** + * @brief A template to convert the service data to . + * @tparam T The type to convert the data to. + * @param [in] uuid The uuid of the service data requested. + * @param [in] skipSizeCheck If true it will skip checking if the data size is less than sizeof(). + * @return The data converted to or NULL if skipSizeCheck is false and the data is + * less than sizeof(). + * @details Use: getServiceData(skipSizeCheck); + */ + template + T getServiceData(const NimBLEUUID& uuid, bool skipSizeCheck = false) const { + std::string data = getServiceData(uuid); + if (!skipSizeCheck && data.size() < sizeof(T)) return T(); + const char* pData = data.data(); + return *((T*)pData); + } + + /** + * @brief A template to convert the service data to . + * @tparam T The type to convert the data to. + * @param [in] uuid The UUID of the service data requested. + * @param [in] skipSizeCheck If true, skips checking if the data size is less than sizeof(). + * @return The data converted to or a default-constructed if size check fails. + * @details Usage: getServiceData(uuid, skipSizeCheck); + */ + template + T getServiceData(uint16_t *len) const { + uint8_t* data = getServiceData(len); + + if (data == nullptr) { + len = 0; + return T(); + } + return *reinterpret_cast(data); + } + + private: + friend class NimBLEScan; + + NimBLEAdvertisedDevice(const ble_gap_event* event, uint8_t eventType); + void update(const ble_gap_event* event, uint8_t eventType); + uint8_t findAdvField(uint8_t type, uint8_t index = 0, size_t* data_loc = nullptr) const; + size_t findServiceData(uint8_t index, uint8_t* bytes) const; + + NimBLEAddress m_address{}; + uint8_t m_advType{}; + int8_t m_rssi{}; + uint8_t m_callbackSent{}; + uint8_t m_advLength{}; + +# if CONFIG_BT_NIMBLE_EXT_ADV + bool m_isLegacyAdv{}; + uint8_t m_sid{}; + uint8_t m_primPhy{}; + uint8_t m_secPhy{}; + uint16_t m_periodicItvl{}; +# endif + + std::vector m_payload; +}; + +#endif /* CONFIG_BT_ENABLED && CONFIG_BT_NIMBLE_ROLE_OBSERVER */ +#endif /* NIMBLE_CPP_ADVERTISED_DEVICE_H_ */ diff --git a/Libraries/NimBLE-Arduino_modified/NimBLEScan.cpp b/Libraries/NimBLE-Arduino_modified/NimBLEScan.cpp new file mode 100644 index 0000000..bda0936 --- /dev/null +++ b/Libraries/NimBLE-Arduino_modified/NimBLEScan.cpp @@ -0,0 +1,625 @@ +/* + * Copyright 2020-2024 Ryan Powell and + * esp-nimble-cpp, NimBLE-Arduino contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "nimconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) + +# include "NimBLEScan.h" +# include "NimBLEDevice.h" +# include "NimBLELog.h" + +# include +# include + +static const char* LOG_TAG = "NimBLEScan"; +static NimBLEScanCallbacks defaultScanCallbacks; + +/** + * @brief Scan constructor. + */ +NimBLEScan::NimBLEScan() + : m_pScanCallbacks{&defaultScanCallbacks}, + // default interval + window, no whitelist scan filter,not limited scan, no scan response, filter_duplicates + m_scanParams{0, 0, BLE_HCI_SCAN_FILT_NO_WL, 0, 1, 1}, + m_pTaskData{nullptr}, + m_maxResults{0xFF}, + m_bDiscovered(false) +{} + +/** + * @brief Scan destructor, release any allocated resources. + */ +NimBLEScan::~NimBLEScan() { + clearResults(); +} + +/** + * @brief Handle GAP events related to scans. + * @param [in] event The event type for this event. + * @param [in] param Parameter data for this event. + */ +int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) { + (void)arg; + NimBLEScan* pScan = NimBLEDevice::getScan(); + + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_DISC: { + //const auto& disc = event->disc; + //const bool isLegacyAdv = true; + //const auto event_type = event->disc.event_type; + NimBLEAddress advertisedAddress(event->disc.addr); + NimBLEAdvertisedDevice tempDevice(event, event->disc.event_type); + + // Process if the address does match the target + if (advertisedAddress == pScan->m_targetAddress) { + pScan->m_pScanCallbacks->onResult(&tempDevice); + return 0; + } else { + // SSID & PW Broadcast + // Get manufacturer data as a string + const std::string& manufacturerData = tempDevice.getManufacturerData(); + if (!manufacturerData.empty() && manufacturerData.length() >= 2) { + // Check if the manufacturer data is valid and the first 2 bytes matches the manufacturer ID + uint16_t manufacturerID = * (uint16_t *) &manufacturerData[0]; + if (manufacturerID == 0x4E80 || manufacturerID == 0x4E81) { + pScan->m_pScanCallbacks->onDiscovered(&tempDevice); + } + } + return 0; + } + + + // Call onDiscovered only once + // if (!pScan->m_bDiscovered) { + // pScan->m_device = NimBLEAdvertisedDevice(event, event->disc.event_type); + // //pScan->m_pScanCallbacks->onDiscovered(&pScan->m_device); + // pScan->m_bDiscovered = true; + // } else { + // // Update the single device instance with new data + // pScan->m_device.update(event, event->disc.event_type); + // } + + // // Always call onResult + // pScan->m_pScanCallbacks->onResult(&pScan->m_device); + // //pScan->erase(&pScan->m_device); + // return 0; + } + break; + case BLE_GAP_EVENT_DISC_COMPLETE: { + NIMBLE_LOGD(LOG_TAG, "Discovery complete; reason=%d", event->disc_complete.reason); + + //pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason); + + if (pScan->m_pTaskData != nullptr) { + NimBLEUtils::taskRelease(*pScan->m_pTaskData, event->disc_complete.reason); + } + + return 0; + } + + default: + return 0; + } +} +/* +int NimBLEScan::handleGapEvent(ble_gap_event* event, void* arg) { + (void)arg; + NimBLEScan* pScan = NimBLEDevice::getScan(); + + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_DISC: { +# if CONFIG_BT_NIMBLE_EXT_ADV + const auto& disc = event->ext_disc; + const bool isLegacyAdv = disc.props & BLE_HCI_ADV_LEGACY_MASK; + const auto event_type = isLegacyAdv ? disc.legacy_event_type : disc.props; +# else + const auto& disc = event->disc; + const bool isLegacyAdv = true; + const auto event_type = disc.event_type; +# endif + NimBLEAddress advertisedAddress(disc.addr); + +# ifdef CONFIG_BT_NIMBLE_ROLE_CENTRAL + // stop processing if already connected + NimBLEClient* pClient = NimBLEDevice::getClientByPeerAddress(advertisedAddress); + if (pClient != nullptr && pClient->isConnected()) { + NIMBLE_LOGI(LOG_TAG, "Ignoring device: address: %s, already connected", advertisedAddress.toString().c_str()); + return 0; + } +# endif + NimBLEAdvertisedDevice* advertisedDevice = nullptr; + + // If we've seen this device before get a pointer to it from the vector + for (const auto& dev : pScan->m_scanResults.m_deviceVec) { +# if CONFIG_BT_NIMBLE_EXT_ADV + // Same address but different set ID should create a new advertised device. + if (dev->getAddress() == advertisedAddress && dev->getSetId() == disc.sid) +# else + if (dev->getAddress() == advertisedAddress) +# endif + { + advertisedDevice = dev; + break; + } + } + + // If we haven't seen this device before; create a new instance and insert it in the vector. + // Otherwise just update the relevant parameters of the already known device. + if (advertisedDevice == nullptr) { + // Check if we have reach the scan results limit, ignore this one if so. + // We still need to store each device when maxResults is 0 to be able to append the scan results + if (pScan->m_maxResults > 0 && pScan->m_maxResults < 0xFF && + (pScan->m_scanResults.m_deviceVec.size() >= pScan->m_maxResults)) { + return 0; + } + + if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) { + NIMBLE_LOGI(LOG_TAG, "Scan response without advertisement: %s", advertisedAddress.toString().c_str()); + } + + advertisedDevice = new NimBLEAdvertisedDevice(event, event_type); + pScan->m_scanResults.m_deviceVec.push_back(advertisedDevice); + NIMBLE_LOGI(LOG_TAG, "New advertiser: %s", advertisedAddress.toString().c_str()); + } else { + advertisedDevice->update(event, event_type); + if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) { + NIMBLE_LOGI(LOG_TAG, "Scan response from: %s", advertisedAddress.toString().c_str()); + } else { + NIMBLE_LOGI(LOG_TAG, "Duplicate; updated: %s", advertisedAddress.toString().c_str()); + } + } + + if (!advertisedDevice->m_callbackSent) { + advertisedDevice->m_callbackSent++; + pScan->m_pScanCallbacks->onDiscovered(advertisedDevice); + } + + // If not active scanning or scan response is not available + // or extended advertisement scanning, report the result to the callback now. + if (pScan->m_scanParams.passive || !isLegacyAdv || !advertisedDevice->isScannable()) { + advertisedDevice->m_callbackSent++; + pScan->m_pScanCallbacks->onResult(advertisedDevice); + } else if (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP) { + advertisedDevice->m_callbackSent++; + // got the scan response report the full data. + pScan->m_pScanCallbacks->onResult(advertisedDevice); + } + + // If not storing results and we have invoked the callback, delete the device. + if (pScan->m_maxResults == 0 && advertisedDevice->m_callbackSent >= 2) { + pScan->erase(advertisedDevice); + } + + return 0; + } + + case BLE_GAP_EVENT_DISC_COMPLETE: { + NIMBLE_LOGD(LOG_TAG, "discovery complete; reason=%d", event->disc_complete.reason); + + if (pScan->m_maxResults == 0) { + pScan->clearResults(); + } + + pScan->m_pScanCallbacks->onScanEnd(pScan->m_scanResults, event->disc_complete.reason); + + if (pScan->m_pTaskData != nullptr) { + NimBLEUtils::taskRelease(*pScan->m_pTaskData, event->disc_complete.reason); + } + + return 0; + } + + default: + return 0; + } +} // handleGapEvent +*/ + +/** + * @brief Should we perform an active or passive scan? + * The default is a passive scan. An active scan means that we will request a scan response. + * @param [in] active If true, we perform an active scan otherwise a passive scan. + */ +void NimBLEScan::setActiveScan(bool active) { + m_scanParams.passive = !active; +} // setActiveScan + +/** + * @brief Set whether or not the BLE controller should only report results + * from devices it has not already seen. + * @param [in] enabled If set to 1 (true), scanned devices will only be reported once. + * If set to 0 duplicates will be reported each time they are seen. + * If using extended scanning this can be set to 2 which will reset the duplicate filter + * at the end of each scan period if the scan period is set. + * @note The controller has a limited buffer and will start reporting +duplicate devices once the limit is reached. + */ +void NimBLEScan::setDuplicateFilter(uint8_t enabled) { + m_scanParams.filter_duplicates = enabled; +} // setDuplicateFilter + +/** + * @brief Set whether or not the BLE controller only reports scan results + * from devices advertising in limited discovery mode. + * @param [in] enabled If true, only limited discovery devices will be in scan results. + */ +void NimBLEScan::setLimitedOnly(bool enabled) { + m_scanParams.limited = enabled; +} // setLimited + +/** + * @brief Sets the scan filter policy. + * @param [in] filter Can be one of: + * * BLE_HCI_SCAN_FILT_NO_WL (0) + * Scanner processes all advertising packets (white list not used) except\n + * directed, connectable advertising packets not sent to the scanner. + * * BLE_HCI_SCAN_FILT_USE_WL (1) + * Scanner processes advertisements from white list only. A connectable,\n + * directed advertisement is ignored unless it contains scanners address. + * * BLE_HCI_SCAN_FILT_NO_WL_INITA (2) + * Scanner process all advertising packets (white list not used). A\n + * connectable, directed advertisement shall not be ignored if the InitA + * is a resolvable private address. + * * BLE_HCI_SCAN_FILT_USE_WL_INITA (3) + * Scanner process advertisements from white list only. A connectable,\n + * directed advertisement shall not be ignored if the InitA is a + * resolvable private address. + */ +void NimBLEScan::setFilterPolicy(uint8_t filter) { + m_scanParams.filter_policy = filter; +} // setFilterPolicy + +/** + * @brief Sets the max number of results to store. + * @param [in] maxResults The number of results to limit storage to\n + * 0 == none (callbacks only) 0xFF == unlimited, any other value is the limit. + */ +void NimBLEScan::setMaxResults(uint8_t maxResults) { + m_maxResults = maxResults; +} // setMaxResults + +/** + * @brief Set the call backs to be invoked. + * @param [in] pScanCallbacks Call backs to be invoked. + * @param [in] wantDuplicates True if we wish to be called back with duplicates, default: false. + */ +void NimBLEScan::setScanCallbacks(NimBLEScanCallbacks* pScanCallbacks, bool wantDuplicates) { + setDuplicateFilter(!wantDuplicates); + if (pScanCallbacks == nullptr) { + m_pScanCallbacks = &defaultScanCallbacks; + return; + } + m_pScanCallbacks = pScanCallbacks; +} // setScanCallbacks + +/** + * @brief Set the interval to scan. + * @param [in] intervalMs The scan interval in milliseconds. + * @details The interval is the time between the start of two consecutive scan windows. + * When a new interval starts the controller changes the channel it's scanning on. + */ +void NimBLEScan::setInterval(uint16_t intervalMs) { + m_scanParams.itvl = (intervalMs * 16) / 10; +} // setInterval + +/** + * @brief Set the window to actively scan. + * @param [in] windowMs How long during the interval to actively scan in milliseconds. + */ +void NimBLEScan::setWindow(uint16_t windowMs) { + m_scanParams.window = (windowMs * 16) / 10; +} // setWindow + +/** + * @brief Get the status of the scanner. + * @return true if scanning or scan starting. + */ +bool NimBLEScan::isScanning() { + return ble_gap_disc_active(); +} + +# if CONFIG_BT_NIMBLE_EXT_ADV +/** + * @brief Set the PHYs to scan. + * @param [in] phyMask The PHYs to scan, a bit mask of: + * * NIMBLE_CPP_SCAN_1M + * * NIMBLE_CPP_SCAN_CODED + */ +void NimBLEScan::setPhy(Phy phyMask) { + m_phy = phyMask; +} // setScanPhy + +/** + * @brief Set the extended scanning period. + * @param [in] periodMs The scan period in milliseconds + * @details The period is the time between the start of two consecutive scan periods. + * This works as a timer to restart scanning at the specified amount of time in periodMs. + * @note The duration used when this is set must be less than period. + */ +void NimBLEScan::setPeriod(uint32_t periodMs) { + m_period = (periodMs + 500) / 1280; // round up 1.28 second units +} // setScanPeriod +# endif + +/** + * @brief Start scanning. + * @param [in] duration The duration in milliseconds for which to scan. 0 == scan forever. + * @param [in] isContinue Set to true to save previous scan results, false to clear them. + * @param [in] restart Set to true to restart the scan if already in progress. + * this is useful to clear the duplicate filter so all devices are reported again. + * @return True if scan started or false if there was an error. + */ +bool NimBLEScan::start(uint32_t duration, bool isContinue, bool restart) { + NIMBLE_LOGD(LOG_TAG, ">> start: duration=%" PRIu32, duration); + if (isScanning()) { + if (restart) { + NIMBLE_LOGI(LOG_TAG, "Scan already in progress, restarting it"); + if (!stop()) { + return false; + } + + if (!isContinue) { + clearResults(); + } + } + } else { // Don't clear results while scanning is active + if (!isContinue) { + clearResults(); + } + } + + // If scanning is already active, call the functions anyway as the parameters can be changed. + +# if CONFIG_BT_NIMBLE_EXT_ADV + ble_gap_ext_disc_params scan_params; + scan_params.passive = m_scanParams.passive; + scan_params.itvl = m_scanParams.itvl; + scan_params.window = m_scanParams.window; + int rc = ble_gap_ext_disc(NimBLEDevice::m_ownAddrType, + duration / 10, // 10ms units + m_period, + m_scanParams.filter_duplicates, + m_scanParams.filter_policy, + m_scanParams.limited, + m_phy & SCAN_1M ? &scan_params : NULL, + m_phy & SCAN_CODED ? &scan_params : NULL, + NimBLEScan::handleGapEvent, + NULL); +# else + int rc = ble_gap_disc(NimBLEDevice::m_ownAddrType, + duration ? duration : BLE_HS_FOREVER, + &m_scanParams, + NimBLEScan::handleGapEvent, + NULL); +# endif + switch (rc) { + case 0: + case BLE_HS_EALREADY: + NIMBLE_LOGD(LOG_TAG, "Scan started"); + break; + + case BLE_HS_EBUSY: + NIMBLE_LOGE(LOG_TAG, "Unable to scan - connection in progress."); + break; + + case BLE_HS_ETIMEOUT_HCI: + case BLE_HS_EOS: + case BLE_HS_ECONTROLLER: + case BLE_HS_ENOTSYNCED: + NIMBLE_LOGE(LOG_TAG, "Unable to scan - Host Reset"); + break; + + default: + NIMBLE_LOGE(LOG_TAG, "Error starting scan; rc=%d, %s", rc, NimBLEUtils::returnCodeToString(rc)); + break; + } + + NIMBLE_LOGD(LOG_TAG, "<< start()"); + return rc == 0 || rc == BLE_HS_EALREADY; +} // start + +/** + * @brief Stop an in progress scan. + * @return True if successful. + */ +bool NimBLEScan::stop() { + NIMBLE_LOGD(LOG_TAG, ">> stop()"); + + int rc = ble_gap_disc_cancel(); + if (rc != 0 && rc != BLE_HS_EALREADY) { + NIMBLE_LOGE(LOG_TAG, "Failed to cancel scan; rc=%d", rc); + return false; + } + + if (m_maxResults == 0) { + clearResults(); + } + + if (m_pTaskData != nullptr) { + NimBLEUtils::taskRelease(*m_pTaskData); + } + + NIMBLE_LOGD(LOG_TAG, "<< stop()"); + return true; +} // stop + +/** + * @brief Delete peer device from the scan results vector. + * @param [in] address The address of the device to delete from the results. + */ +void NimBLEScan::erase(const NimBLEAddress& address) { + NIMBLE_LOGD(LOG_TAG, "erase device: %s", address.toString().c_str()); + for (auto it = m_scanResults.m_deviceVec.begin(); it != m_scanResults.m_deviceVec.end(); ++it) { + if ((*it)->getAddress() == address) { + delete *it; + m_scanResults.m_deviceVec.erase(it); + break; + } + } +} + +/** + * @brief Delete peer device from the scan results vector. + * @param [in] device The device to delete from the results. + */ +void NimBLEScan::erase(const NimBLEAdvertisedDevice* device) { + NIMBLE_LOGD(LOG_TAG, "erase device: %s", device->getAddress().toString().c_str()); + for (auto it = m_scanResults.m_deviceVec.begin(); it != m_scanResults.m_deviceVec.end(); ++it) { + if ((*it) == device) { + delete *it; + m_scanResults.m_deviceVec.erase(it); + break; + } + } +} + +/** + * @brief If the host reset and re-synced this is called. + * If the application was scanning indefinitely with a callback, restart it. + */ +void NimBLEScan::onHostSync() { + m_pScanCallbacks->onScanEnd(m_scanResults, BLE_HS_ENOTSYNCED); +} + +/** + * @brief Start scanning and block until scanning has been completed. + * @param [in] duration The duration in milliseconds for which to scan. + * @param [in] is_continue Set to true to save previous scan results, false to clear them. + * @return The scan results. + */ +NimBLEScanResults NimBLEScan::getResults(uint32_t duration, bool is_continue) { + if (duration == 0) { + NIMBLE_LOGW(LOG_TAG, "Blocking scan called with duration = forever"); + } + + if (m_pTaskData != nullptr) { + NIMBLE_LOGE(LOG_TAG, "Scan already in progress"); + return m_scanResults; + } + + NimBLETaskData taskData; + m_pTaskData = &taskData; + + if (start(duration, is_continue)) { + NimBLEUtils::taskWait(taskData, BLE_NPL_TIME_FOREVER); + } + + m_pTaskData = nullptr; + return m_scanResults; +} // getResults + +/** + * @brief Get the results of the scan. + * @return NimBLEScanResults object. + */ +NimBLEScanResults NimBLEScan::getResults() { + return m_scanResults; +} + +/** + * @brief Clear the stored results of the scan. + */ +void NimBLEScan::clearResults() { + if (m_scanResults.m_deviceVec.size()) { + std::vector vSwap{}; + ble_npl_hw_enter_critical(); + vSwap.swap(m_scanResults.m_deviceVec); + ble_npl_hw_exit_critical(0); + for (const auto& dev : vSwap) { + delete dev; + } + } +} // clearResults + +/** + * @brief Dump the scan results to the log. + */ +void NimBLEScanResults::dump() const { +#if CONFIG_NIMBLE_CPP_LOG_LEVEL >=3 + for (const auto& dev : m_deviceVec) { + NIMBLE_LOGI(LOG_TAG, "- %s", dev->toString().c_str()); + } +#endif +} // dump + +/** + * @brief Get the count of devices found in the last scan. + * @return The number of devices found in the last scan. + */ +int NimBLEScanResults::getCount() const { + return m_deviceVec.size(); +} // getCount + +/** + * @brief Return the specified device at the given index. + * The index should be between 0 and getCount()-1. + * @param [in] idx The index of the device. + * @return The device at the specified index. + */ +const NimBLEAdvertisedDevice* NimBLEScanResults::getDevice(uint32_t idx) const { + return m_deviceVec[idx]; +} + +/** + * @brief Get iterator to the beginning of the vector of advertised device pointers. + * @return An iterator to the beginning of the vector of advertised device pointers. + */ +std::vector::const_iterator NimBLEScanResults::begin() const { + return m_deviceVec.begin(); +} + +/** + * @brief Get iterator to the end of the vector of advertised device pointers. + * @return An iterator to the end of the vector of advertised device pointers. + */ +std::vector::const_iterator NimBLEScanResults::end() const { + return m_deviceVec.end(); +} + +/** + * @brief Get a pointer to the specified device at the given address. + * If the address is not found a nullptr is returned. + * @param [in] address The address of the device. + * @return A pointer to the device at the specified address. + */ +const NimBLEAdvertisedDevice* NimBLEScanResults::getDevice(const NimBLEAddress& address) const { + for (const auto& dev : m_deviceVec) { + if (dev->getAddress() == address) { + return dev; + } + } + + return nullptr; +} + +static const char* CB_TAG = "NimBLEScanCallbacks"; + +void NimBLEScanCallbacks::onDiscovered(const NimBLEAdvertisedDevice* pAdvertisedDevice) { + NIMBLE_LOGD(CB_TAG, "Discovered: %s", pAdvertisedDevice->toString().c_str()); +} + +void NimBLEScanCallbacks::onResult(const NimBLEAdvertisedDevice* pAdvertisedDevice) { + NIMBLE_LOGD(CB_TAG, "Result: %s", pAdvertisedDevice->toString().c_str()); +} + +void NimBLEScanCallbacks::onScanEnd(const NimBLEScanResults& results, int reason) { + NIMBLE_LOGD(CB_TAG, "Scan ended; reason %d, num results: %d", reason, results.getCount()); +} + +#endif /* CONFIG_BT_ENABLED && CONFIG_BT_NIMBLE_ROLE_OBSERVER */ diff --git a/Libraries/NimBLE-Arduino_modified/NimBLEScan.h b/Libraries/NimBLE-Arduino_modified/NimBLEScan.h new file mode 100644 index 0000000..81cf7b0 --- /dev/null +++ b/Libraries/NimBLE-Arduino_modified/NimBLEScan.h @@ -0,0 +1,146 @@ +/* + * Copyright 2020-2024 Ryan Powell and + * esp-nimble-cpp, NimBLE-Arduino contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NIMBLE_CPP_SCAN_H_ +#define NIMBLE_CPP_SCAN_H_ + +#include "nimconfig.h" +#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) + +# include "NimBLEAdvertisedDevice.h" +# include "NimBLEUtils.h" + +# if defined(CONFIG_NIMBLE_CPP_IDF) +# include "host/ble_gap.h" +# else +# include "nimble/nimble/host/include/host/ble_gap.h" +# endif + +# include + +class NimBLEDevice; +class NimBLEScan; +class NimBLEAdvertisedDevice; +class NimBLEScanCallbacks; +class NimBLEAddress; + +/** + * @brief A class that contains and operates on the results of a BLE scan. + * @details When a scan completes, we have a set of found devices. Each device is described + * by a NimBLEAdvertisedDevice object. The number of items in the set is given by + * getCount(). We can retrieve a device by calling getDevice() passing in the + * index (starting at 0) of the desired device. + */ +class NimBLEScanResults { + public: + void dump() const; + int getCount() const; + const NimBLEAdvertisedDevice* getDevice(uint32_t idx) const; + const NimBLEAdvertisedDevice* getDevice(const NimBLEAddress& address) const; + std::vector::const_iterator begin() const; + std::vector::const_iterator end() const; + + private: + friend NimBLEScan; + std::vector m_deviceVec; +}; + +/** + * @brief Perform and manage %BLE scans. + * + * Scanning is associated with a %BLE client that is attempting to locate BLE servers. + */ +class NimBLEScan { + public: + bool m_bDiscovered; + NimBLEAdvertisedDevice m_device; + NimBLEAddress m_targetAddress; + void setTargetAddress(NimBLEAddress& addr) { m_targetAddress = addr; }; + + public: + bool start(uint32_t duration, bool isContinue = false, bool restart = true); + bool isScanning(); + void setScanCallbacks(NimBLEScanCallbacks* pScanCallbacks, bool wantDuplicates = false); + void setActiveScan(bool active); + void setInterval(uint16_t intervalMs); + void setWindow(uint16_t windowMs); + void setDuplicateFilter(uint8_t enabled); + void setLimitedOnly(bool enabled); + void setFilterPolicy(uint8_t filter); + bool stop(); + void clearResults(); + NimBLEScanResults getResults(); + NimBLEScanResults getResults(uint32_t duration, bool is_continue = false); + void setMaxResults(uint8_t maxResults); + void erase(const NimBLEAddress& address); + void erase(const NimBLEAdvertisedDevice* device); + +# if CONFIG_BT_NIMBLE_EXT_ADV + enum Phy { SCAN_1M = 0x01, SCAN_CODED = 0x02, SCAN_ALL = 0x03 }; + void setPhy(Phy phyMask); + void setPeriod(uint32_t periodMs); +# endif + + private: + friend class NimBLEDevice; + + NimBLEScan(); + ~NimBLEScan(); + static int handleGapEvent(ble_gap_event* event, void* arg); + void onHostSync(); + + NimBLEScanCallbacks* m_pScanCallbacks; + ble_gap_disc_params m_scanParams; + NimBLEScanResults m_scanResults; + NimBLETaskData* m_pTaskData; + uint8_t m_maxResults; + +# if CONFIG_BT_NIMBLE_EXT_ADV + uint8_t m_phy{SCAN_ALL}; + uint16_t m_period{0}; +# endif +}; + +/** + * @brief A callback handler for callbacks associated device scanning. + */ +class NimBLEScanCallbacks { + public: + virtual ~NimBLEScanCallbacks() {} + + /** + * @brief Called when a new device is discovered, before the scan result is received (if applicable). + * @param [in] advertisedDevice The device which was discovered. + */ + virtual void onDiscovered(const NimBLEAdvertisedDevice* advertisedDevice); + + /** + * @brief Called when a new scan result is complete, including scan response data (if applicable). + * @param [in] advertisedDevice The device for which the complete result is available. + */ + virtual void onResult(const NimBLEAdvertisedDevice* advertisedDevice); + + /** + * @brief Called when a scan operation ends. + * @param [in] scanResults The results of the scan that ended. + * @param [in] reason The reason code for why the scan ended. + */ + virtual void onScanEnd(const NimBLEScanResults& scanResults, int reason); +}; + +#endif // CONFIG_BT_ENABLED CONFIG_BT_NIMBLE_ROLE_OBSERVER +#endif // NIMBLE_CPP_SCAN_H_ diff --git a/OTA.cpp b/OTA.cpp index 3fe0b37..4ad6ff4 100644 --- a/OTA.cpp +++ b/OTA.cpp @@ -17,7 +17,7 @@ // OTA // // ============================================================== -const char *HC__VERSION = "20250405001"; +const char *HC__VERSION = "20260413001"; #define UPDATE_PORT ((uint16_t) 443) String url = "visionsoft.kr"; String uri = "/hc/hc_firmware_update.php"; diff --git a/README.md b/README.md index fc1c95b..5a53371 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@  -dependancy: NimBLE-Arduino library, maintained by h2zero (Ryan Powell) \ No newline at end of file +dependancy: NimBLE-Arduino library, maintained by h2zero (Ryan Powell) +requires Arduino core 3.0.2 \ No newline at end of file diff --git a/Setup.cpp b/Setup.cpp index 28e93a8..ea4f8c4 100644 --- a/Setup.cpp +++ b/Setup.cpp @@ -9,7 +9,6 @@ #include "TimeManager.h" #include "OTA.h" #include "UI.h" -#include "LED0.h" #include "BLEScan.h" #include #include "esp_coexist.h" @@ -36,69 +35,75 @@ void setup_BLE(); void scanI2C(); void setup() { - // put your setup code here, to run once: - #ifdef DEBUG - Serial.begin(115200); - #endif - //esp_log_level_set("*", ESP_LOG_INFO); // Global log level - //esp_log_level_set("BLE_POLL", ESP_LOG_INFO); // Module-specific level - //esp_coex_preference_set(ESP_COEX_PREFER_BT); + // put your setup code here, to run once: + #ifdef DEBUG + Serial.begin(115200); + #endif + //esp_log_level_set("*", ESP_LOG_INFO); // Global log level + //esp_log_level_set("BLE_POLL", ESP_LOG_INFO); // Module-specific level + //esp_coex_preference_set(ESP_COEX_PREFER_BT); - DPRINTLN(" **********************"); - DPRINTF(" SETUP - Start - ver. %s type: %d\n", HC__VERSION, THIS_DEVICE_TYPE); - DPRINTLN(" **********************"); - g_bWiFiHasBeenConnected = false; - g_bWiFiHasBeenConnected = false; - g_nYear = 2024; - g_nMonth = 10; - g_nDay = 15; - g_nHour = 0; - g_nMinute = 0; - g_nSecond = 0; - bShowSensor = false; + DPRINTLN(" **********************"); + DPRINTF(" SETUP - Start - ver. %s type: %d\n", HC__VERSION, THIS_DEVICE_TYPE); + DPRINTLN(" **********************"); + g_bWiFiHasBeenConnected = false; + g_bWiFiHasBeenConnected = false; + g_nYear = 2024; + g_nMonth = 10; + g_nDay = 15; + g_nHour = 0; + g_nMinute = 0; + g_nSecond = 0; + bShowSensor = false; - led0.setup(PIN_LED_WIFI, PWM_AP_FREQ, PWM_AP_CHANNEL); - led0.setDuty(20); - setupConfig(); - setupStatus(); - - setupPins(); - scanI2C(); - setupSensor(); + DPRINTLN("1"); + setupConfig(); + DPRINTLN("2"); + setupStatus(); + DPRINTLN("3"); + setupPins(); + DPRINTLN("4"); + scanI2C(); + DPRINTLN("5"); + setupSensor(); + DPRINTLN("6"); + ui.setup(); + DPRINTLN("7"); + ui.message(0, (char *) "WiFi..."); + DPRINTLN("8"); + setupWiFi(); + DPRINTLN("9"); - ui.setup(); - ui.message(0, (char *) "WiFi..."); - setupWiFi(); - - if (aht25.sensor() || aht10_0x39.sensor()) { + if (aht25.sensor() || aht10_0x39.sensor()) { ui.message(4, (char *) "Sensor... OK!"); - } else { + } else { ui.message(4, (char *) "Sensor... None!"); - } + } - ui.message(5, (char *) "ZCD..."); - setupZCD(); - ui.message(5, (char *) "ZCD... OK!"); + ui.message(5, (char *) "ZCD..."); + setupZCD(); + ui.message(5, (char *) "ZCD... OK!"); - ui.message(6, (char *) "Setup OK!"); - //if (!isWiFiConnected) delay(3000); - ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2); - - // Restore Status - restoreStatus(); + ui.message(6, (char *) "Setup OK!"); + //if (!isWiFiConnected) delay(3000); + ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2); - // Create a task pinned to core 0 - xTaskCreatePinnedToCore( + // Restore Status + restoreStatus(); + + + // Create a task pinned to core 0 + xTaskCreatePinnedToCore( core0Task, // Function to run as a task "Task0", // Task name 10240, // Stack size in words NULL, // Task input parameter - 0, // Priority of the task + 1, // Priority of the task &TaskHandle_0, // Task handle 0 // Core 0 - ); - DPRINTLN("Setup Completed\n========================\n"); + ); + DPRINTLN("Setup Completed\n========================\n"); } @@ -193,30 +198,70 @@ void setupPostWiFi(bool bBoot = false) { } } +#include "driver/ledc.h" + void setupPins() { - pinMode(PIN_HEATER1, OUTPUT); - pinMode(PIN_HEATER2, OUTPUT); - digitalWrite(PIN_HEATER1, HEATER_OFF); - digitalWrite(PIN_HEATER2, HEATER_OFF); + // --- Basic Digital Pin Initialization --- + pinMode(PIN_HEATER1, OUTPUT); + pinMode(PIN_HEATER2, OUTPUT); + digitalWrite(PIN_HEATER1, HEATER_OFF); + digitalWrite(PIN_HEATER2, HEATER_OFF); - //ledcAttachChannel(PIN_LED_WIFI, PWM_AP_FREQ, PWM_RESOLUTION, PWM_AP_CHANNEL); - //ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink + // --- 1. High Speed Group: Motor and Fan (~26kHz) --- + // Core 2.0.17 uses APB (80MHz). 80MHz / (3 * 1024) ≈ 26kHz. + ledcAttachChannel(PIN_MOTOR, PWM_26KHZ_FREQ, PWM_RESOLUTION, PWM_MOTOR_CHANNEL); + ledcAttachChannel(PIN_FAN, PWM_26KHZ_FREQ, PWM_RESOLUTION, PWM_FAN_CHANNEL); - ledcAttachChannel(PIN_MIST, PWM_MIST_FREQ, PWM_RESOLUTION, PWM_MIST_CHANNEL); - ledcWrite(PIN_MIST, PWM_OFF); + // --- 2. Medium Speed Group: LED & Heaters (1221Hz) --- + // 80MHz / (64 * 1024) = 1220.7Hz. + ledcAttachChannel(PIN_LIGHT, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_LIGHT_CHANNEL); + ledcAttachChannel(PIN_LED_HEATER1, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER1_CHANNEL); + ledcAttachChannel(PIN_LED_HEATER2, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER2_CHANNEL); - ledcAttachChannel(PIN_LED_HEATER1, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER1_CHANNEL); - ledcAttachChannel(PIN_LED_HEATER2, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER2_CHANNEL); - ledcAttachChannel(PIN_LIGHT, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_LIGHT_CHANNEL); + // --- 3. Low Speed Group: Mist & WiFi LED (1.19Hz) --- + // Initialized at 32Hz to bypass the slow divider calculation/WDT panic. + ledcAttachChannel(PIN_MIST, PWM_1HZ_FREQ, PWM_RESOLUTION, PWM_MIST_CHANNEL); + ledcAttachChannel(PIN_LED_WIFI, PWM_1HZ_FREQ, PWM_RESOLUTION, PWM_WIFI_LED_CHANNEL); - ledcAttachChannel(PIN_MOTOR, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_MOTOR_CHANNEL); - ledcAttachChannel(PIN_FAN, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_FAN_CHANNEL); - - ledcWrite(PIN_LED_HEATER1, PWM_FULL - PWM_OFF); - ledcWrite(PIN_LED_HEATER2, PWM_FULL - PWM_OFF); - ledcWrite(PIN_LIGHT, PWM_OFF); - ledcWrite(PIN_MOTOR, PWM_OFF); - ledcWrite(PIN_FAN, PWM_OFF); + // MANUALLY RECONFIGURE TIMER FOR 1.19Hz + // Source: LEDC_REF_TICK (1MHz) + // Divider: 819 (Integer part) + // Result: 1,000,000 / (819 * 1024) ≈ 1.192 Hz + // Note: On Core 2.0.17, LEDC_USE_REF_TICK is the correct constant. + /* + ledc_timer_set( + LEDC_LOW_SPEED_MODE, + LEDC_TIMER_0, + (uint32_t)(819 << 8), // 18.8 bit register: 819 integer, 0 fractional + (uint32_t)PWM_RESOLUTION, + LEDC_REF_TICK + ); + */ + /* + ledc_timer_config_t mist_timer_conf = { + .speed_mode = LEDC_LOW_SPEED_MODE, + .duty_resolution = LEDC_TIMER_8_BIT, + .timer_num = LEDC_TIMER_0, + .freq_hz = 2, // The driver will target 1Hz + .clk_cfg = LEDC_USE_REF_TICK, // Forces the 1MHz source + .deconfigure = false + }; + esp_err_t err = ledc_timer_config(&mist_timer_conf); + if (err == ESP_OK) { + // Apply the change and reset the counter + ledc_timer_rst(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0); + } + */ + // --- Initial State Writes --- + ledcWrite(PIN_MOTOR, PWM_OFF); + ledcWrite(PIN_FAN, PWM_OFF); + ledcWrite(PIN_LIGHT, PWM_OFF); + ledcWrite(PIN_MIST, PWM_OFF); + ledcWrite(PIN_LED_WIFI, (PWM_FULL * 4) / 5); // 80% Initial Blink + + // Heaters (Inverted logic for MOSFET safety) + ledcWrite(PIN_LED_HEATER1, PWM_FULL - PWM_OFF); + ledcWrite(PIN_LED_HEATER2, PWM_FULL - PWM_OFF); } void setupStatus() { @@ -243,13 +288,15 @@ void restoreStatus() { time_t now; time(&now); uint32_t gap = (uint32_t)now - config.statusSave.now; - DPRINTF("Reboot in %.1f seconds\n", gap / 1000.0f); + DPRINTF("Reboot within %.1f seconds after last reset... \n", gap / 1000.0f); if (gap < 60000 && config.bStatusSaved) { status = config.statusSave; status.nFlags |= (uint16_t)(config.statusSave.nFlags & 0xFF); status.nLightDuty = 0; config.bStatusSaved = false; DPRINTLN(" Status Restored!"); + } else { + DPRINTLN(" Status NOT Restored!"); } } } diff --git a/Task0.ino b/Task0.ino index a9f9d03..ea97810 100644 --- a/Task0.ino +++ b/Task0.ino @@ -23,6 +23,7 @@ void setup_BLE(); // ================================================================================== MY_IRAM_ATTR void core0Task(void *pvParameters) { ESP_LOGI(TAG_TASK0,"Core 0 Task Started"); + DPRINTLN("Core 0 Task Started"); wl_status_t lastWiFiStatus = WL_DISCONNECTED; unsigned long tickMillis = millis(); unsigned long tickSecond; diff --git a/UI.cpp b/UI.cpp index 9b790d3..601e008 100644 --- a/UI.cpp +++ b/UI.cpp @@ -160,7 +160,7 @@ enum UI_ITEM { ITEM_COUNT }; -char *title[] = { +const char *title[] = { "Time:", "Temp1", "Temp2", "Temp3", @@ -691,7 +691,7 @@ MY_IRAM_ATTR bool CUI::displayBottom() { int lastC = 0; char sz[32]; static uint8_t bLastNTP = 0xFF; - static uint8_t bLastBLE = ~FLAG_BLE_NODATA; + static uint8_t bLastBLE = (uint8_t)(~FLAG_BLE_NODATA); static uint8_t bLastConn = 0xFF; static uint8_t bLastWiFi = 0xFF; @@ -1133,20 +1133,20 @@ void CUI::checkButtonStates(unsigned long currentMillis) { } // ISR for the Set button handling -IRAM_ATTR void buttonSetISR() { +ARDUINO_ISR_ATTR void buttonSetISR() { // Record the time of the button interrupt and set a flag ui.buttonSetChangeTime = millis(); ui.bButtonSetChanged = true; // Flag for main loop to process } // ISR for the Up button handling -IRAM_ATTR void buttonUpISR() { +ARDUINO_ISR_ATTR void buttonUpISR() { ui.buttonUpChangeTime = millis(); ui.bButtonUpChanged = true; // Flag for main loop to process } // ISR for the Down button handling -IRAM_ATTR void buttonDownISR() { +ARDUINO_ISR_ATTR void buttonDownISR() { ui.buttonDownChangeTime = millis(); ui.bButtonDownChanged = true; // Flag for main loop to process } \ No newline at end of file diff --git a/WiFiHost.cpp b/WiFiHost.cpp index fb186b4..1cf9ecf 100644 --- a/WiFiHost.cpp +++ b/WiFiHost.cpp @@ -157,14 +157,16 @@ MY_IRAM_ATTR void CWiFiHost::Stop() { udpExternal.stop(); // or udpExternal.end(); depending on your preference } -MY_IRAM_ATTR void CWiFiHost::CloseConnection() +MY_IRAM_ATTR +void CWiFiHost::CloseConnection() { if (wifiClient && wifiClient.connected()) wifiClient.stop(); m_bClientConnected = false; m_nMode = MODE_WAITING; } -IRAM_ATTR void CWiFiHost::Loop(unsigned long clock) +IRAM_ATTR +void CWiFiHost::Loop(unsigned long clock) { if (!isWiFiConnected()) return; @@ -250,7 +252,8 @@ IRAM_ATTR void CWiFiHost::Loop(unsigned long clock) } } -MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) { +MY_IRAM_ATTR +void CWiFiHost::SendHeartBeat(unsigned long clock) { if (!isWiFiConnected()) { //ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendHeartBeat() called while not connected!"); return; @@ -302,7 +305,8 @@ MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) { } } -MY_IRAM_ATTR void CWiFiHost::MonitorUDP() { +MY_IRAM_ATTR +void CWiFiHost::MonitorUDP() { // Listen for UDP External port IPAddress ip; int size = udpExternal.parsePacket(); @@ -355,7 +359,8 @@ MY_IRAM_ATTR void CWiFiHost::MonitorUDP() { } } -IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock) +IRAM_ATTR +void CWiFiHost::CheckClient(unsigned long clock) { // 1. Quick exit if no data is ready to be read int available = wifiClient.available(); @@ -386,7 +391,8 @@ IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock) } } -IRAM_ATTR void CWiFiHost::ProcessPacket(TCP_PACKET& pkt) +IRAM_ATTR +void CWiFiHost::ProcessPacket(TCP_PACKET& pkt) { switch (pkt.cmd) { @@ -463,10 +469,6 @@ IRAM_ATTR void CWiFiHost::ProcessPacket(TCP_PACKET& pkt) } } break; - case CMD_PAUSE: - break; - case CMD_RESUME: - break; case CMD_RESET_SENSOR: aht25.setScanFlag(true); aht10_0x39.setScanFlag(true); @@ -515,11 +517,6 @@ IRAM_ATTR void CWiFiHost::ProcessPacket(TCP_PACKET& pkt) ESP_LOGI(TAG_WIFI_HOST,"WiFi - Send Config initiated..."); } break; - case CMD_SEND_CONFIG_SERVER: - - case CMD_LOAD_CONFIG_SERVER: - break; - // PID case CMD_INIT_PID_PARAM: history.savePID(); @@ -713,7 +710,8 @@ IRAM_ATTR void CWiFiHost::ProcessPacket(TCP_PACKET& pkt) } } -IRAM_ATTR int CWiFiHost::SendPacket(TCP_PACKET& pkt) +IRAM_ATTR +int CWiFiHost::SendPacket(TCP_PACKET& pkt) { size_t sent = 0; if (m_bClientConnected && wifiClient && wifiClient.connected()) @@ -723,7 +721,8 @@ IRAM_ATTR int CWiFiHost::SendPacket(TCP_PACKET& pkt) return sent; } -IRAM_ATTR size_t CWiFiHost::SendData(const uint8_t* data, size_t size) { +IRAM_ATTR +size_t CWiFiHost::SendData(const uint8_t* data, size_t size) { if (data != nullptr) { m_nMode = MODE_SEND; m_pDataSend_data = (char *) data; @@ -735,7 +734,8 @@ IRAM_ATTR size_t CWiFiHost::SendData(const uint8_t* data, size_t size) { return 0; } -IRAM_ATTR // This function sends a small "chunk" then returns false. +IRAM_ATTR +// This function sends a small "chunk" then returns false. // It returns true ONLY when the entire buffer is finished. bool CWiFiHost::SendData(unsigned long clock) { @@ -767,7 +767,8 @@ bool CWiFiHost::SendData(unsigned long clock) return true; } -IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size) +IRAM_ATTR +size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size) { m_nMode = MODE_RECV; m_pDataReceive_data = (char *) data; @@ -779,7 +780,8 @@ IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size) return size; } -IRAM_ATTR bool CWiFiHost::ReceiveData(unsigned long clock) +IRAM_ATTR +bool CWiFiHost::ReceiveData(unsigned long clock) { // Receive Data size_t nSize = 0; @@ -822,7 +824,8 @@ IRAM_ATTR bool CWiFiHost::ReceiveData(unsigned long clock) return false; } -IRAM_ATTR void CWiFiHost::SendHeartBeat() { +IRAM_ATTR +void CWiFiHost::SendHeartBeat() { if (m_bHelloSent) { hostPacket.cmd = CMD_HEARTBEAT; time_t now = time(NULL); // Get current time in seconds @@ -833,7 +836,8 @@ IRAM_ATTR void CWiFiHost::SendHeartBeat() { } } -MY_IRAM_ATTR void CWiFiHost::Restart() { +MY_IRAM_ATTR +void CWiFiHost::Restart() { if (isWiFiConnected()) { if (m_bClientConnected && m_bClientConnected && wifiClient && wifiClient.connected()) { hostPacket.cmd = CMD_DROP_CONNECTION; diff --git a/WiFiHost.h b/WiFiHost.h index e651e70..0dd96e8 100644 --- a/WiFiHost.h +++ b/WiFiHost.h @@ -271,7 +271,10 @@ private: volatile bool m_bClientConnected; //uint32_t m_dwPublicIP; //uint16_t m_nPublicPort; + + // Send Status to VisionSoft HeartBeat service IPAddress m_cExternalServerIPAddress; + WiFiUDP udpLocal, udpExternal; WiFiServer wifiServer; //, wifiExternal; WiFiClient wifiClient; diff --git a/zcd.cpp b/zcd.cpp index e33bc89..ed3c17a 100644 --- a/zcd.cpp +++ b/zcd.cpp @@ -40,7 +40,7 @@ short getHeater2Duty() { } // Function to set the duty based on percentage (0 to 10000) -IRAM_ATTR void setHeater1Duty(short duty) { +ARDUINO_ISR_ATTR void setHeater1Duty(short duty) { if (duty <= 0) { dutyHeater1 = 0; // If 0% duty, no pulse (turn off TRIAC) } else if (duty >= 10000) { @@ -66,7 +66,7 @@ IRAM_ATTR void setHeater1Duty(short duty) { } // Function to set the duty based on percentage (0 to 10000) -IRAM_ATTR void setHeater2Duty(short duty) { +ARDUINO_ISR_ATTR void setHeater2Duty(short duty) { if (config.bAC2_OnOff) { if (duty >= 10000) { digitalWrite(PIN_HEATER2, HEATER_ON);