Note: staff-provided content does not represent an official statement from FARGOS Development, LLC. The policy on staff-authored content can be found here.
Go back to Geoff's home page.
This simple ESP8266-based project is intended to assist a driver with positioning their vehicle as they enter a garage so that it is parked at an appropriate spot. The driver is provided information via a string of WS2812B LED modules that are layered in several rows to form an information display.
The current time, if available, will be displayed when the system is idle due to a lack of vehicle movement. Normally the time is obtained from a public Network Time Protocol server by exploiting the WiFi-capable ESP8266's support for TCP/IP.
The positioning of a vehicle inside the garage can be determined by using LIDAR and ultrasonic sensors. The Benewake TFmini-S provides high resolution distance measurements over a distance of up to 12 meters, but is an order of magnitude more expensive compared to the other components.
In contrast, the common HC-SR04 module provides accuracy of up to 3 millimeters over a distance of up to 4 meters.
The Arduino IDE sketch implementing the control logic is found at the bottom of this document.
The schematic for the Parking Alignment Monitor appears below.
The ESP8266 uses 3.3V TTL logic and is not tolerant of 5V inputs, so bi-directional level shifters are used to convert between 3.3 volts and 5 volts as required. There are those who assert that the speed requirements of the WS2812B LED controller strings are such that a high speed logic level converter, such as a TXS0108E, are required. This is untrue and one may actually obtain far worse results than from using a cheap "IIC I2C Logic Level Converter".
Note: a single-pole, double-throw switch is tied to the analog pin A0 and used to select between a ground or 3.3 volt source. The pin is read by the control program to select between normal operation and a debug mode that displays the current sensor readings.
A trivial amount of voltage protection is provided by a 5.1 volt zener diode; it is not intended to provide protection against a power supply that is designed to be greater than 5 volts.
Pin | Name | Description | Used For |
---|---|---|---|
RX | RX | Receive pin for Serial UART | Debugging console |
TX | TX | Transmit pin for Serial UART | Debugging console |
D0 | Digital Pin 0 | Data Stream for WS2812B | Controls LED strip |
D1 | Digital Pin 1 | LIDAR Receive | Receive data from LIDAR sensor |
D2 | Digital Pin 2 | LIDAR Transmit | Send commands to LIDAR sensor |
D3 | Digital Pin 3 | Trigger 1 | Ultrasonic sensor 1 trigger |
D4 | Digital Pin 4 | Built-in LED | Reserved for activity light |
D5 | Digital Pin 5 | Echo 1 | Ultrasonic sensor 1 echo |
D6 | Digital Pin 6 | Trigger 2 | Ultrasonic sensor 2 trigger |
D7 | Digital Pin 7 | Echo 2 | Ultrasonic sensor 2 echo |
D8 | Digital Pin 8 | unused | currently unused |
Pin | Name | Description | Used For |
---|---|---|---|
A0 | Analog Pin 0 | Debug | Enable debug display |
The source code to the Parking Alignment Monitor application that runs on the ESP8266 is illustrated below. The most current release can be retrieved from this Parking Alignment Monitor source download link. The zip file contains the bulk of the source in a .ino file, which is really just C++ source with an alternate file suffix to permit association with the Arduino IDE application.
The comments within the source code provide more detail that will not be repeated here.
/*! \file Parking Monitor Display with Clock * * This ESP8266-based project drives a collection of WS2812B LED modules * to display information regarding the relative positioning of an automobile * with respect to its desired parking spot. * * The ESP8266 module's TCP/IP stack is exploited to automatically obtain the * time via NTP. The current time is subsequently displayed when the system * is idle due to a lack of any vehicle movement. * * Support for Benewake's TF Mini-S LIDAR sensor and the very common * HC-SR04 ultrasonic module is provided. * * Typical configuration is to use the expensive but extremely precise * LIDAR sensor for determining the depth of the vehicle within the garage * and one or two of the very inexpensive but less accurate ultrasonic sensors * to monitor the left/right offset of the vehicle with respect to its * desired parking spot. * * The distance remaining and offset from the center of the parking spot * are displayed when a vehicle enters the garage. Animated graphics also * provide a visual indication as to how much more distance needs to be * traveled. * * \sa http://www.fargos.net/parkingMonitor/parkingMonitor.html * * \author Geoff Carpenter gcc@fargos.net http://www.fargos.net/gcc.html * \date 2024/03/06 Initial public release */ #include <Adafruit_GFX.h> #include <FastLED.h> #include <FastLED_NeoMatrix.h> #include <SoftwareSerial.h> #include <sys/time.h> #include <time.h> /* Obtain local customizations */ #include "ParkingLocalConfig.h" #ifndef ENABLE_NTP #define ENABLE_NTP 1 /*!< Enable use of NTP to obtain current time */ #endif #if ENABLE_NTP #include <ESP8266WiFi.h> #include <TZ.h> #endif #ifndef NUM_LED_ROWS #define NUM_LED_ROWS 7 /*!< Number of rows of LEDs to control */ #endif #ifndef NUM_LED_COLS #define NUM_LED_COLS 50 /*!< Number of LEDs in each row */ #endif #if NUM_LED_ROWS > 6 #include "segoeui_5.h" /* use segoeui_5.h when 7 strips are available */ #define FONT_ARRAY segoeui5pt7b #else #include "segoeui_4.h" /* use segoeui_4.h only 6 strips are available */ #define FONT_ARRAY segoeui4pt7b #endif /* NOTE: all measurements are internally maintained as centimeters, * but can be displayed as inches on the LED panel. */ #ifndef USE_METRIC_DISPLAY #define USE_METRIC_DISPLAY 0 /*!< Indicate if display should show centimeters rather than inches. */ #endif #ifndef USE_LIDAR #define USE_LIDAR 1 /*!< Enable use of TFmini-S LIDAR sensor */ #endif #ifndef USE_ULTRASONIC #define USE_ULTRASONIC 1 /*!< Enable use of HC-SR04 ultrasonic sensor */ #endif #ifndef USE_DEPTH_SENSOR #define USE_DEPTH_SENSOR 1 /*!< Enable use of depth sensor */ #endif #ifndef USE_OFFSET_SENSORS #define USE_OFFSET_SENSORS 2 /*!< Number of offset sensors to use */ #endif #ifndef OFFSET1_SENSOR_ON_LEFT #define OFFSET1_SENSOR_ON_LEFT 1 /*!< Indicates if sensor 1 is to left of vehicle */ #endif #ifndef OFFSET2_SENSOR_ON_LEFT #define OFFSET2_SENSOR_ON_LEFT 1 /*!< Indicates if sensor 2 is to left of vehicle */ #endif #define ANIMATION_INTERVAL 100 /*!< Interval between animation frames in milliseconds */ #ifndef SIMULATE_LIDAR #define SIMULATE_LIDAR 0 /*!< If non-zero, simulates LIDAR depth sensor for testomg. Normally 0. */ #endif #ifndef SIMULATE_ULTRASONIC #define SIMULATE_ULTRASONIC 0 /*!< If non-zero, simulates ultrasonic offset sensors for testing. Normally 0. */ #endif #if ENABLE_NTP #ifndef USE_WIFI_SSID #error "Must define USE_WIFI_SSID in ParkingLocalConfig.h" #endif #ifndef USE_WIFI_PASSWORD #error "Must define USE_WIFI_PASSWORD in ParkingLocalConfig.h" #endif #ifndef USE_LOCAL_TZ #error "Must define USE_LOCAL_TZ in ParkingLocalConfig.h" #endif #ifndef USE_DEVICE_HOSTNAME #define USE_DEVICE_HOSTNAME "ParkingMonitor" /*!< Hostname offered in DHCP request */ #endif #endif /* ENABLE_NTP */ /* Parameters to define maximum expected values as derived from the * respective sensor data sheets. Can be adjusted if desired, such as * if dealing with a single bay garage. */ #define MAX_DEPTH_CM 600 /*!< Maximum feasible value from depth sensor; TFmini-S technically capable of 12 meters, but accuracy degrades after 6 meters. */ #define MAX_OFFSET_CM 400 /*!< Maximum feasible value from offset sensor. */ /* Parameters to define minimum expected values; below indicates blocked sensor */ #ifndef MIN_DEPTH_CM #define MIN_DEPTH_CM 10 /*!< Minimum feasible value from depth sensor. */ #endif #ifndef MIN_OFFSET1_CM #define MIN_OFFSET1_CM 2 /*!< Minimum feasible value from front offset sensor */ #endif #ifndef MIN_OFFSET2_CM #define MIN_OFFSET2_CM 2 /*!< Minimum feasible value from rear offset sensor */ #endif /* Parameters to define the desired depth of the parking spot */ #ifndef MIN_DESIRED_DEPTH_CM #define MIN_DESIRED_DEPTH_CM 40 /*!< Minimum physically-permitted depth within garage */ #endif #ifndef MAX_DESIRED_DEPTH_CM #define MAX_DESIRED_DEPTH_CM 500 /*!< Maximum reportable depth within garage */ #endif #ifndef TARGET_DESIRED_DEPTH_CM #define TARGET_DESIRED_DEPTH_CM 60 /*!< Desired stop depth */ #endif #ifndef DESIRED_DEPTH_TOLERANCE #define DESIRED_DEPTH_TOLERANCE_CM 10 /*!< Acceptable depth is TARGET_DESIRED_DEPTH_CM +/- DESIRED_DEPTH_TOLERANCE_CM */ #endif #ifndef MIN_INTERESTING_DEPTH_CHANGE_CM #define MIN_INTERESTING_DEPTH_CHANGE_CM 5 /*!< Amount of depth change required to be interesting */ #endif /* Parameters to define desired offset to parking spot for sensor 1 */ #ifndef MIN_DESIRED_OFFSET1_CM #define MIN_DESIRED_OFFSET1_CM 50 /*!< Minimum physically-permitted offset for sensor 1 */ #endif #ifndef MAX_DESIRED_OFFSET1_CM #define MAX_DESIRED_OFFSET1_CM 150 /*!< Maximum physically-permitted offset for sensor 1 */ #endif #ifndef TARGET_DESIRED_OFFSET1_CM #define TARGET_DESIRED_OFFSET1_CM 100 /*!< Desired offset from sensor 1 */ #endif /* Parameters to define desired offset to parking spot for sensor 2 */ #ifndef MIN_DESIRED_OFFSET2_CM #define MIN_DESIRED_OFFSET2_CM 50 /*!< Minimum physically-permitted offset for sensor 2 */ #endif #ifndef MAX_DESIRED_OFFSET2_CM #define MAX_DESIRED_OFFSET2_CM 150 /*!< Maximum physically-permitted offset for sensor 2 */ #endif #ifndef TARGET_DESIRED_OFFSET2_CM #define TARGET_DESIRED_OFFSET2_CM 100 /*!< Desired offset from sensor 2 */ #endif #ifndef DESIRED_OFFSET_TOLERANCE_CM #define DESIRED_OFFSET_TOLERANCE_CM 10 /*!< Tolerance for offset readings */ #endif #ifndef MIN_INTERESTING_OFFSET_CHANGE_CM #define MIN_INTERESTING_OFFSET_CHANGE_CM 5 /*!< Amount of offset change required to be interesting. */ #endif #define IDLE_SECS 20 /*!< Number of seconds of no movement before movement flags are cleared. */ #define SECONDS_UNTIL_DISPLAY_TIME 60 /*!< Number of seconds of no movement before current time of day is displayed */ #define ANNOUNCE_ARRIVE_DELAY 2 /*!< Number of seconds to hold static arrival message */ #define ANNOUNCE_DEPART_DELAY 5 /*!< Number of seconds to hold static departure message */ #define MAXIMUM_SENSOR_FAIL_COUNT 30 /*!< Number of unsuccessful requests before a sensor is declared as failed. */ #ifndef LOG_ENABLED /*! \brief Enables debug output to serial console. * * Normally 0, but can be set to nonzero to enable debug output to * the serial console. 2 also outputs the file name instead of just the * line number. * \note This must be nonzero to enable any debug output. All debug * output is inhibited if LOG_ENABLED is set to 0. * \sa ENABLE_CONSOLE_COMMANDS */ #define LOG_ENABLED 0 #endif #ifndef ENABLE_CONSOLE_COMMANDS #define ENABLE_CONSOLE_COMMANDS 0 /*!< Enable entering of debug commands via console serial line */ #endif #if (LOG_ENABLED == 0) && (ENABLE_CONSOLE_COMMANDS != 0) #error "LOG_ENABLED must be set when ENABLE_CONSOLE_COMMANDS is non-zero" #endif #ifndef DISPLAY_HEARTBEAT_PIXEL #define DISPLAY_HEARTBEAT_PIXEL 0 /*!< If non-zero, moves a colored pixel along the LED strip during each time update */ #endif /*! \brief Console baud rate * * This value should match the baud rate selected in the Arduino IDE's * serial monitor window or a dedicated terminal program like Putty. */ #define CONSOLE_BAUD_RATE 9600 /* These log interfaces are compatible with the advanced thread-safe * logging API made available by FARGOS Development, LLC. See http://www.fargos.net/documents/FARGOSutilsLibrary.html */ #if LOG_ENABLED > 0 #include <Streaming.h> #if LOG_ENABLED == 2 #define LOG_COUT(level) Serial << F(__FILE__) << F(":") << __LINE__ << F("\t") << F(#level) << F("\t") #else #define LOG_COUT(level) Serial << F(":") << __LINE__ << F("\t") << F(#level) << F("\t") #endif #define LOG_ENDLINE endl #endif /*! Current sensor readings are displayed if the analog pin is fed 3.3 VDC. * Normal output is enabled if the analog pin is connected to ground. */ #define DEBUG_PIN A0 /* NOTE: D4 is also the LED pin */ #define LIDAR_RX_PIN D1 /*!< Receive pin for LIDAR sensor (its green wire) */ #define LIDAR_TX_PIN D2 /*!< Transmit pin for LIDAR sensor (its white wire) */ /*! Pin used to trigger measurement request for ultrasonic sensor 1 */ #define SONIC1_TRIGGER_PIN D3 /*! Pin used to time duration of the echo pin of ultrasonic sensor 1 being held high */ #define SONIC1_DATA_PIN D5 /*! Pin used to trigger measurement request for ultrasonic sensor 2 */ #define SONIC2_TRIGGER_PIN D6 /*! Pin used to time duration of the echo pin of ultrasonic sensor 2 being held high */ #define SONIC2_DATA_PIN D7 /*! Pin used to send stream of color encodings to string of WS2812B LED modules */ #define LED_CONTROL_PIN D0 #define DO_LIDAR_CONFIG 1 /*!< Indicates if LIDAR sensor should be reconfigured each startup */ #define SAVE_LIDAR_CONFIG 0 /*!< Indicates if current settings should be saved by LIDAR sensor */ #define DO_LIDAR_FACTORY_RESET 0 /*!< Set non-zero to force a factory reset of TFmini-S */ #define INITIAL_LIDAR_BAUD_RATE 9600 /*!< Factory setting is 115200 */ #define NEW_LIDAR_BAUD_RATE 9600 /*!< Set slow enough for reliable operation using SoftwareSerial */ #define ULTRASONIC_TRIGGER_DURATION 10 /*!< Duration of trigger pulse, minimum 10 microseconds */ #define ULTRASONIC_MAX_SAMPLE_TIME 50000UL /*!< maximum time to wait for response on echo line */ #define DEFAULT_BRIGHTNESS 32 /*!< max possible is 255 */ #ifndef PANEL_ORIGIN /*! Default panel configuration is a zig-zag chain with the initial connection point being in * the upper right hand corner. */ #define PANEL_ORIGIN (NEO_MATRIX_TOP + NEO_MATRIX_RIGHT) #endif #ifndef STRIP_LAYOUT /*! Default panel configuration is a collection of strips, connected in alternating back-and-forth * manner. */ #define STRIP_LAYOUT NEO_MATRIX_ZIGZAG #endif // This could also be defined as matrix->color(255,0,0) but those defines // are meant to work for adafruit_gfx backends that are lacking color() #define LED_BLACK 0 #define LED_RED_VERYLOW (3 << 11) #define LED_RED_LOW (7 << 11) #define LED_RED_MEDIUM (15 << 11) #define LED_RED_HIGH (31 << 11) #define LED_GREEN_VERYLOW (1 << 5) #define LED_GREEN_LOW (15 << 5) #define LED_GREEN_MEDIUM (31 << 5) #define LED_GREEN_HIGH (63 << 5) #define LED_BLUE_VERYLOW 3 #define LED_BLUE_LOW 7 #define LED_BLUE_MEDIUM 15 #define LED_BLUE_HIGH 31 #define LED_ORANGE_VERYLOW (LED_RED_VERYLOW + LED_GREEN_VERYLOW) #define LED_ORANGE_LOW (LED_RED_LOW + LED_GREEN_LOW) #define LED_ORANGE_MEDIUM (LED_RED_MEDIUM + LED_GREEN_MEDIUM) #define LED_ORANGE_HIGH (LED_RED_HIGH + LED_GREEN_HIGH) #define LED_PURPLE_VERYLOW (LED_RED_VERYLOW + LED_BLUE_VERYLOW) #define LED_PURPLE_LOW (LED_RED_LOW + LED_BLUE_LOW) #define LED_PURPLE_MEDIUM (LED_RED_MEDIUM + LED_BLUE_MEDIUM) #define LED_PURPLE_HIGH (LED_RED_HIGH + LED_BLUE_HIGH) #define LED_CYAN_VERYLOW (LED_GREEN_VERYLOW + LED_BLUE_VERYLOW) #define LED_CYAN_LOW (LED_GREEN_LOW + LED_BLUE_LOW) #define LED_CYAN_MEDIUM (LED_GREEN_MEDIUM + LED_BLUE_MEDIUM) #define LED_CYAN_HIGH (LED_GREEN_HIGH + LED_BLUE_HIGH) #define LED_WHITE_VERYLOW (LED_RED_VERYLOW + LED_GREEN_VERYLOW + LED_BLUE_VERYLOW) #define LED_WHITE_LOW (LED_RED_LOW + LED_GREEN_LOW + LED_BLUE_LOW) #define LED_WHITE_MEDIUM (LED_RED_MEDIUM + LED_GREEN_MEDIUM + LED_BLUE_MEDIUM) #define LED_WHITE_HIGH (LED_RED_HIGH + LED_GREEN_HIGH + LED_BLUE_HIGH) #define LED_RGB(r, g, b) (((r) << 1) | ((g) << 5) | (B)) #define LED_VERYLOW 3 #define LED_LOW 5 #define LED_MEDIUM 7 #define LED_HIGH 15 #define LED_VERYHIGH 31 #ifndef FORCE_INTENSE_COLORS #define FORCE_INTENSE_COLORS 0 /*!< if non-zero, use bright colors regardless if debugging */ #endif #if (LOG_ENABLED != 0) && (FORCE_INTENSE_COLORS == 0) /* If connected to a computer's USB port, use low intensity colors for * reduced power requirements. */ #define LED_RED LED_RED_LOW #define LED_BLUE LED_BLUE_LOW #define LED_GREEN LED_GREEN_LOW #define LED_ORANGE LED_ORANGE_LOW #else #define LED_RED LED_RED_HIGH #define LED_BLUE LED_BLUE_HIGH #define LED_GREEN LED_GREEN_HIGH #define LED_ORANGE LED_ORANGE_HIGH #endif #define LED_TIME_COLOR LED_BLUE #if ENABLE_CONSOLE_COMMANDS > 0 static int32_t forceDepth = -1; static int32_t forceOffset = -1; #endif #if USE_LIDAR /*! \brief Control and obtain object depth from a TFmini-S LIDAR sensor. */ class TFmini_S_LIDARsensor { public: enum { TFMINI_DEFAULT_BAUD_RATE = 115200, //!< factory default MAX_TFMINI_RESPONSE_TIME=50 //!< Maximum milliseconds to wait for measurement }; protected: SoftwareSerial serialPort; uint16_t selectedFrameRate; uint8_t bfrIndex; uint8_t failedCount; unsigned char recvBfr[9]; uint8_t computeChecksum(unsigned char bytes[], uint8_t len) { uint8_t checksum = 0; for (uint8_t i = 0; i < len; i += 1) { checksum += bytes[i]; } return (checksum); } bool waitForData(unsigned long maxWait) { if (serialPort.available()) return (true); unsigned long curTime = millis(); unsigned long stopTime = curTime + maxWait; while (curTime < stopTime) { // max wait time not yet reached if (serialPort.available()) return (true); curTime = millis(); } return (false); } int8_t readResponse(uint8_t expectedLen, unsigned long maxWaitTime = 10) { bool dataAvailable; bfrIndex = 0; unsigned long stopMillis = millis() + maxWaitTime; do { while (serialPort.available()) { recvBfr[bfrIndex] = serialPort.read(); bfrIndex += 1; switch (bfrIndex) { case 1: if (recvBfr[0] != 0x5a) { // start of message not yet found #if LOG_ENABLED LOG_COUT(warn) << F("not header=") << recvBfr[0] << LOG_ENDLINE; #endif bfrIndex = 0; // restart } break; case 2: if (recvBfr[1] != expectedLen) { // corrupted response #if LOG_ENABLED LOG_COUT(warn) << F("wrong len=") << recvBfr[1] << LOG_ENDLINE; #endif bfrIndex = 0; // reset } break; default: if (bfrIndex == expectedLen) { return (bfrIndex); } break; } // end switch bfrIndex } // end while available unsigned long currentMillis = millis(); if (currentMillis >= stopMillis) break; dataAvailable = waitForData(stopMillis - currentMillis); } while (dataAvailable); return (bfrIndex); } int8_t readDataFrame(unsigned long maxWaitTime) { do { while (serialPort.available()) { recvBfr[bfrIndex] = serialPort.read(); switch (bfrIndex) { case 0: case 1: if (recvBfr[bfrIndex] != 0x59) { // haven't found header of 0x59 0x59 bfrIndex = 0; continue; } break; case 2: if (recvBfr[2] == 0x59) { // assume stream of header markers bfrIndex = 1; continue; } break; case 8: { uint16_t checksum = 0; for (uint8_t j = 0; j < 8; j += 1) { checksum += recvBfr[j]; } bfrIndex = 0; if (recvBfr[8] == (checksum & 255)) { // success return (9); } // failed #if LOG_ENABLED LOG_COUT(error) << F("bad checksum=") << (int)(checksum & 255) << F(" vs ") << (int)recvBfr[8] << LOG_ENDLINE; for (uint8_t i = 0; i <= 8; i += 1) { LOG_COUT(info) << F("bfr[") << i << F("] = ") << recvBfr[i] << LOG_ENDLINE; } #endif return (0); } // NOTREACHED default: break; } // end switch bfrIndex bfrIndex += 1; } // end while serial data available if (maxWaitTime != 0) { bool dataAvailable = waitForData(maxWaitTime); if (dataAvailable == false) break; } } while (maxWaitTime != 0); return (bfrIndex); } public: TFmini_S_LIDARsensor(uint8_t rxPin, uint8_t txPin) : serialPort(rxPin, txPin) { selectedFrameRate = 100; // default from specification failedCount = 0; bfrIndex = 0; pinMode(rxPin, INPUT); if (txPin != rxPin) { pinMode(txPin, OUTPUT); } } ~TFmini_S_LIDARsensor() {} void init(uint32_t baudRate = TFMINI_DEFAULT_BAUD_RATE) { // Initialize the data rate for the SoftwareSerial port serialPort.begin(baudRate); } #if DO_LIDAR_CONFIG bool changeBaudRate(uint32_t newRate) { unsigned char cmd[8]; uint32_t desiredRate = newRate; cmd[0] = 0x5a; cmd[1] = 0x08; cmd[2] = 0x06; cmd[3] = newRate & 255; newRate >>= 8; cmd[4] = newRate & 255; newRate >>= 8; cmd[5] = newRate & 255; newRate >>= 8; cmd[6] = newRate & 255; cmd[7] = computeChecksum(cmd, sizeof(cmd) - 1); serialPort.write(cmd, sizeof(cmd)); bool dataAvail = waitForData(100); // wait a bit for response if (dataAvail == false) { #if LOG_ENABLED LOG_COUT(error) << F("No response to baud rate change") << LOG_ENDLINE; #endif return (false); } int8_t len = readResponse(8); #if LOG_ENABLED LOG_COUT(info) << F("response len=") << len << LOG_ENDLINE; #endif if (len == 8) { #if LOG_ENABLED LOG_COUT(info) << F("Change baud rate response=") << recvBfr[2] << LOG_ENDLINE; #endif if (recvBfr[2] == 6) { // was change baud rate response #if LOG_ENABLED LOG_COUT(info) << F("saving settings") << LOG_ENDLINE; #endif #if SAVE_LIDAR_CONFIG saveSettings(); #endif serialPort.begin(desiredRate); return (true); } } return (false); } bool changeFrameRate(uint16_t newRate) { unsigned char cmd[6]; selectedFrameRate = newRate; // send command to change LIDAR device frame rate cmd[0] = 0x5a; cmd[1] = 0x06; cmd[2] = 0x03; cmd[3] = newRate & 255; newRate >>= 8; cmd[4] = newRate & 255; cmd[5] = computeChecksum(cmd, sizeof(cmd) - 1); serialPort.write(cmd, sizeof(cmd)); bool dataAvail = waitForData(100); // wait a bit for response if (dataAvail == false) { #if LOG_ENABLED LOG_COUT(error) << F("No response to frame rate change") << LOG_ENDLINE; #endif return (false); } int8_t len = readResponse(6); #if LOG_ENABLED LOG_COUT(info) << F("response len=") << len << LOG_ENDLINE; #endif if (len == 6) { #if LOG_ENABLED LOG_COUT(info) << F("Change Frame Rate response=") << recvBfr[3] << LOG_ENDLINE; #endif if (recvBfr[2] == 3) { // was change frame response return (true); } } return (false); } #if DO_LIDAR_FACTORY_RESET bool restoreFactorySettings() { unsigned char cmd[4]; cmd[0] = 0x5a; cmd[1] = 0x04; cmd[2] = 0x10; cmd[3] = 0x6e; serialPort.write(cmd, sizeof(cmd)); bool dataAvail = waitForData(2000); // wait 2 seconds for response if (dataAvail == false) { LOG_COUT(error) << F("No response to reset to factory settings") << LOG_ENDLINE; return (false); } int8_t len = readResponse(5); if (len == 5) { LOG_COUT(info) << F("restore factory settings response=") << (int)recvBfr[3] << LOG_ENDLINE; if (recvBfr[2] == 0x10) { return (true); } } return (false); } #endif #if SAVE_LIDAR_CONFIG bool saveSettings() { unsigned char cmd[4]; cmd[0] = 0x5a; cmd[1] = 0x04; cmd[2] = 0x11; cmd[3] = 0x6f; serialPort.write(cmd, sizeof(cmd)); bool dataAvail = waitForData(2000); // wait 2 seconds for response if (dataAvail == false) { LOG_COUT(error) << F("No response to save settings") << LOG_ENDLINE; return (false); } int8_t len = readResponse(5); if (len == 5) { LOG_COUT(info) << F("Save settings response=") << (int)recvBfr[3] << LOG_ENDLINE; if (recvBfr[2] == 0x11) { return (true); } } return (false); } #endif void requestMeasurement() { unsigned char cmd[4]; cmd[0] = 0x5a; cmd[1] = 0x04; cmd[2] = 0x04; cmd[3] = 0x62; serialPort.write(cmd, sizeof(cmd)); } #endif bool sensorHasFailed() const { return (failedCount >= MAXIMUM_SENSOR_FAIL_COUNT); } bool getDistanceReading(uint16_t *distance, uint16_t *strength, uint16_t *temp = nullptr) { #if SIMULATE_LIDAR /* generate simulate results rather than query a real sensor */ static uint16_t simDistance = MAX_DEPTH_CM + 10; static int16_t moveDirection = -1; static time_t lastTime; #if ENABLE_CONSOLE_COMMANDS > 0 if (forceDepth != -1) { *distance = forceDepth; return (true); } #endif unsigned long secondsSinceBoot = millis() / 1000; if (secondsSinceBoot != lastTime) { lastTime = secondsSinceBoot; if (simDistance < (MIN_DEPTH_CM / 2)) { moveDirection = 1; } else if (simDistance > (MAX_DEPTH_CM + 60)) { moveDirection = -1; } else if ((secondsSinceBoot > 360) && (moveDirection == 0)) { moveDirection = -2; } else if ((secondsSinceBoot > 300) && (secondsSinceBoot <= 360)) { moveDirection = 0; } if (secondsSinceBoot > 10) { simDistance += moveDirection; } } *distance = simDistance; if (strength != nullptr) { *strength = 200; } if (temp != nullptr) { *temp = 30; } return (true); /* NOTREACHED */ #endif /* SIMULATE_LIDAR */ if (selectedFrameRate == 0) { // request measurement on demand requestMeasurement(); bool dataArrived = waitForData(MAX_TFMINI_RESPONSE_TIME); // wait for response bfrIndex = 0; if (dataArrived == false) { #if LOG_ENABLED LOG_COUT(warn) << F("No LIDAR response after request") << LOG_ENDLINE; #endif if (failedCount < MAXIMUM_SENSOR_FAIL_COUNT) { failedCount += 1; } return (false); } } int8_t readLen = readDataFrame((selectedFrameRate == 0) ? 2 : 0); #if LOG_ENABLED if (readLen != 9) { LOG_COUT(error) << F("only got ") << readLen << F(" bytes in response") << LOG_ENDLINE; for (uint8_t i = 0; i < readLen; i += 1) { LOG_COUT(info) << F("bfr[") << i << F("] = ") << recvBfr[i] << LOG_ENDLINE; } } #endif if (readLen == 9) { // entire data frame read *distance = recvBfr[2] + recvBfr[3] * 256; if (strength != nullptr) { *strength = recvBfr[4] + recvBfr[5] * 256; } if (temp != nullptr) { *temp = recvBfr[6] + recvBfr[7] * 256; } failedCount = 0; // reset return (true); } #if LOG_ENABLED LOG_COUT(warn) << F("No LIDAR response bfrIndex=") << bfrIndex << LOG_ENDLINE; #endif if (failedCount < MAXIMUM_SENSOR_FAIL_COUNT) { failedCount += 1; } return (false); } }; // end class TFmini_S_LIDARsensor #endif #if USE_ULTRASONIC /*! Poll HC-SR04 ultrasonic sensor for object distance. */ class HC_SR04sensor { protected: uint8_t triggerPin; uint8_t echoPin; uint8_t failedCount; public: HC_SR04sensor(uint8_t trigPin, uint8_t echoPin) : triggerPin(trigPin), echoPin(echoPin) { failedCount = 0; } ~HC_SR04sensor() {} void init() { pinMode(triggerPin, OUTPUT); pinMode(echoPin, INPUT); digitalWrite(triggerPin, LOW); // force low } bool sensorHasFailed() const { return (failedCount >= MAXIMUM_SENSOR_FAIL_COUNT); } bool getDistanceReading(uint32_t *distance, unsigned long *strength) { #if SIMULATE_ULTRASONIC static uint32_t simOffset = MIN_OFFSET1_CM; static int32_t dir = 1; static time_t lastTime; #if ENABLE_CONSOLE_COMMANDS > 0 if (forceOffset != -1) { *distance = forceOffset; if (strength != nullptr) { *strength = 2000; } return (true); } #endif unsigned long secondsSinceBoot = millis() / 1000; if (secondsSinceBoot != lastTime) { if (simOffset > (MAX_OFFSET_CM + 10)) dir = -1; else if (simOffset < (MIN_OFFSET1_CM / 2)) dir = 1; simOffset += dir; } *distance = simOffset; if (strength != nullptr) { *strength = 1000; } return (true); #endif digitalWrite(triggerPin, HIGH); // raise trigger signal, must hold for at least 10 microseconds delayMicroseconds(ULTRASONIC_TRIGGER_DURATION); noInterrupts(); // disable interrupts for precise timing digitalWrite(triggerPin, LOW); // test triggered by signal being dropped low unsigned long duration = pulseIn(echoPin, HIGH, ULTRASONIC_MAX_SAMPLE_TIME); interrupts(); if (duration == 0) { // failed reading if (strength != nullptr) { *strength = 0; } #if LOG_ENABLED LOG_COUT(error) << F("No response on echo pin=") << echoPin << F(" trigger=") << triggerPin << LOG_ENDLINE; #endif if (failedCount < MAXIMUM_SENSOR_FAIL_COUNT) { failedCount += 1; } return (false); } #if LOG_ENABLED if (duration > 36000) { LOG_COUT(warn) << F("Duration too long=") << duration << LOG_ENDLINE; } #endif failedCount = 0; float dist = (duration * .0343) / 2; // centimeters per microsecond, then halved // inches per microsecond = 0.0135039, halved=0.00675195 #if LOG_ENABLED > 1 LOG_COUT(info) << F("Distance=") << dist << F(" cm") << LOG_ENDLINE; #endif *distance = static_cast<int>(dist); if (strength != nullptr) { *strength = duration; } return (true); } }; // end class HC_SR04sensor #endif /* USE_ULTRASONIC */ /*! \brief Control strips of WS2812B LED modules. * * Displays text and animated graphics on LED panel comprised of * multiple rows of WS2812B LED modules. * * \note Makes heavy use of FastLED_NeoMatrix. */ template <uint8_t DATA_PIN, int8_t COLS, int8_t ROWS = 1> class LEDstrips { public: enum { MAX_MESSAGE_DISPLAY_LEN = 128, MIN_UPDATE_INTERVAL_MILLIS = 100, //!< minimum update interval in milliseconds NUM_LEDS = ROWS * COLS, TIME_TEXT_X_ORIGIN = 4, #if NUM_LED_ROWS > 6 TEXT_Y_ORIGIN = 6, #else TEXT_Y_ORIGIN = 5, #endif MAX_DISPLAY_PHASES = 10, ANIMATION_PHASES = 4 }; enum eDisplayContent { SCROLL_BLOCK_LEFT = 1, SCROLL_BLOCK_RIGHT = 2, SCROLL_DIRECTION_MASK = (SCROLL_BLOCK_LEFT | SCROLL_BLOCK_RIGHT), SCROLL_LEFT_SIDE = 4, SCROLL_RIGHT_SIDE = 8, SCROLL_MIDDLE = 16, SCROLL_SIDE_MASK = (SCROLL_LEFT_SIDE | SCROLL_RIGHT_SIDE | SCROLL_MIDDLE), SCROLL_ENTIRE_BLOCK_LEFT = (SCROLL_LEFT_SIDE | SCROLL_RIGHT_SIDE | SCROLL_BLOCK_LEFT | SCROLL_MIDDLE), DISPLAY_TIME = 32, DISPLAY_MESSAGE = 64, DISPLAY_GRAPHIC = 128, DISPLAY_BLINKING_MESSAGE = 256, DISPLAY_SCROLLED_MESSAGE = (DISPLAY_MESSAGE | SCROLL_ENTIRE_BLOCK_LEFT), DISPLAY_LEFT_WARNING = (DISPLAY_GRAPHIC | SCROLL_LEFT_SIDE | SCROLL_BLOCK_RIGHT), DISPLAY_RIGHT_WARNING = (DISPLAY_GRAPHIC | SCROLL_RIGHT_SIDE | SCROLL_BLOCK_LEFT) }; protected: CRGB ledDataWithSafety[NUM_LEDS + 1]; CRGB *const ledData; FastLED_NeoMatrix matrix; uint16_t messageColor; uint16_t messageBackground; int16_t messageText_X_origin; int16_t markerDirection; uint16_t markerColor; int16_t marker_X_origin; uint16_t targetBoxColor; int16_t targetBox_X; #if DISPLAY_HEARTBEAT_PIXEL uint16_t lastPixel; uint8_t heartbeatColor; #endif public: char currentTimeText[12]; char depthMessage[16]; char offsetMessage[16]; char messageToDisplay[MAX_MESSAGE_DISPLAY_LEN]; uint16_t activeContentMode; bool timeChanged; protected: static constexpr int16_t XY(uint8_t x, uint8_t y) { // odd rows run backwards return ((y & 1) ? ((y * COLS) + (COLS - 1) - x) : ((y * COLS) + x)); } static int16_t XYsafe(uint8_t x, uint8_t y) { if ((x >= COLS) || (y >= ROWS)) return (-1); return (XY(x, y)); } #if 0 void moveBlockLeft1(uint8_t firstCol, uint8_t lastCol) { for (uint8_t c = firstCol; c < lastCol; c += 1) { for (uint8_t r = 0; r < ROWS; r += 1) { uint8_t destPixel = XYsafe(c, r); uint8_t sourcePixel = XYsafe(c + 1, r); ledData[destPixel] = ledData[sourcePixel]; } } for (uint8_t r = 0; r < ROWS; r += 1) { uint8_t destPixel = XYsafe(lastCol, r); ledData[destPixel] = CRGB::Black; } } void moveBlockRight1(uint8_t firstCol, uint8_t lastCol) { for (uint8_t c = lastCol; c >= firstCol; c -= 1) { for (uint8_t r = 0; r < ROWS; r += 1) { uint8_t sourcePixel = XYsafe(c, r); uint8_t destPixel = XYsafe(c + 1, r); ledData[destPixel] = ledData[sourcePixel]; } } for (uint8_t r = 0; r < ROWS; r += 1) { uint8_t destPixel = XYsafe(firstCol, r); ledData[destPixel] = CRGB::Black; } } #endif void drawOffsetMarker(int32_t x, uint16_t color, int16_t dir, uint8_t phaseNumber) { // LOG_COUT(info) << F("drawMarker x=") << x << F(" color=") << color << LOG_ENDLINE; if (phaseNumber == 0) { matrix.fillRect(0, 0, COLS, ROWS, LED_BLACK); } int x_dir = (dir == 0) ? 0 : ((dir >= 0) ? -1 : 1); int x_offset = phaseNumber * x_dir; int cur_x = x + x_offset; if (x_dir >= 0) { while (cur_x < (COLS / 2)) { for (int y = phaseNumber; y < (ROWS - phaseNumber); y += 1) { matrix.drawPixel(cur_x, y, color); } cur_x += 5; } } else { while (cur_x > (COLS / 2)) { for (int y = phaseNumber; y < (ROWS - phaseNumber); y += 1) { matrix.drawPixel(cur_x, y, color); } cur_x -= 5; } } } void drawTargetBox(int32_t x, uint16_t color, uint8_t phaseNumber) { int32_t h = ROWS - (phaseNumber * 2); if (h < 1) h = 1; matrix.drawRect(x + phaseNumber, phaseNumber, h, h, color); } void drawMessage(uint8_t phaseNumber) { #if 0 int16_t messageTextBox_X; int16_t messageTextBox_Y; uint16_t messageTextBox_width; uint16_t messageTextBox_height; matrix.getTextBounds(messageToDisplay, messageText_X_origin, TEXT_Y_ORIGIN, &messageTextBox_X, &messageTextBox_Y, &messageTextBox_width, &messageTextBox_height); // TODO: use black for fill matrix.fillRect(messageTextBox_X, messageTextBox_Y, messageTextBox_width, messageTextBox_height, messageBackground); #endif matrix.fillRect(0, 0, COLS, ROWS, LED_BLACK); if ((phaseNumber & 1) == 0) { // even numbered phase matrix.setTextColor(messageColor, messageBackground); matrix.setCursor(messageText_X_origin, TEXT_Y_ORIGIN); matrix.print(messageToDisplay); } #if LOG_ENABLED LOG_COUT(info) << F("phase=") << phaseNumber << F(" drawMessage x=") << messageText_X_origin << F(" mess=") << messageToDisplay << LOG_ENDLINE; #endif } void drawTimeDisplay(uint8_t phaseNumber) { #if LOG_ENABLED > 1 unsigned long curTime = millis(); LOG_COUT(info) << F("drawTimeDisplay millis=") << curTime << F(" timeChanged=") << timeChanged << F(" text=") << currentTimeText << LOG_ENDLINE; #endif if (timeChanged == false) { return; } timeChanged = false; // clear bounding box matrix.fillRect(0, 0, COLS, ROWS, LED_BLACK); matrix.setTextColor(LED_TIME_COLOR, LED_BLACK); matrix.setCursor(TIME_TEXT_X_ORIGIN, TEXT_Y_ORIGIN); matrix.print(currentTimeText); #if DISPLAY_HEARTBEAT_PIXEL ledData[lastPixel] = CRGB::Black; lastPixel += 1; if (lastPixel >= NUM_LEDS) { lastPixel = 0; heartbeatColor += 1; if (heartbeatColor >= 3) heartbeatColor = 0; } switch (heartbeatColor) { case 0: ledData[lastPixel] = CRGB::Red; break; case 1: ledData[lastPixel] = CRGB::Green; break; case 2: ledData[lastPixel] = CRGB::Blue; break; } #if LOG_ENABLED LOG_COUT(info) << F("lastPixel=") << lastPixel << LOG_ENDLINE; #endif #endif /* DISPLAY_HEARTBEAT_PIXEL */ } public: LEDstrips() : ledData(ledDataWithSafety + 1), matrix(ledData, COLS, ROWS, PANEL_ORIGIN + STRIP_LAYOUT) { messageColor = LED_RED; markerColor = LED_BLACK; timeChanged = false; depthMessage[0] = '\0'; offsetMessage[0] = '\0'; } void init() { // pinMode(LED_CONTROL_PIN, OUTPUT); FastLED.addLeds<WS2812B, DATA_PIN, GRB>(ledData, NUM_LEDS).setCorrection(TypicalSMD5050); matrix.begin(); matrix.cp437(true); // use correct character codes matrix.setTextWrap(false); matrix.setBrightness(DEFAULT_BRIGHTNESS); matrix.fillScreen(LED_BLACK); matrix.setFont(&FONT_ARRAY); matrix.setTextSize(1); #if DISPLAY_HEARTBEAT_PIXEL lastPixel = NUM_LEDS - 1; heartbeatColor = 0; #endif #if LOG_ENABLED LOG_COUT(info) << F("MAC=") << WiFi.macAddress() << LOG_ENDLINE; #endif setMessage(WiFi.macAddress().c_str(), 0, LED_PURPLE_HIGH); for (int8_t i = 0; i < 10; i += 1) { drawMessage(0); FastLED.show(); FastLED.delay(1000); } } void setMessage(const char *message, uint8_t origin_x = 0, uint16_t color = LED_BLUE, uint16_t background = LED_BLACK) { if (message[0] == '\0') { activeContentMode = DISPLAY_TIME; messageToDisplay[0] = '\0'; return; } strncpy(messageToDisplay, message, sizeof(messageToDisplay) - 1); messageToDisplay[sizeof(messageToDisplay) - 1] = '\0'; messageText_X_origin = origin_x; messageColor = color; messageBackground = background; activeContentMode = DISPLAY_MESSAGE; } void setOffsetLine(int16_t x, int32_t relativeOffset, uint16_t color, uint8_t sensorId) { marker_X_origin = x; markerDirection = static_cast<int16_t>(relativeOffset); markerColor = color; } void setTargetBoxOrigin(int16_t x, uint16_t color) { targetBox_X = x; targetBoxColor = color; } void refreshDisplay(uint8_t phaseNumber) { #if LOG_ENABLED > 1 LOG_COUT(info) << F("activeContentMode=") << activeContentMode << F(" phase=") << phaseNumber << LOG_ENDLINE; #endif switch (activeContentMode) { case DISPLAY_TIME: if (phaseNumber == 0) { #if LOG_ENABLED LOG_COUT(info) << F("DISPLAY_TIME phase=") << phaseNumber << LOG_ENDLINE; #endif drawTimeDisplay(phaseNumber); } break; case DISPLAY_MESSAGE: case DISPLAY_BLINKING_MESSAGE: if (phaseNumber < (MAX_DISPLAY_PHASES - ANIMATION_PHASES)) { #if LOG_ENABLED > 1 LOG_COUT(info) << F("DISPLAY_MESSAGE") << LOG_ENDLINE; #endif if ((phaseNumber == 0) || (activeContentMode == DISPLAY_BLINKING_MESSAGE)) { drawMessage(phaseNumber); } } else { uint8_t drawPhase = phaseNumber - (MAX_DISPLAY_PHASES - ANIMATION_PHASES); if (markerColor != LED_BLACK) { drawOffsetMarker(marker_X_origin, markerColor, markerDirection, drawPhase); } if (targetBoxColor != LED_BLACK) { if (drawPhase > 0) { // erase previous rectangle outline drawTargetBox(targetBox_X, LED_BLACK, drawPhase - 1); } drawTargetBox(targetBox_X, targetBoxColor, drawPhase); } } break; case DISPLAY_SCROLLED_MESSAGE: // FALLTHROUGH for now default: #if LOG_ENABLED LOG_COUT(warn) << F("Unsupported display mode=") << activeContentMode << LOG_ENDLINE; #endif break; } } }; // end class LEDstrips<> template <uint8_t DATA_PIN, int8_t COLS, int8_t ROWS> class ParkingMonitor { protected: enum eDisplayMode { NOT_MOVING = 0, MOVING_FORWARD = 1, MOVING_BACKWARD = 2, MOVING_LEFT = 4, MOVING_RIGHT = 8, MOVEMENT_MASK = MOVING_FORWARD | MOVING_BACKWARD | MOVING_LEFT | MOVING_RIGHT, DISPLAY_CURRENT_TIME = 16, DISPLAY_DEPTH = 32, DISPLAY_DEPTH_MOVEMENT_MASK = MOVING_FORWARD | MOVING_BACKWARD, DISPLAY_OFFSET = 64, DISPLAY_INITIAL = 128, DISPLAY_ARRIVING = DISPLAY_DEPTH | MOVING_FORWARD | DISPLAY_INITIAL, DISPLAY_DEPARTING = DISPLAY_DEPTH | MOVING_BACKWARD | DISPLAY_INITIAL, DISPLAY_MASK = DISPLAY_CURRENT_TIME | DISPLAY_DEPTH | DISPLAY_OFFSET | DISPLAY_INITIAL, DISPLAY_STOP_HERE = 256, DISPLAY_CAUTION = 512, DISPLAY_TOO_FAR = 1024, DISPLAY_BLOCKED_SENSOR = 2048, DISPLAY_FAILED_SENSOR = 4096, DISPLAY_ALERT_MASK_WITHOUT_BLOCKED = DISPLAY_STOP_HERE | DISPLAY_CAUTION | DISPLAY_TOO_FAR | DISPLAY_FAILED_SENSOR, DISPLAY_ALERT_MASK_WITHOUT_FAILED = DISPLAY_STOP_HERE | DISPLAY_CAUTION | DISPLAY_TOO_FAR | DISPLAY_BLOCKED_SENSOR, DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED = DISPLAY_STOP_HERE | DISPLAY_CAUTION | DISPLAY_TOO_FAR, DISPLAY_ALERT_MASK = DISPLAY_ALERT_MASK_WITHOUT_BLOCKED | DISPLAY_BLOCKED_SENSOR }; enum eSensorIdMask { DEPTH_SENSOR_MASK = 1, OFFSET1_SENSOR_MASK = 2, OFFSET2_SENSOR_MASK = 4 }; LEDstrips<DATA_PIN, COLS, ROWS> ledController; #if USE_DEPTH_SENSOR TFmini_S_LIDARsensor depthDevice; #endif #if USE_OFFSET_SENSORS > 0 HC_SR04sensor offsetDevice1; #if USE_OFFSET_SENSORS > 1 HC_SR04sensor offsetDevice2; #endif #endif time_t lastTime; time_t lastUpdateDepthTime; time_t lastUpdateOffsetTime; time_t nextIdleTime; time_t timeUntilDisplayChange; int32_t lastDepth; #if USE_OFFSET_SENSORS > 0 int32_t lastOffset[USE_OFFSET_SENSORS]; #endif uint16_t displayMode; uint8_t displayPhase; uint8_t blockedSensorMask; uint8_t failedSensorMask; public: enum { MAX_DISPLAY_PHASES = LEDstrips<DATA_PIN, COLS, ROWS>::MAX_DISPLAY_PHASES }; ParkingMonitor() : #if USE_DEPTH_SENSOR depthDevice(LIDAR_RX_PIN, LIDAR_TX_PIN), #endif #if USE_OFFSET_SENSORS > 0 offsetDevice1(SONIC1_TRIGGER_PIN, SONIC1_DATA_PIN), #if USE_OFFSET_SENSORS > 1 offsetDevice2(SONIC2_TRIGGER_PIN, SONIC2_DATA_PIN), #endif #endif lastTime(0), lastUpdateDepthTime(0), lastUpdateOffsetTime(0), nextIdleTime(0), timeUntilDisplayChange(0), displayMode(DISPLAY_CURRENT_TIME), displayPhase(0), blockedSensorMask(0), failedSensorMask(0) { #if USE_OFFSET_SENSORS > 0 for (uint8_t i = 0; i < USE_OFFSET_SENSORS; i += 1) { lastOffset[i] = 0; } #endif } void init() { #if USE_DEPTH_SENSOR #if LOG_ENABLED LOG_COUT(info) << F("Initial LIDAR baud rate=") << INITIAL_LIDAR_BAUD_RATE << F(" desired baud rate=") << NEW_LIDAR_BAUD_RATE << LOG_ENDLINE; #endif uint32_t currentBaudRate = INITIAL_LIDAR_BAUD_RATE; depthDevice.init(currentBaudRate); #if DO_LIDAR_FACTORY_RESET bool resetSuccessful = false; for (int t = 0; t < 5; t += 1) { #if LOG_ENABLED LOG_COUT(info) << F("Attempt reset rate=" << currentBaudRate << " attempt=" << t << LOG_ENDLINE; #endif resetSuccessful = depthDevice.restoreFactorySettings(); if (resetSuccessful) { depthDevice.init(); // reinit with default baud rate currentBaudRate = depthDevice.TF_MINI_DEFAULT_BAUD_RATE; break; } } #endif /* DO_LIDAR_FACTORY_RESET */ if (currentBaudRate != NEW_LIDAR_BAUD_RATE) { for (int t = 0; t < 5; t += 1) { #if LOG_ENABLED LOG_COUT(info) << F("Change baud rate to ") << NEW_LIDAR_BAUD_RATE << LOG_ENDLINE; #endif bool ok = depthDevice.changeBaudRate(NEW_LIDAR_BAUD_RATE); if (ok) { break; } #if LOG_ENABLED LOG_COUT(warn) << F("Failed to change baud rate attempt=") << t << LOG_ENDLINE; #endif delay(1000); // delay 1 second } } #if DO_LIDAR_CONFIG for (int t = 0; t < 5; t += 1) { #if LOG_ENABLED LOG_COUT(info) << F("Change frame rate") << LOG_ENDLINE; #endif bool ok = depthDevice.changeFrameRate(0); // 0 = request on demand if (ok) { #if SAVE_LIDAR_CONFIG #if LOG_ENABLED LOG_COUT(info) << F("saving settings") << LOG_ENDLINE; #endif depthDevice.saveSettings(); #endif /* SAVE_LIDAR_CONFIG */ break; } #if LOG_ENABLED LOG_COUT(warn) << F("Failed to change frame rate attempt=") << t << LOG_ENDLINE; #endif delay(1000); // delay 1 second } #endif /* DO_LIDAR_CONFIG */ #endif /* USE_DEPTH_SENSOR */ ledController.init(); #if USE_OFFSET_SENSORS > 0 offsetDevice1.init(); #if USE_OFFSET_SENSORS > 1 offsetDevice2.init(); #endif /* USE_OFFSET_SENSORS > 1 */ #endif /* USE_OFFSET_SENSORS > 0 */ #if ENABLE_NTP configTime(USE_LOCAL_TZ, "pool.ntp.org"); WiFi.mode(WIFI_STA); WiFi.setHostname(USE_DEVICE_HOSTNAME); WiFi.begin(USE_WIFI_SSID, USE_WIFI_PASSWORD); #if LOG_ENABLED LOG_COUT(info) << F("MACaddress=") << WiFi.macAddress() << LOG_ENDLINE; #endif #endif /* ENABLE_NTP */ } void updateDepth(time_t now, uint32_t newDepth) { int32_t delta = lastDepth - newDepth; int32_t abs_delta = (delta >= 0) ? delta : -delta; if (abs_delta < MIN_INTERESTING_DEPTH_CHANGE_CM) { // no useful change if ((now - lastUpdateDepthTime) > IDLE_SECS) { #if LOG_ENABLED LOG_COUT(info) << F("no depth change in secs=") << now - lastUpdateDepthTime << F(" lastUpdate=") << lastUpdateDepthTime << LOG_ENDLINE; #endif displayMode &= ~(DISPLAY_INITIAL | DISPLAY_DEPTH_MOVEMENT_MASK | DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED); // clear movement flags #if LOG_ENABLED LOG_COUT(info) << F("displayMode now=") << displayMode << LOG_ENDLINE; #endif // displayMode |= DISPLAY_CURRENT_TIME; } return; // no interesting change } #if LOG_ENABLED LOG_COUT(info) << F("newDepth=") << newDepth << LOG_ENDLINE; #endif lastDepth = newDepth; lastUpdateDepthTime = now; nextIdleTime = now + SECONDS_UNTIL_DISPLAY_TIME; if ((displayMode & (DISPLAY_INITIAL | DISPLAY_DEPTH_MOVEMENT_MASK)) == 0) { // no movement previously detected displayMode |= DISPLAY_INITIAL; #if LOG_ENABLED LOG_COUT(info) << F("Set INITIAL depth=") << newDepth << F(" displayMode=") << displayMode << F(" text=") << ledController.depthMessage << LOG_ENDLINE; #endif } displayMode &= ~(MOVING_FORWARD | MOVING_BACKWARD); displayMode |= (delta >= 0) ? (DISPLAY_DEPTH | MOVING_FORWARD) : (DISPLAY_DEPTH | MOVING_BACKWARD); int32_t depthRemaining = newDepth - TARGET_DESIRED_DEPTH_CM; const char *direction = (depthRemaining >= 0) ? "F" : "B"; snprintf(ledController.depthMessage, sizeof(ledController.depthMessage), #if USE_METRIC_DISPLAY "%d cm %s", depthRemaining, #else "%.0f %s", depthRemaining * 0.3937, #endif direction); if ((displayMode & DISPLAY_CURRENT_TIME) != 0) { // showing time displayMode &= ~DISPLAY_CURRENT_TIME; // stop display of time } blockedSensorMask &= ~DEPTH_SENSOR_MASK; // clear blocked depth sensor flag if ((blockedSensorMask == 0) && (displayMode & DISPLAY_BLOCKED_SENSOR)) { displayMode &= ~DISPLAY_BLOCKED_SENSOR; // clear flag } if (newDepth < MIN_DEPTH_CM) { displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED | DISPLAY_CURRENT_TIME); displayMode |= DISPLAY_BLOCKED_SENSOR; blockedSensorMask |= DEPTH_SENSOR_MASK; ledController.depthMessage[0] = '\0'; strcpy(ledController.offsetMessage, "L1 blocked"); return; } else if (newDepth <= (MIN_DESIRED_DEPTH_CM + DESIRED_DEPTH_TOLERANCE_CM)) { displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED | DISPLAY_CURRENT_TIME); displayMode |= DISPLAY_TOO_FAR; } else if (newDepth <= (TARGET_DESIRED_DEPTH_CM + DESIRED_DEPTH_TOLERANCE_CM)) { displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED | DISPLAY_CURRENT_TIME); displayMode |= DISPLAY_STOP_HERE; } else if (newDepth <= (MAX_DESIRED_DEPTH_CM)) { // valid data displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED | DISPLAY_CURRENT_TIME); displayMode |= DISPLAY_CAUTION; } else if (newDepth <= MAX_DEPTH_CM) { // valid depth displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED | DISPLAY_CURRENT_TIME); } else { // too far away #if LOG_ENABLED LOG_COUT(info) << F("depth too far away") << LOG_ENDLINE; #endif displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED | DISPLAY_DEPTH | DISPLAY_INITIAL | DISPLAY_DEPTH_MOVEMENT_MASK); displayMode |= DISPLAY_CURRENT_TIME; // set display time flag } #if LOG_ENABLED LOG_COUT(info) << F("delta=") << delta << F(" depth=") << newDepth << F(" displayMode=") << displayMode << LOG_ENDLINE; #endif } void displaySensorData() { char bfr[32]; snprintf(bfr, sizeof(bfr), #if USE_DEPTH_SENSOR "%d " #endif #if USE_OFFSET_SENSORS > 0 "%d " #endif #if USE_OFFSET_SENSORS > 1 "%d" #endif #if USE_DEPTH_SENSOR , lastDepth #endif #if USE_OFFSET_SENSORS > 0 , lastOffset[0] #endif #if USE_OFFSET_SENSORS > 1 , lastOffset[1] #endif ); ledController.setMessage(bfr); ledController.refreshDisplay(0); FastLED.show(); } void updateOffset(time_t now, uint32_t newOffset, uint8_t sensorId) { int32_t delta = lastOffset[sensorId] - newOffset; int32_t abs_delta = (delta >= 0) ? delta : -delta; if (abs_delta < MIN_INTERESTING_OFFSET_CHANGE_CM) { // no change if ((now - lastUpdateOffsetTime) > IDLE_SECS) { displayMode &= ~(MOVING_LEFT | MOVING_RIGHT); // clear movement flags } return; } #if LOG_ENABLED LOG_COUT(info) << F("set newOffset=") << newOffset << LOG_ENDLINE; #endif lastOffset[sensorId] = newOffset; lastUpdateOffsetTime = now; blockedSensorMask &= ~(OFFSET1_SENSOR_MASK << sensorId); // assume cleared if ((blockedSensorMask == 0) && (displayMode & DISPLAY_BLOCKED_SENSOR)) { displayMode &= ~DISPLAY_BLOCKED_SENSOR; // clear flag } int32_t offsetRemaining; int32_t dirToMove; // -1=left, 1=right bool belowMinOffset, aboveMaxOffset; switch (sensorId) { case 0: belowMinOffset = (newOffset <= MIN_OFFSET1_CM); aboveMaxOffset = (newOffset >= MAX_OFFSET_CM); dirToMove = (OFFSET1_SENSOR_ON_LEFT == 1) ? 1 : -1; // constant expression offsetRemaining = TARGET_DESIRED_OFFSET1_CM - newOffset; break; case 1: belowMinOffset = (newOffset <= MIN_OFFSET2_CM); aboveMaxOffset = (newOffset >= MAX_OFFSET_CM); dirToMove = (OFFSET2_SENSOR_ON_LEFT == 1) ? 1 : -1; // constant expression offsetRemaining = TARGET_DESIRED_OFFSET2_CM - newOffset; break; } if (aboveMaxOffset == true) { // nothing present displayMode &= ~(DISPLAY_OFFSET | MOVING_LEFT | MOVING_RIGHT); if ((displayMode & DISPLAY_BLOCKED_SENSOR) == 0) { // no sensor is blocked #if LOG_ENABLED LOG_COUT(info) << F("offset sensor no reading and no blocked sensors") << LOG_ENDLINE; #endif } return; } if (belowMinOffset == true) { displayMode &= ~(DISPLAY_CURRENT_TIME); displayMode |= DISPLAY_BLOCKED_SENSOR; blockedSensorMask |= (OFFSET1_SENSOR_MASK << sensorId); // flag sensor snprintf(ledController.offsetMessage, sizeof(ledController.offsetMessage), ("U%d blocked"), sensorId + 1); #if LOG_ENABLED LOG_COUT(info) << F("offset sensor blocked sensor=") << sensorId << F(" offset=") << newOffset << LOG_ENDLINE; #endif return; } displayMode &= ~(DISPLAY_CURRENT_TIME | MOVING_LEFT | MOVING_RIGHT); int32_t leftOrRightOffset = dirToMove * offsetRemaining; int32_t abs_offset = (leftOrRightOffset >= 0) ? leftOrRightOffset : -leftOrRightOffset; // for display purposes, we indicate what direction is needed to return to the center // rather than the last detected movement direction const char *direction = (leftOrRightOffset >= 0) ? ("R") : ("L"); displayMode |= (leftOrRightOffset >= 0) ? (DISPLAY_OFFSET | MOVING_LEFT) : (DISPLAY_OFFSET | MOVING_RIGHT); #if LOG_ENABLED LOG_COUT(info) << F("newOffset=") << newOffset << F(" offsetRemaing=") << offsetRemaining << F(" leftOrRightOffset=") << leftOrRightOffset << F(" dirToMove=") << dirToMove << F(" dir=") << direction << F(" display=") << displayMode << LOG_ENDLINE; #endif snprintf(ledController.offsetMessage, sizeof(ledController.offsetMessage), #if USE_METRIC_DISPLAY ("%d cm %s"), abs_offset, #else ("%.0f %s"), abs_offset * 0.3937, #endif direction); } void updateTime(time_t now) { #if LOG_ENABLED > 1 LOG_COUT(info) << F("updateTime now=") << now << LOG_ENDLINE; #endif time_t delta = now - lastTime; if (delta > 0) { lastTime = now; ledController.timeChanged = true; if (now >= 1704085200) { // time set as later than January 1, 2024 struct tm *curTime = localtime(&now); snprintf(ledController.currentTimeText, sizeof(ledController.currentTimeText), ("%2.2d :%2.2d :%2.2d"), curTime->tm_hour, curTime->tm_min, curTime->tm_sec); } else { strcpy(ledController.currentTimeText, "NO TIME"); } #if LOG_ENABLED } else if (delta < 0) { LOG_COUT(error) << F("updateTime delta=") << delta << LOG_ENDLINE; #endif } } bool updateDisplay(time_t now, uint8_t animationPhase) { #if LOG_ENABLED LOG_COUT(info) << F("updateDisplay phase=") << animationPhase << #if USE_DEPTH_SENSOR F(" lastDepth=") << lastDepth << #endif #if USE_OFFSET_SENSORS > 0 F(" lastOffset1=") << lastOffset[0] << #if USE_OFFSET_SENSORS > 1 F(" lastOffset2=") << lastOffset[1] << #endif #endif F(" toNextIdle=") << nextIdleTime - now << F(" now=") << now << F(" nextIdle=") << nextIdleTime << F(" displayMode=") << displayMode << F(" activeContentMode=") << ledController.activeContentMode << LOG_ENDLINE; #endif /* LOG_ENABLED */ if ((nextIdleTime != 0) && (now >= nextIdleTime)) { // ledController.activeContentMode = ledController.DISPLAY_TIME; #if LOG_ENABLED LOG_COUT(info) << F("IDLE TIME REACHED") << LOG_ENDLINE; #endif displayMode &= ~(DISPLAY_INITIAL | MOVEMENT_MASK | DISPLAY_ALERT_MASK_WITHOUT_BLOCKED_OR_FAILED); displayMode |= DISPLAY_CURRENT_TIME; nextIdleTime = 0; } #if USE_OFFSET_SENSORS > 0 // clear offset marker #if USE_OFFSET_SENSORS > 1 // ledController.setOffsetLine(-1, 0, LED_BLACK, 1); #endif ledController.setOffsetLine(-1, 0, LED_BLACK, 0); #endif uint16_t alert = displayMode & DISPLAY_ALERT_MASK; switch (alert) { case DISPLAY_TOO_FAR: ledController.setMessage("BACK UP!", 3, LED_RED, LED_BLACK); // timeUntilDisplayChange = 10; break; case DISPLAY_BLOCKED_SENSOR: case DISPLAY_FAILED_SENSOR: ledController.setMessage(ledController.offsetMessage, 0, LED_RED); // timeUntilDisplayChange = 10; break; case DISPLAY_STOP_HERE: ledController.setMessage("STOP HERE", 0, LED_GREEN, LED_BLACK); // timeUntilDisplayChange = 10; break; case DISPLAY_CAUTION: { char text[128]; text[0] = '\0'; if (displayMode & DISPLAY_DEPTH) { strcat(text, ledController.depthMessage); } if (displayMode & DISPLAY_OFFSET) { if (text[0] != '\0') { strcat(text, " "); } #if LOG_ENABLED if (ledController.offsetMessage[0] == '\0') { LOG_COUT(error) << F("OFFSET set but no text") << LOG_ENDLINE; } #endif strcat(text, ledController.offsetMessage); } uint8_t l = static_cast<uint8_t>(strlen(text)); uint8_t x = (l >= 10) ? 0 : 5 - (l / 2); #if LOG_ENABLED LOG_COUT(info) << F("text=") << text << F(" l=") << l << F(" x=") << x << LOG_ENDLINE; #endif ledController.setMessage(text, x * 5, LED_ORANGE); } break; default: break; } // end switch alert #if USE_OFFSET_SENSORS > 0 { int32_t relativeOffset, scaledOffset; #if USE_OFFSET_SENSORS > 1 // handle sensor 2 first if ((lastOffset[1] > MIN_OFFSET2_CM) && (lastOffset[1] < MAX_OFFSET_CM)) { // valid signal if (lastOffset[1] < MIN_DESIRED_OFFSET2_CM) { scaledOffset = 0; relativeOffset = TARGET_DESIRED_OFFSET2_CM - MIN_DESIRED_OFFSET2_CM; } else if (lastOffset[1] > MAX_DESIRED_OFFSET2_CM) { scaledOffset = COLS - 1; relativeOffset = TARGET_DESIRED_OFFSET2_CM - MAX_DESIRED_OFFSET2_CM; } else { // following constants are computed at compile-time const int32_t range1 = MAX_DESIRED_OFFSET2_CM - TARGET_DESIRED_OFFSET2_CM; const int32_t range2 = TARGET_DESIRED_OFFSET2_CM - MIN_DESIRED_OFFSET2_CM; const int32_t rangeScale = (range1 >= range2) ? range1 : range2; relativeOffset = TARGET_DESIRED_OFFSET2_CM - lastOffset[1]; scaledOffset = (relativeOffset * COLS) / (rangeScale * 2); #if OFFSET2_SENSOR_ON_LEFT == 0 scaledOffset = (COLS - 1) - scaledOffset; // increasing values mean more to the left relativeOffset *= -1; // invert sign #endif scaledOffset = (COLS / 2) - scaledOffset; if (scaledOffset < 0) scaledOffset = 0; else if (scaledOffset >= COLS) scaledOffset = COLS - 1; } uint16_t color = LED_BLUE_HIGH; if ((scaledOffset <= (COLS / 4)) || (scaledOffset >= (COLS - (COLS / 4)))) { color = LED_RED; } ledController.setOffsetLine(scaledOffset, (scaledOffset <= (COLS / 2) ? -1 : 1), color, 1); } // valid signal #endif /* end USE_OFFSET_SENSORS > 1 */ // handle sensor 1 if ((lastOffset[0] >= MIN_OFFSET1_CM) && (lastOffset[0] < MAX_OFFSET_CM)) { // valid signal if (lastOffset[0] < MIN_DESIRED_OFFSET1_CM) { scaledOffset = 0; relativeOffset = TARGET_DESIRED_OFFSET1_CM - MIN_DESIRED_OFFSET1_CM; } else if (lastOffset[0] > MAX_DESIRED_OFFSET1_CM) { scaledOffset = COLS - 1; relativeOffset = TARGET_DESIRED_OFFSET1_CM - MAX_DESIRED_OFFSET1_CM; } else { // following constants are computed at compile-time const int32_t range1 = MAX_DESIRED_OFFSET1_CM - TARGET_DESIRED_OFFSET1_CM; const int32_t range2 = TARGET_DESIRED_OFFSET1_CM - MIN_DESIRED_OFFSET1_CM; const int32_t rangeScale = (range1 >= range2) ? range1 : range2; relativeOffset = TARGET_DESIRED_OFFSET1_CM - lastOffset[0]; scaledOffset = (relativeOffset * COLS) / (rangeScale * 2); #if OFFSET1_SENSOR_ON_LEFT == 0 scaledOffset = (COLS - 1) - scaledOffset; // increasing values mean more to the left relativeOffset *= -1; // invert sign #endif scaledOffset = (COLS / 2) - scaledOffset; if (scaledOffset < 0) scaledOffset = 0; else if (scaledOffset >= COLS) scaledOffset = COLS - 1; } uint16_t color = LED_BLUE_HIGH; if ((scaledOffset < (COLS / 4)) || (scaledOffset >= (COLS - (COLS / 4)))) { color = LED_RED; } #if LOG_ENABLED LOG_COUT(info) << F("scaledOffset=") << scaledOffset << F(" offset=") << lastOffset[0] << F(" relative=") << relativeOffset << F(" color=") << color << LOG_ENDLINE; #endif ledController.setOffsetLine(scaledOffset, (scaledOffset <= (COLS / 2) ? -1 : 1), color, 0); } // valid signal } #endif /* USE_OFFSET_SENSORS > 0 */ #if USE_DEPTH_SENSOR { // following constants are computed at compile-time const int32_t range1 = MAX_DESIRED_DEPTH_CM - TARGET_DESIRED_DEPTH_CM; const int32_t range2 = TARGET_DESIRED_DEPTH_CM - MIN_DESIRED_DEPTH_CM; const int32_t rangeScale = (range1 >= range2) ? range1 : range2; int32_t depthRemaining = lastDepth - TARGET_DESIRED_DEPTH_CM; int32_t scaledDepth = (depthRemaining * COLS) / (rangeScale * 2); int32_t centeredOffset = ((COLS / 2) - ledController.ANIMATION_PHASES) + scaledDepth; // center #if LOG_ENABLED LOG_COUT(info) << F("depthRemaining=") << depthRemaining << F(" scaled=") << scaledDepth << F(" centered=") << centeredOffset << LOG_ENDLINE; #endif if (centeredOffset < 0) centeredOffset = 0; else if (centeredOffset >= (COLS - (ledController.ANIMATION_PHASES * 2))) centeredOffset = COLS - (ledController.ANIMATION_PHASES * 2); ledController.setTargetBoxOrigin(static_cast<int16_t>(centeredOffset), (lastDepth >= (TARGET_DESIRED_DEPTH_CM - DESIRED_DEPTH_TOLERANCE_CM)) ? LED_GREEN_HIGH : LED_RED_HIGH); } #endif /* USE_DEPTH_SENSOR */ if (now < timeUntilDisplayChange) { #if LOG_ENABLED LOG_COUT(info) << F("seconds until display change=") << timeUntilDisplayChange - now << LOG_ENDLINE; #endif return (true); } uint8_t maxPhases = ledController.MAX_DISPLAY_PHASES; switch (displayMode & (DISPLAY_MASK | MOVING_FORWARD | MOVING_BACKWARD)) { case DISPLAY_ARRIVING: case DISPLAY_ARRIVING | DISPLAY_OFFSET: #if LOG_ENABLED LOG_COUT(info) << F("SET ARRIVE MESSAGE") << LOG_ENDLINE; #endif ledController.setMessage("HELLO", 10, LED_GREEN); timeUntilDisplayChange = now + ANNOUNCE_ARRIVE_DELAY; displayMode &= ~DISPLAY_INITIAL; maxPhases = 1; break; case DISPLAY_DEPARTING: case DISPLAY_DEPARTING | DISPLAY_OFFSET: #if LOG_ENABLED LOG_COUT(info) << F("SET DEPART MESSAGE") << LOG_ENDLINE; #endif ledController.setMessage("BE SAFE", 10, LED_GREEN); timeUntilDisplayChange = now + ANNOUNCE_DEPART_DELAY; displayMode &= ~DISPLAY_INITIAL; maxPhases = 1; break; #if LOG_ENABLED case DISPLAY_INITIAL | DISPLAY_DEPTH | DISPLAY_OFFSET: case DISPLAY_INITIAL | DISPLAY_DEPTH: case DISPLAY_DEPTH | DISPLAY_OFFSET: case DISPLAY_DEPTH: LOG_COUT(warn) << F("DEPTH but no movement") << LOG_ENDLINE; break; #endif #if LOG_ENABLED case DISPLAY_DEPTH | MOVING_FORWARD: case DISPLAY_DEPTH | MOVING_BACKWARD: LOG_COUT(warn) << F("DEPTH AND DIRECTION BUT NO OFFSET") << LOG_ENDLINE; break; #endif #if LOG_ENABLED case DISPLAY_DEPTH | DISPLAY_OFFSET | MOVING_FORWARD: case DISPLAY_DEPTH | DISPLAY_OFFSET | MOVING_BACKWARD: LOG_COUT(info) << F("normal depth and offset and movement") << LOG_ENDLINE; break; #endif case DISPLAY_INITIAL | DISPLAY_OFFSET: // no valid depth data #if LOG_ENABLED LOG_COUT(info) << F("Initial offset only") << LOG_ENDLINE; #endif displayMode &= ~DISPLAY_INITIAL; // clear initial flag /* FALLTHROUGH */ case DISPLAY_OFFSET: #if LOG_ENABLED LOG_COUT(warn) << F("OFFSET but NO DEPTH") << LOG_ENDLINE; #endif /* FALLTHROUGH */ case DISPLAY_CURRENT_TIME | DISPLAY_OFFSET: case DISPLAY_CURRENT_TIME | DISPLAY_DEPTH: case DISPLAY_CURRENT_TIME | DISPLAY_DEPTH | DISPLAY_OFFSET: case DISPLAY_CURRENT_TIME: ledController.activeContentMode = ledController.DISPLAY_TIME; ledController.refreshDisplay(0); FastLED.show(); FastLED.delay(10); #if LOG_ENABLED > 1 LOG_COUT(info) << F("did show for time") << LOG_ENDLINE; #endif return (true); case 0: #if LOG_ENABLED LOG_COUT(warn) << F("no display mode set") << LOG_ENDLINE; #endif break; default: #if LOG_ENABLED LOG_COUT(warn) << F("UNSUPPORTED displayMode=") << displayMode << F(" mask=") << (displayMode & (DISPLAY_MASK | MOVING_FORWARD | MOVING_BACKWARD)) << LOG_ENDLINE; // delay(5000); #endif break; } // for (displayPhase = 0; displayPhase < maxPhases; displayPhase += 1) { ledController.refreshDisplay(animationPhase); FastLED.show(); FastLED.delay(10); #if LOG_ENABLED > 1 LOG_COUT(info) << F("did show for regular animationPhase=") << animationPhase << F(" maxPhases=") << maxPhases << LOG_ENDLINE; #endif if ((animationPhase + 1) >= maxPhases) { return (true); } return (false); } void pollSensors(time_t now) { /* If enabled, read secondary ultrasonic device first, then query LIDAR sensor followed by * reading of primary ultrasonic device. This minimizes time wasted enforcing 60 milliseconds * between readings. */ #if USE_OFFSET_SENSORS > 0 uint32_t offset; unsigned long duration; bool gotResponse; #endif #if USE_OFFSET_SENSORS > 1 unsigned long startMillis = millis(); // do furthest away sensor first gotResponse = offsetDevice2.getDistanceReading(&offset, &duration); if (gotResponse) { // sensor working failedSensorMask &= ~(OFFSET1_SENSOR_MASK << 1); // clear failed depth sensor flag if ((failedSensorMask == 0) && (displayMode & DISPLAY_FAILED_SENSOR)) { displayMode &= ~DISPLAY_FAILED_SENSOR; // clear failed flag } #if LOG_ENABLED > 1 float inches = offset * 0.3937; LOG_COUT(info) << F("SONIC2 distance=") << offset << F(" cm ") << inches << F(" in duration=") << duration << LOG_ENDLINE; #endif updateOffset(now, offset, 1); } else { bool sensorFailed = offsetDevice2.sensorHasFailed(); if (sensorFailed) { #if LOG_ENABLED LOG_COUT(error) << F("SONIC2 failed") << LOG_ENDLINE; #endif displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_FAILED | DISPLAY_CURRENT_TIME); displayMode |= DISPLAY_FAILED_SENSOR; failedSensorMask |= (OFFSET1_SENSOR_MASK << 1); strcpy(ledController.offsetMessage, ("U2 failed")); ledController.depthMessage[0] = '\0'; updateOffset(now, MAX_OFFSET_CM, 1); } } #endif #if USE_DEPTH_SENSOR uint16_t distance; uint16_t strength; bool haveReading = depthDevice.getDistanceReading(&distance, &strength); if (haveReading) { failedSensorMask &= ~DEPTH_SENSOR_MASK; // clear failed depth sensor flag if ((failedSensorMask == 0) && (displayMode & DISPLAY_FAILED_SENSOR)) { displayMode &= ~DISPLAY_FAILED_SENSOR; // clear failed flag } #if LOG_ENABLED > 1 float inches = distance * 0.3937; LOG_COUT(info) << F("LIDAR distance=") << distance << F(" cm ") << inches << F(" in strength=") << strength << LOG_ENDLINE; #endif // distance 65535 = signal strength < 100 // distance 65534 = signal strength saturation // distance 65532 = ambient light saturation if ((strength > 100) && (distance < 65532)) { // valid signal strength updateDepth(now, distance); } } else { // sensor seems to not be working.... if (depthDevice.sensorHasFailed()) { displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_FAILED | DISPLAY_CURRENT_TIME | DISPLAY_DEPTH | DISPLAY_INITIAL | DISPLAY_DEPTH_MOVEMENT_MASK); displayMode |= DISPLAY_FAILED_SENSOR; failedSensorMask |= DEPTH_SENSOR_MASK; ledController.depthMessage[0] = '\0'; strcpy(ledController.offsetMessage, "L1 failed"); updateDepth(now, 0); } } #endif /* USE_DEPTH_SENSOR */ #if USE_OFFSET_SENSORS > 1 unsigned long nowMillis = millis(); unsigned long sampleTime = nowMillis - startMillis; if (sampleTime < 60) { // enforce 60 milliseconds between samples FastLED.delay(60 - sampleTime); } #endif /* USE_OFFSET_SENSORS > 1 */ #if USE_OFFSET_SENSORS > 0 gotResponse = offsetDevice1.getDistanceReading(&offset, &duration); if (gotResponse) { failedSensorMask &= ~(OFFSET1_SENSOR_MASK << 0); // clear failed depth sensor flag if ((failedSensorMask == 0) && (displayMode & DISPLAY_FAILED_SENSOR)) { displayMode &= ~DISPLAY_FAILED_SENSOR; // clear failed flag } #if LOG_ENABLED > 1 float inches = offset * 0.3937; LOG_COUT(info) << F("SONIC1 distance=") << offset << F(" cm ") << inches << F(" in duration=") << duration << LOG_ENDLINE; #endif updateOffset(now, offset, 0); } else { bool sensorFailed = offsetDevice1.sensorHasFailed(); if (sensorFailed) { #if LOG_ENABLED LOG_COUT(error) << F("SONIC1 failed") << LOG_ENDLINE; #endif displayMode &= ~(DISPLAY_ALERT_MASK_WITHOUT_FAILED | DISPLAY_CURRENT_TIME); displayMode |= DISPLAY_FAILED_SENSOR; failedSensorMask |= (OFFSET1_SENSOR_MASK << 0); strcpy(ledController.offsetMessage, ("U1 failed")); ledController.depthMessage[0] = '\0'; updateOffset(now, MAX_OFFSET_CM, 0); } } #endif /* USE_OFFSET_SENSORS > 0 */ } }; // end class ParkingMonitor<> static ParkingMonitor<LED_CONTROL_PIN, NUM_LED_COLS, NUM_LED_ROWS> monitor; void setup() { delay(1000); // stabilize after power-on #if LOG_ENABLED // Setup hardware serial port Serial.begin(CONSOLE_BAUD_RATE); delay(500); // stabilize after power-on while (!Serial) ; // wait for serial port to connect. Needed for native USB port only LOG_COUT(info) << F("Initializing...") << LOG_ENDLINE; #endif pinMode(LED_BUILTIN, OUTPUT); monitor.init(); #if LOG_ENABLED LOG_COUT(info) << F("end setup") << LOG_ENDLINE; #endif } #if ENABLE_CONSOLE_COMMANDS > 0 static uint8_t consoleReadOffset; static char consoleCmdBfr[64]; static bool pauseFlag = false; static bool processConsoleCommand(const time_t now, char *cmd) { uint16_t val; char *nextCmd; char *cmdStart = cmd; while (*cmdStart != '\0') { nextCmd = cmdStart; while ((*nextCmd != '\0') && (*nextCmd != ',')) nextCmd += 1; if (*nextCmd == ',') { *nextCmd = '\0'; nextCmd += 1; } #if LOG_ENABLED LOG_COUT(debug) << F(" cmd=") << cmdStart << LOG_ENDLINE; #endif if ((cmdStart[0] == 'd') && (cmdStart[1] == ':')) { forceDepth = atoi(cmdStart + 2); } else if ((cmdStart[0] == 'o') && (cmdStart[1] == ':')) { forceOffset = atoi(cmdStart + 2); } else if (strcmp(cmdStart, "stop") == 0) { pauseFlag = true; } else if (strcmp(cmdStart, "go") == 0) { pauseFlag = false; } cmdStart = nextCmd; } return (pauseFlag); } static bool readAndProcessConsoleCommands(const time_t now) { static uint8_t consoleReadOffset; while (Serial.available()) { bool sawEOL = false; do { uint8_t c = Serial.read(); if ((c == '\r') || (c == '\n')) { sawEOL = true; break; } consoleCmdBfr[consoleReadOffset++] = c; } while (Serial.available()); // Serial.stopListening(); if (sawEOL == false) break; consoleCmdBfr[consoleReadOffset] = '\0'; #if LOG_ENABLED LOG_COUT(debug) << F("len=") << consoleReadOffset << F(" bfr=") << consoleCmdBfr << LOG_ENDLINE; #endif consoleReadOffset = 0; // reset bool result = processConsoleCommand(now, consoleCmdBfr); } return (pauseFlag); } #endif void loop() { static uint64_t lastDisplayCall; static uint8_t loopCount; static uint8_t heartbeatLEDstate; // blink mode for builtin LED static uint8_t animationPhase = monitor.MAX_DISPLAY_PHASES - 1; if (++loopCount == 10) { // reset loopCount = 0; heartbeatLEDstate = 1 - heartbeatLEDstate; // toggle high vs. low digitalWrite(LED_BUILTIN, heartbeatLEDstate); } #if LOG_ENABLED > 2 LOG_COUT(info) << F("begin loop") << LOG_ENDLINE; #endif time_t now = time(nullptr); monitor.updateTime(now); #if ENABLE_CONSOLE_COMMANDS bool pauseNow = readAndProcessConsoleCommands(now); if (pauseNow == true) { FastLED.delay(1000); // delay 1 second return; } #endif monitor.pollSensors(now); uint64_t currentMillis = millis(); int64_t msSinceLastDisplay = currentMillis - lastDisplayCall; if (msSinceLastDisplay < ANIMATION_INTERVAL) { FastLED.delay(ANIMATION_INTERVAL - msSinceLastDisplay); currentMillis = millis(); } lastDisplayCall = currentMillis; if (++animationPhase >= monitor.MAX_DISPLAY_PHASES) { animationPhase = 0; } int debugState = analogRead(DEBUG_PIN); #if LOG_ENABLED > 1 LOG_COUT(info) << F("debug pin level=") << debugState << LOG_ENDLINE; #endif if (debugState > 1000) { // pin was attached to 3.3v monitor.displaySensorData(); } bool didLastPhase = monitor.updateDisplay(now, animationPhase); if (didLastPhase) { // reset phase counter animationPhase = monitor.MAX_DISPLAY_PHASES - 1; } // FastLED.delay(100); // slow for debugging #if LOG_ENABLED > 2 LOG_COUT(info) << F("end loop") << LOG_ENDLINE; #endif } /* vim: set filetype=cpp : */ /* vim: set expandtab shiftwidth=4 tabstop=4 : */
The file ParkingLocalConfig.h needs to be edited to reflect the local conditions. The illustration below attempts to illuminate the effect of some of the parameters.