Hello everyone.
This will be my single unified thread to showcase all and any D1 R1 board projects using various display setup but with Adafruit Libraries. Only wet test passed firmware and projects will be uploaded here.
For the first one, I present:
1.44 TFT Hyperclock Firmware Raycaster
A LOLIN/Wemos/NodeMCU D1 R1 ESP8266 1.44 SPI TFT 128*128 ST7735 (GREENTAB) Firmware which uses dedicated text and graphics display buffer to procedurally render a ray casted 2.5D visual as the clock animation loop.
Tested to run on D1 R1 for days uninterrupted without any issues.
If the breadboard only contains the display and nothing else, the limiting resistor and the decoupling capacitor of the circuit/breadboard can be removed or not used. Ceramic capacitors are always recommended, use electrolytics only on unavailability of ceramics.
Alternate schematic is included in the commented header of the main sketch.
PS: The board present in the above Fritzing diagram is R2. But I have kept the physical positions of the Pin relevant to R1, which is the board on which the firmware has been tested. Kindly check the GPIO relevance if you have R2 version of the board.
The TFT shown here (in the fritzing export) is 128x160, the one I have used is 128*128 but by the same manufacturer. (The file itself has been provided by the manufacturer). So the pin definitions match perfectly (personally double checked). You can safely follow them.
Device info link:
1.44 128*128 SPI TFT ST7735
If your board is R1, follow the physical positions along with the red helper labels to be sure, and you are golden.
If your board is R2, refer to your datasheet for appropriate MOSI and SCK female slots.
Here are the R1 and R2 side by side for verification and reference. I will go ahead in the rest of the article assuming my hardware, which is R1.
Image source thread:
Image by Costas from blynk community.
Main sketch:
/**
* Core1D Automation Labs Present:
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* โ โ
* โโโ โโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโ โโโโโโโโโโ โโโ
* โโโ โโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโ
* โโโโโโโโ โโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโ โโโ โโโ โโโโโโ โโโโโโโ
* โโโโโโโโ โโโโโ โโโโโโโ โโโโโโ โโโโโโโโโโโ โโโ โโโ โโโโโโ โโโโโโโ
* โโโ โโโ โโโ โโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโ
* โโโ โโโ โโโ โโโ โโโโโโโโโโโ โโโ โโโโโโโโโโโโโโโ โโโโโโโ โโโโโโโโโโ โโโโ
* โ ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, โ
* โ โโโโโโโ โโโโโโโโโโโโโโโ โโโโ\ /โโโโโโโโโโโโโโ โ
* โ โโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ\ /โโโโโโโโโโโโโโโ โ
* โ โโโโโโโ โโโโโโโโโโโ โโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
* โ โโโโโโโ โโโโโโโโโโโ โโโ โโโโโโโโโโโโโโโโ/ \โโโโโโโโโโโโโโโ โ
* โ โโโโโโโโ โโโ โโโโโโโโโโโโโโโโ โโโโ/โโโโโโโโโโโ \โโโโโโโโโโโโโโ โ
* โ โโโโโโโโ โโโ โโโโโโโโโโโโโโโ โโโโ โโโโโโโโโโโโโโ โ
* โ """"""""""""""""""""""""""""""""""""" โ
* โ โโโโโโโ โโโโโโ โโโ โโโ โโโโโโโ โโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* โ โโโโโโโโโโโโโโโโ โโโโโโโ โโโ โโโโโโโโโโโโโโโโ โโโ โโโโโโ โโโโโโโโ
* โ โโโโโโโโโโโโโโโโ โโโโโ โโโ โโโโโโโโโโโโโโโโ โโโ โโโโโโ โโโโโโโโ
* โ โโโ โโโโโโ โโโ โโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโ โโโโโโโโโโโโ โโโโ
* โ โโโ โโโโโโ โโโ โโโ โโโโโโโโโโ โโโโโโโโโโโ โโโ โโโโโโโโโโโโ โโโ
* โ ESP8266 ST77xx Firmware Version V: 2.5.2 โ
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*
* ESP8266 2.5D Raycasting Real-Time Clock with Weather Integration
* Advanced Pseudo-3D Graphics Engine with Optimized Memory Management
*
* Version: 2.5 Polished Edition Revision 2
* Author: Sir Ronnie from Core1D Automation Labs
* License: MIT License
* Target: ESP8266 WeMos D1 R1 with 1.44" ST7735 SPI TFT Display (128x128)
*
* HARDWARE CONNECTIONS:
* =====================
*
* ST7735 1.44" TFT Display (128x128):
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* โ ST7735 Pin โ WeMos D1 Pin โ
* โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
* โ VCC โ 3V3 โ
* โ GND โ GND โ
* โ CS โ D2 (GPIO4) โ
* โ RESET โ D3 (GPIO0) โ
* โ A0/DC โ D4 (GPIO2) โ
* โ SDA/MOSI โ D7 (GPIO13) โ
* โ SCK/CLK โ D5 (GPIO14) โ
* โ LED โ 3V3 (Optional) โ
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*
* HARDWARE ASCII SCHEMATIC:
* ========================
*
* WeMos D1 R1 (ESP8266) ST7735 1.44" TFT
* โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
* โ โ โ โ
* โ [USB-C] 3V3 โโโโโโโโโโโโคโ VCC โ
* โ โ โ โ
* โ [WIFI ANTENNA] โ โ โ
* โ โ โ โ
* โ D7/13 โโโโโโโโโโโโคโ SDA (MOSI) โ
* โ D5/14 โโโโโโโโโโโโคโ SCK (CLK) โ
* โ D2/4 โโโโโโโโโโโโคโ CS โ
* โ D3/0 โโโโโโโโโโโโคโ RST โ
* โ D4/2 โโโโโโโโโโโโคโ DC (A0) โ
* โ โ โ โ
* โ GND โโโโโโโโโโโโคโ GND โ
* โ โ โ โ
* โ [ESP8266-12E] โ โ [128x128 LCD] โ
* โ [80MHz/160MHz] โ โ [65K Colors] โ
* โ [4MB Flash] โ โ [SPI Interface] โ
* โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
* โ โ
* โโโโโ [ Xtensa LX106 Core ] โโโโ
* [ 2.4GHz WiFi Radio ]
*
* Principles Applied:
* ==================
* โข Real-time 2.5D raycasting engine with wall collision detection
* โข Dynamic color-coded walls based on current hour (12-hour cycle)
* โข Smooth camera movement with physics-based collision response
* โข Real-time weather data integration via OpenWeatherMap API
* โข NTP time synchronization with automatic timezone handling
* โข Optimized memory management with PROGMEM usage
* โข Split-screen rendering: 3D viewport + 2D info display
* โข Custom degree symbol rendering for temperature display
* โข WiFi connection management with visual feedback
* โข Efficient frame rate optimization (~25 FPS target)
*
* PERFORMANCE SPECS:
* ==========================
* โข Render Resolution: 128x68 pixels (2.5D viewport)
* โข Info Display Area: 128x60 pixels (2D text/data)
* โข Map Size: 16x16 grid world
* โข Wall Height: 48 pixels (variable based on distance)
* โข Field of View: 90 degrees
* โข Maximum Draw Distance: 12.0 units
* โข Z-Buffer Depth Testing: 16-bit precision
* โข Memory Usage: <32KB RAM, Flash optimized with PROGMEM
*/
#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <time.h>
#include <ArduinoJson.h>
// Hard SPI pins for WeMos D1 R1
#define TFT_CS D2 // GPIO4
#define TFT_RST D3 // GPIO0
#define TFT_DC D4 // GPIO2
#define TFT_LED -1 // Disable pin if connected to 3v3
// WiFi credentials
static constexpr const char* const SSID = "YourSSID";
static constexpr const char* const PASSWORD = "yourPassword";
// Weather API configuration for performance optimization
static constexpr const char* const WEATHER_API_KEY = "cb840413161e7ee08e831af35dfb9c53";
static constexpr const char* const WEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather?id=";
static constexpr const char* const MUMBAI_CITY_ID = "1275339"; // Mumbai, India city ID
/*
// To change cities, simply use your City ID from OpenWeatherMap
static constexpr const char* const NEW_YORK_CITY_ID = "5128581"; // New York City, USA
static constexpr const char* const LONDON_CITY_ID = "2643743"; // London, UK
static constexpr const char* const TOKYO_CITY_ID = "1850147"; // Tokyo, Japan
*/
// Display setup
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
// 2.5D Raycasting constants for compile-time optimization
static constexpr uint8_t RENDER_WIDTH = 128;
static constexpr uint8_t RENDER_HEIGHT = 68;
static constexpr uint8_t MAP_SIZE = 16;
static constexpr uint8_t FOV_ANGLE = 90;
static constexpr uint8_t WALL_HEIGHT = 48;
static constexpr float MAX_DISTANCE = 12.0f;
// Fixed perspective constants for Camera
static constexpr float CAMERA_HEIGHT = 0.5f;
static constexpr float PROJ_PLANE_DIST = 1.0f;
static constexpr float COLLISION_RADIUS = 0.4f;
static constexpr float MOVEMENT_SPEED = 0.008f;
static constexpr float BOUNCE_DAMPING = 0.8f;
static constexpr float MIN_VELOCITY = 0.002f;
// Degree symbol bitmap (5x5 pixels) - PROGMEM for flash storage
static const uint8_t PROGMEM degree_bitmap[5] = {
0b01100000, // .##.....
0b10010000, // #..#....
0b10010000, // #..#....
0b10010000, // #..#....
0b01100000 // .##.....
};
// Day names array for performance
static constexpr const char* const DAY_NAMES[7] = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
// Wall colors hourly scheme
static constexpr uint16_t WALL_COLORS[12] = {
ST7735_RED, // 0-1 or 12-1
0xFD20, // 1-2 (Orange)
ST7735_YELLOW, // 2-3
ST7735_GREEN, // 3-4
ST7735_CYAN, // 4-5
ST7735_BLUE, // 5-6
ST7735_MAGENTA, // 6-7
0xF81F, // 7-8 (Pink)
0x07E0, // 8-9 (Bright Green)
0x001F, // 9-10 (Bright Blue)
0xFFE0, // 10-11 (Bright Yellow)
0x7BEF // 11-12 (Light Gray)
};
// Game world structure
struct World {
uint8_t map[MAP_SIZE][MAP_SIZE];
float cameraX, cameraY;
float cameraAngle;
float cameraDirX, cameraDirY;
float velocityX, velocityY; // For bounce physics
uint8_t columns[MAP_SIZE * MAP_SIZE];
uint32_t lastMoveTime;
} world;
// Optimized color scheme
struct Colors {
uint16_t wall;
uint16_t background; // Single background color for non-wall areas
uint16_t column;
} colors;
// Weather and time data
struct TimeData {
int hour, minute, second;
int day, month, year;
const char* dayName;
float temperature;
const char* weatherDesc;
int humidity;
float pressure;
float windSpeed;
const char* cityName;
const char* ipAddress;
int wifiStrength;
uint32_t lastWeatherUpdate;
} timeData;
// Z-buffer for depth testing (optimized for walls only)
uint16_t zbuffer[RENDER_WIDTH];
void setup() {
// Initialize display
tft.initR(INITR_144GREENTAB);
/*
If having any issues, try these alternatives one at a time:
tft.initR(INITR_GREENTAB); // Alternative 1
tft.initR(INITR_REDTAB); // Alternative 2
tft.initR(INITR_BLACKTAB); // Alternative 3 (Creates dirty region/line on the screen bottom with the tested screen)
// Check Footer Notes (Technical Documentation)
// for a detailed brief on the ST7735 and it's functioning regarding this firmware sketch.
*/
tft.setRotation(3); // Landscape mode
tft.setAddrWindow(0, 0, 128, 128);
tft.fillScreen(ST7735_BLACK);
// Show startup logo
showStartupScreen();
// Initialize WiFi
connectToWiFi();
// Configure time
configTime(5.5 * 3600, 0, "pool.ntp.org", "time.nist.gov");
// Initialize world
initializeWorld();
}
void loop() {
updateTime();
updateWorld();
renderFrame();
displayInfo();
// Update weather every 10 minutes
if (millis() - timeData.lastWeatherUpdate > 600000UL) {
updateWeather();
}
//delay(40); // ~More delay = Low FPS for stability, power saving (Stress Tested)
delay(16); // ~Less delay = Higher FPS, also more power hungry (Stress Test under progress)
}
void showStartupScreen() {
tft.fillScreen(ST7735_BLACK);
// Text area configuration
const int textAreaStart = RENDER_HEIGHT;
const int textAreaHeight = 128 - RENDER_HEIGHT;
const int textAreaCenter = textAreaStart + (textAreaHeight / 2);
// Display title text in text buffer area
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(1);
tft.setCursor(20, textAreaCenter - 20);
tft.println(F("CORE1D LABS"));
tft.setTextColor(ST7735_YELLOW);
tft.setCursor(15, textAreaCenter - 10);
tft.println(F("HyperClock 2.5D"));
tft.setTextColor(ST7735_WHITE);
tft.setCursor(25, textAreaCenter + 5);
tft.println(F("ESP8266 Tested"));
// 3D tunnel effect in render area
const int effectLayers = 25;
const int maxBoxWidth = RENDER_WIDTH;
const int maxBoxHeight = RENDER_HEIGHT;
// Rainbow gradient tunnel effect
for (int i = 0; i < effectLayers; i++) {
const float hueStep = 360.0f / effectLayers;
const float currentHue = i * hueStep;
// HSV to RGB conversion for vibrant colors
uint16_t color;
if (currentHue < 60) {
const uint8_t r = 31;
const uint8_t g = (currentHue / 60.0f) * 63;
const uint8_t b = 0;
color = (r << 11) | (g << 5) | b;
} else if (currentHue < 120) {
const uint8_t r = ((120 - currentHue) / 60.0f) * 31;
const uint8_t g = 63;
const uint8_t b = 0;
color = (r << 11) | (g << 5) | b;
} else if (currentHue < 180) {
const uint8_t r = 0;
const uint8_t g = 63;
const uint8_t b = ((currentHue - 120) / 60.0f) * 31;
color = (r << 11) | (g << 5) | b;
} else if (currentHue < 240) {
const uint8_t r = 0;
const uint8_t g = ((240 - currentHue) / 60.0f) * 63;
const uint8_t b = 31;
color = (r << 11) | (g << 5) | b;
} else if (currentHue < 300) {
const uint8_t r = ((currentHue - 240) / 60.0f) * 31;
const uint8_t g = 0;
const uint8_t b = 31;
color = (r << 11) | (g << 5) | b;
} else {
const uint8_t r = 31;
const uint8_t g = 0;
const uint8_t b = ((360 - currentHue) / 60.0f) * 31;
color = (r << 11) | (g << 5) | b;
}
// Calculate layer dimensions
const int layerX = i * 2;
const int layerY = i * 1;
const int layerWidth = maxBoxWidth - (i * 4);
const int layerHeight = maxBoxHeight - (i * 2);
// Bounds checking
if (layerX >= 0 && layerY >= 0 &&
layerX + layerWidth < RENDER_WIDTH &&
layerY + layerHeight < RENDER_HEIGHT &&
layerWidth > 0 && layerHeight > 0) {
tft.drawRect(layerX, layerY, layerWidth, layerHeight, color);
// Inner glow effect
if (i > 0 && layerWidth > 4 && layerHeight > 4) {
const uint16_t glowColor = color | 0x2104;
tft.drawRect(layerX + 1, layerY + 1, layerWidth - 2, layerHeight - 2, glowColor);
}
}
}
// Pulsing center core
const int renderCenterX = RENDER_WIDTH / 2;
const int renderCenterY = RENDER_HEIGHT / 2;
const int coreSize = 12;
const int coreX = renderCenterX - (coreSize / 2);
const int coreY = renderCenterY - (coreSize / 2);
if (coreX >= 0 && coreY >= 0 &&
coreX + coreSize < RENDER_WIDTH &&
coreY + coreSize < RENDER_HEIGHT) {
tft.fillRect(coreX + 2, coreY + 2, coreSize - 4, coreSize - 4, ST7735_WHITE);
tft.drawRect(coreX + 1, coreY + 1, coreSize - 2, coreSize - 2, ST7735_YELLOW);
tft.drawRect(coreX, coreY, coreSize, coreSize, ST7735_CYAN);
}
delay(4000);
}
void connectToWiFi() {
tft.fillScreen(ST7735_BLACK);
tft.setTextColor(ST7735_WHITE);
tft.setCursor(10, RENDER_HEIGHT / 2 - 10);
tft.println(F("Connecting WiFi..."));
WiFi.begin(SSID, PASSWORD);
uint8_t attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
tft.print(F("."));
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
tft.setCursor(10, RENDER_HEIGHT / 2 + 10);
tft.setTextColor(ST7735_GREEN);
tft.println(F("Connected!"));
} else {
tft.setCursor(10, RENDER_HEIGHT / 2 + 10);
tft.setTextColor(ST7735_RED);
tft.println(F("Connection failed"));
}
delay(1000);
}
void initializeWorld() {
// Initialize room layout (walls around perimeter)
memset(world.map, 0, sizeof(world.map));
// Create room walls
for (uint8_t x = 0; x < MAP_SIZE; x++) {
world.map[x][0] = 1;
world.map[x][MAP_SIZE-1] = 1;
}
for (uint8_t y = 0; y < MAP_SIZE; y++) {
world.map[0][y] = 1;
world.map[MAP_SIZE-1][y] = 1;
}
// Add interior walls
world.map[8][4] = 1; world.map[8][5] = 1; world.map[8][6] = 1;
world.map[4][8] = 1; world.map[5][8] = 1; world.map[6][8] = 1;
world.map[12][8] = 1; world.map[12][9] = 1; world.map[12][10] = 1;
// Initialize camera in safe starting position
world.cameraX = 8.0f;
world.cameraY = 8.0f;
world.cameraAngle = 0.0f;
world.cameraDirX = 1.0f;
world.cameraDirY = 0.0f;
world.velocityX = 0.0f;
world.velocityY = 0.0f;
// Initialize destructible columns
memset(world.columns, 0, sizeof(world.columns));
respawnColumns();
world.lastMoveTime = millis();
}
void respawnColumns() {
// Column positions array
static constexpr uint8_t columnPositions[][2] = {
{3, 3}, {5, 3}, {7, 3}, {9, 3}, {11, 3},
{3, 7}, {5, 7}, {7, 7}, {9, 7}, {11, 7},
{3, 11}, {5, 11}, {7, 11}, {9, 11}, {11, 11}
};
for (uint8_t i = 0; i < 15; i++) {
const uint8_t x = columnPositions[i][0];
const uint8_t y = columnPositions[i][1];
world.columns[y * MAP_SIZE + x] = 1;
}
}
// General collision detection
inline bool checkCollision(float x, float y) {
const int mapX = (int)x;
const int mapY = (int)y;
// Bounds check with safety margin
if (mapX < 1 || mapX >= MAP_SIZE - 1 || mapY < 1 || mapY >= MAP_SIZE - 1) {
return true;
}
// Wall check
if (world.map[mapX][mapY] != 0) {
return true;
}
// Column check
if (world.columns[mapY * MAP_SIZE + mapX] != 0) {
return true;
}
return false;
}
// Multi point sampling collision detection for camera movement
inline bool checkCameraCollision(float x, float y, float radius = COLLISION_RADIUS) {
// Check center point
if (checkCollision(x, y)) return true;
// Check 8 points around the camera in a circle
for (uint8_t i = 0; i < 8; i++) {
const float angle = i * PI / 4.0f;
const float testX = x + cos(angle) * radius;
const float testY = y + sin(angle) * radius;
if (checkCollision(testX, testY)) return true;
}
return false;
}
void updateTime() {
const time_t now = time(nullptr);
const struct tm* const timeinfo = localtime(&now);
timeData.hour = timeinfo->tm_hour;
timeData.minute = timeinfo->tm_min;
timeData.second = timeinfo->tm_sec;
timeData.day = timeinfo->tm_mday;
timeData.month = timeinfo->tm_mon + 1;
timeData.year = timeinfo->tm_year + 1900;
// Update colors based on hour
const uint8_t hourIndex = timeData.hour % 12;
colors.wall = WALL_COLORS[hourIndex];
// Set background and column colors
colors.background = ST7735_BLACK; // Black background for non-wall areas
colors.column = 0xFD20; // Orange for columns
// Day name
timeData.dayName = DAY_NAMES[timeinfo->tm_wday];
// Get WiFi info
timeData.wifiStrength = WiFi.RSSI();
static String ipStr = WiFi.localIP().toString();
timeData.ipAddress = ipStr.c_str();
}
void updateWorld() {
const uint32_t currentTime = millis();
// Camera movement with enhanced collision detection
// Use "40UL" for sower but more stable movement if this settings is somehow unstable
if (currentTime - world.lastMoveTime > 16UL) {
// Time scaling for movement calculations
const uint16_t timeScaleInt = (currentTime >> 8) & 0xFFFF;
const float timeScale = timeScaleInt * 0.001f;
// Primary orbital motion
const float actualMovementSpeed = MOVEMENT_SPEED + 0.005f;
world.cameraAngle += actualMovementSpeed;
// Angle normalization
if (world.cameraAngle > 6.28318f) world.cameraAngle -= 6.28318f;
// Bounded radius calculation
const float currentRadius = constrain(4.0f + sin(timeScale) * 1.2f, 2.8f, 5.5f);
// Center with controlled drift
const float centerX = constrain(8.0f + sin(timeScale * 0.3f) * 0.6f, 4.5f, 11.5f);
const float centerY = constrain(8.0f + cos(timeScale * 0.3f) * 0.6f, 4.5f, 11.5f);
// Calculate target position with orbital motion
const float primaryOrbit = world.cameraAngle;
float targetX = centerX + currentRadius * cos(primaryOrbit);
float targetY = centerY + currentRadius * sin(primaryOrbit);
// Add secondary motion
targetX += 0.3f * cos(timeScale * 0.8f);
targetY += 0.3f * sin(timeScale * 0.8f);
// Enhanced boundary enforcement (reduced safety margin for more dynamic movement)
const float SAFE_MARGIN = 0.9f;
targetX = constrain(targetX, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
targetY = constrain(targetY, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
// Pre-check if target position is valid before movement
if (checkCameraCollision(targetX, targetY)) {
// Target is blocked, try alternative movement
const float escapeAngle = world.cameraAngle + PI / 2.0f;
targetX = world.cameraX + cos(escapeAngle) * 0.1f;
targetY = world.cameraY + sin(escapeAngle) * 0.1f;
// Ensure escape position is also safe
targetX = constrain(targetX, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
targetY = constrain(targetY, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
}
// Movement vector calculation with smaller steps
const float deltaX = targetX - world.cameraX;
const float deltaY = targetY - world.cameraY;
const float moveLengthSq = deltaX * deltaX + deltaY * deltaY;
float moveX = 0.0f, moveY = 0.0f;
if (moveLengthSq > 0.000001f) {
const float moveLength = sqrt(moveLengthSq);
const float moveSpeed = min(0.08f, moveLength * 0.1f); // Restored original speeds
moveX = (deltaX / moveLength) * moveSpeed;
moveY = (deltaY / moveLength) * moveSpeed;
} else {
// Emergency movement when stuck - keep aggressive
moveX = cos(timeScale * 2.0f) * 0.08f;
moveY = sin(timeScale * 2.0f) * 0.08f;
}
// Collision detection with gradual movement
float newX = world.cameraX + moveX;
float newY = world.cameraY + moveY;
// Try full movement first
if (!checkCameraCollision(newX, newY)) {
world.cameraX = newX;
world.cameraY = newY;
} else {
// Try X movement only
if (!checkCameraCollision(world.cameraX + moveX, world.cameraY)) {
world.cameraX += moveX;
world.cameraAngle += 0.1f; // Slight angle adjustment
}
// Try Y movement only
else if (!checkCameraCollision(world.cameraX, world.cameraY + moveY)) {
world.cameraY += moveY;
world.cameraAngle += 0.1f; // Slight angle adjustment
}
// Try smaller movements
else {
const float smallMoveX = moveX * 0.3f;
const float smallMoveY = moveY * 0.3f;
if (!checkCameraCollision(world.cameraX + smallMoveX, world.cameraY + smallMoveY)) {
world.cameraX += smallMoveX;
world.cameraY += smallMoveY;
} else {
// Last resort - force movement away from nearest wall
float escapeX = 0.0f, escapeY = 0.0f;
uint8_t openDirections = 0;
// Check all 8 directions and find open ones
for (uint8_t dir = 0; dir < 8; dir++) {
const float testAngle = dir * PI / 4.0f;
const float testX = world.cameraX + cos(testAngle) * 0.15f;
const float testY = world.cameraY + sin(testAngle) * 0.15f;
if (!checkCameraCollision(testX, testY)) {
escapeX += cos(testAngle);
escapeY += sin(testAngle);
openDirections++;
}
}
if (openDirections > 0) {
escapeX /= openDirections;
escapeY /= openDirections;
const float escapeLength = sqrt(escapeX * escapeX + escapeY * escapeY);
if (escapeLength > 0.1f) {
world.cameraX += (escapeX / escapeLength) * 0.1f;
world.cameraY += (escapeY / escapeLength) * 0.1f;
}
}
world.cameraAngle += 0.3f; // Larger angle change when stuck
}
}
}
// Final boundary enforcement
const float FINAL_MARGIN = 0.8f;
world.cameraX = constrain(world.cameraX, FINAL_MARGIN, MAP_SIZE - FINAL_MARGIN);
world.cameraY = constrain(world.cameraY, FINAL_MARGIN, MAP_SIZE - FINAL_MARGIN);
// Enhanced stuck detection with better recovery
static uint8_t stuckCounter = 0;
static uint8_t recoveryAttempts = 0;
static float lastCameraX = world.cameraX;
static float lastCameraY = world.cameraY;
static uint32_t lastStuckTime = 0;
const float movementThisFrame = abs(world.cameraX - lastCameraX) + abs(world.cameraY - lastCameraY);
if (movementThisFrame < 0.001f) {
if (++stuckCounter > 20) {
recoveryAttempts++;
if (recoveryAttempts < 3) {
// Try different recovery strategies
const float recoveryAngle = timeScale * 4.0f + recoveryAttempts * PI / 3.0f;
const float recoveryRadius = 1.5f + recoveryAttempts * 0.5f;
float newPosX = 8.0f + cos(recoveryAngle) * recoveryRadius;
float newPosY = 8.0f + sin(recoveryAngle) * recoveryRadius;
// Ensure recovery position is safe
newPosX = constrain(newPosX, 2.0f, MAP_SIZE - 2.0f);
newPosY = constrain(newPosY, 2.0f, MAP_SIZE - 2.0f);
if (!checkCameraCollision(newPosX, newPosY)) {
world.cameraX = newPosX;
world.cameraY = newPosY;
world.cameraAngle += PI / 4.0f; // 45-degree turn
stuckCounter = 0;
recoveryAttempts = 0;
}
} else {
// Nuclear option - teleport to guaranteed safe position
world.cameraX = 8.0f;
world.cameraY = 8.0f;
world.cameraAngle = (float)(millis() % 628) / 100.0f;
stuckCounter = 0;
recoveryAttempts = 0;
lastStuckTime = currentTime;
}
}
} else {
if (stuckCounter > 0) stuckCounter--;
if (currentTime - lastStuckTime > 30000UL) {
recoveryAttempts = 0;
}
}
lastCameraX = world.cameraX;
lastCameraY = world.cameraY;
// Camera orientation with smooth interpolation
const float lookAtX = centerX - world.cameraX;
const float lookAtY = centerY - world.cameraY;
const float lookLength = sqrt(lookAtX * lookAtX + lookAtY * lookAtY);
if (lookLength > 0.1f) {
const float targetDirX = lookAtX / lookLength;
const float targetDirY = lookAtY / lookLength;
// Smooth camera sway
const float sway = sin(timeScale * 1.5f) * 0.03f;
const float swayY = cos(timeScale * 1.1f) * 0.02f;
// Smooth interpolation with adaptive factor
const float interpFactor = min(0.15f, movementThisFrame * 5.0f + 0.08f);
world.cameraDirX = world.cameraDirX * (1.0f - interpFactor) + (targetDirX + sway) * interpFactor;
world.cameraDirY = world.cameraDirY * (1.0f - interpFactor) + (targetDirY + swayY) * interpFactor;
// Normalize camera direction
const float dirLength = sqrt(world.cameraDirX * world.cameraDirX + world.cameraDirY * world.cameraDirY);
if (dirLength > 0.1f) {
world.cameraDirX /= dirLength;
world.cameraDirY /= dirLength;
} else {
world.cameraDirX = cos(world.cameraAngle);
world.cameraDirY = sin(world.cameraAngle);
}
} else {
world.cameraDirX = cos(world.cameraAngle);
world.cameraDirY = sin(world.cameraAngle);
}
// Enhanced NaN safety check with better recovery
if (world.cameraX != world.cameraX || world.cameraY != world.cameraY ||
world.cameraDirX != world.cameraDirX || world.cameraDirY != world.cameraDirY ||
world.cameraX < 0 || world.cameraX >= MAP_SIZE ||
world.cameraY < 0 || world.cameraY >= MAP_SIZE) {
// Reset to safe defaults
world.cameraX = 8.0f;
world.cameraY = 8.0f;
world.cameraAngle = 0.0f;
world.cameraDirX = 1.0f;
world.cameraDirY = 0.0f;
world.velocityX = 0.0f;
world.velocityY = 0.0f;
}
world.lastMoveTime = currentTime;
}
}
void renderFrame() {
// Clear z-buffer
for (uint8_t i = 0; i < RENDER_WIDTH; i++) {
zbuffer[i] = (uint16_t)(MAX_DISTANCE * 1000.0f);
}
// Render walls
renderRaycastedView();
}
void renderRaycastedView() {
// Calculate plane vectors for proper FOV
const float fovRad = FOV_ANGLE * PI / 180.0f;
const float planeMagnitude = tan(fovRad / 2.0f);
// Camera direction normalization
const float dirLength = sqrt(world.cameraDirX * world.cameraDirX + world.cameraDirY * world.cameraDirY);
float normDirX = world.cameraDirX;
float normDirY = world.cameraDirY;
if (dirLength > 0.1f) {
normDirX /= dirLength;
normDirY /= dirLength;
} else {
normDirX = cos(world.cameraAngle);
normDirY = sin(world.cameraAngle);
}
const float planeX = -normDirY * planeMagnitude;
const float planeY = normDirX * planeMagnitude;
for (uint8_t x = 0; x < RENDER_WIDTH; x++) {
// Calculate ray direction with perspective correction
const float cameraX_ray = 2.0f * x / (float)(RENDER_WIDTH - 1) - 1.0f;
const float rayDirX = normDirX + planeX * cameraX_ray;
const float rayDirY = normDirY + planeY * cameraX_ray;
// Ensure ray direction is not zero
const float rayLength = sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
if (rayLength < 0.001f) {
drawVerticalLine(x, RENDER_HEIGHT / 2, RENDER_HEIGHT / 2 + 1, MAX_DISTANCE);
continue;
}
// Perform DDA raycasting
const float distance = castRay(world.cameraX, world.cameraY, rayDirX / rayLength, rayDirY / rayLength);
// Calculate wall height with proper perspective
const int wallHeight = constrain((int)(WALL_HEIGHT / max(distance, 0.1f)), 2, RENDER_HEIGHT - 8);
// Calculate wall start and end positions
const int wallStart = constrain((RENDER_HEIGHT / 2) - wallHeight / 2, 0, RENDER_HEIGHT - 2);
const int wallEnd = constrain((RENDER_HEIGHT / 2) + wallHeight / 2, wallStart + 2, RENDER_HEIGHT);
// Draw vertical line - walls only
drawVerticalLine(x, wallStart, wallEnd, distance);
zbuffer[x] = (uint16_t)(min(distance, MAX_DISTANCE) * 1000.0f);
}
}
float castRay(const float startX, const float startY, const float dirX, const float dirY) {
// DDA algorithm for raycasting
int mapX = (int)startX;
int mapY = (int)startY;
// Calculate delta distances
const float deltaDistX = (dirX == 0) ? 1e30f : abs(1.0f / dirX);
const float deltaDistY = (dirY == 0) ? 1e30f : abs(1.0f / dirY);
float sideDistX, sideDistY;
int stepX, stepY;
uint8_t side; // 0 for NS wall, 1 for EW wall
// Calculate step and initial sideDist
if (dirX < 0) {
stepX = -1;
sideDistX = (startX - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0f - startX) * deltaDistX;
}
if (dirY < 0) {
stepY = -1;
sideDistY = (startY - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0f - startY) * deltaDistY;
}
// Perform DDA
bool hit = false;
while (!hit) {
// Jump to next map square
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
// Check if ray has hit a wall or is out of bounds
if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) {
return MAX_DISTANCE;
}
if (world.map[mapX][mapY] > 0 || world.columns[mapY * MAP_SIZE + mapX] > 0) {
hit = true;
}
}
// Calculate distance
const float distance = (side == 0) ?
(mapX - startX + (1 - stepX) / 2) / dirX :
(mapY - startY + (1 - stepY) / 2) / dirY;
return constrain(distance, 0.1f, MAX_DISTANCE);
}
// Walls-only vertical line drawing - optimized for performance
void drawVerticalLine(const uint8_t x, const int wallStart, const int wallEnd, const float distance) {
// Calculate fog intensity for depth perception
const uint8_t fogLevel = constrain((int)(distance * 4), 0, 15);
// Apply fog to wall color only
const uint16_t wallColor = applyFog(colors.wall, fogLevel);
// Wall bounds checking
const int safeWallStart = constrain(wallStart, 0, RENDER_HEIGHT - 1);
const int safeWallEnd = constrain(wallEnd, safeWallStart + 1, RENDER_HEIGHT);
// Draw background (non-wall areas) - single color for performance
for (int y = 0; y < safeWallStart; y++) {
tft.drawPixel(x, y, colors.background);
}
// Draw wall segment
for (int y = safeWallStart; y < safeWallEnd; y++) {
tft.drawPixel(x, y, wallColor);
}
// Draw background (non-wall areas)
for (int y = safeWallEnd; y < RENDER_HEIGHT; y++) {
tft.drawPixel(x, y, colors.background);
}
}
// Apply fog effect for depth perception
inline uint16_t applyFog(const uint16_t color, const uint8_t fogLevel) {
if (fogLevel >= 16) return ST7735_BLACK;
// Extract RGB components using bit operations
const uint8_t r = (color >> 11) & 0x1F;
const uint8_t g = (color >> 5) & 0x3F;
const uint8_t b = color & 0x1F;
// Apply fog intensity
const uint8_t intensity = 16 - fogLevel;
const uint8_t foggedR = (r * intensity) >> 4;
const uint8_t foggedG = (g * intensity) >> 4;
const uint8_t foggedB = (b * intensity) >> 4;
return (foggedR << 11) | (foggedG << 5) | foggedB;
}
// Custom degree symbol drawing function
void drawDegreeSymbol(const int16_t x, const int16_t y, const uint16_t color) {
for (uint8_t row = 0; row < 5; row++) {
const uint8_t bitmap_row = pgm_read_byte(°ree_bitmap[row]);
for (uint8_t col = 0; col < 4; col++) {
if (bitmap_row & (0x80 >> col)) {
tft.drawPixel(x + col, y + row, color);
}
}
}
}
void displayInfo() {
// Update display once per minute to reduce CPU cycles
static int lastMinute = -1;
if (timeData.minute == lastMinute) return;
lastMinute = timeData.minute;
// Clear info area
const int textAreaHeight = 128 - RENDER_HEIGHT - 2;
tft.fillRect(0, RENDER_HEIGHT, 160, textAreaHeight, ST7735_BLACK);
// Convert to 12-hour format
int displayHour = timeData.hour;
const char* ampm;
if (displayHour == 0) {
displayHour = 12; // Midnight
ampm = "AM";
} else if (displayHour > 12) {
displayHour -= 12;
ampm = "PM";
} else if (displayHour == 12) {
ampm = "PM"; // Noon
} else {
ampm = "AM";
}
// Display time in 12-hour format
const int baseY = RENDER_HEIGHT + 1;
tft.setTextColor(ST7735_WHITE);
tft.setTextSize(2);
tft.setCursor(10, baseY);
tft.printf("%02d:%02d", displayHour, timeData.minute);
// Display AM/PM
tft.setTextSize(1);
tft.setCursor(90, baseY + 7);
tft.print(ampm);
// Display date
tft.setTextSize(1);
tft.setCursor(10, baseY + 20);
tft.printf("%s %02d/%02d/%04d", timeData.dayName,
timeData.day, timeData.month, timeData.year);
// Display weather with custom degree symbol
if (timeData.temperature != 0 && (baseY + 30) < (128 - 10)) {
tft.setCursor(10, baseY + 30);
tft.printf("%.1f", timeData.temperature);
// Draw custom degree symbol
drawDegreeSymbol(tft.getCursorX(), tft.getCursorY() - 1, ST7735_WHITE);
// Continue with C and humidity
tft.setCursor(tft.getCursorX() + 6, tft.getCursorY());
tft.printf("C %dRH", timeData.humidity);
}
// Display WiFi strength and pressure
if ((baseY + 40) < (128 - 8)) {
tft.setTextColor(ST7735_CYAN);
tft.setCursor(10, baseY + 40);
tft.printf("WiFi:%ddBm", timeData.wifiStrength);
// Display pressure if available
if (timeData.pressure > 0 && (baseY + 40) < (128 - 8)) {
tft.setCursor(80, baseY + 40);
tft.printf("%.0fhPa", timeData.pressure);
}
}
}
void updateWeather() {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
WiFiClient client;
// Construct URL using efficient string operations
static char url[200];
strcpy_P(url, WEATHER_BASE_URL);
strcat_P(url, MUMBAI_CITY_ID);
strcat (url, "&appid=");
strcat_P(url, WEATHER_API_KEY);
strcat (url, "&units=metric");
http.begin(client, url);
const int httpResponseCode = http.GET();
if (httpResponseCode == 200) {
const String payload = http.getString();
// Parse JSON for weather data
DynamicJsonDocument doc(2048);
deserializeJson(doc, payload);
timeData.temperature = doc["main"]["temp"];
timeData.humidity = doc["main"]["humidity"];
timeData.pressure = doc["main"]["pressure"];
// Store weather description
static String weatherDescStr = doc["weather"][0]["description"].as<String>();
timeData.weatherDesc = weatherDescStr.c_str();
static String cityNameStr = doc["name"].as<String>();
timeData.cityName = cityNameStr.c_str();
if (doc["wind"]["speed"]) {
timeData.windSpeed = doc["wind"]["speed"];
}
timeData.lastWeatherUpdate = millis();
}
http.end();
}
/**
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
* โ TECHNICAL DOCUMENTATION โ
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*
* CONSTEXPR IN C++11:
* ===============================
*
* The 'constexpr' keyword was introduced in C++11 (ISO/IEC 14882:2011) as part of
* the language's move toward more compile-time computation capabilities. It serves
* multiple purposes in modern C++ development:
*
* 1. COMPILE-TIME CONSTANTS:
* - constexpr variables are evaluated at compile time, not runtime
* - Values are embedded directly into the binary, eliminating memory loads
* - E.g.: constexpr uint8_t RENDER_WIDTH = 128;
*
* 2. COMPILE-TIME FUNCTIONS:
* - constexpr functions can be evaluated during compilation if arguments are constant
* - Enables template metaprogramming and compile-time calculations
* - Reduces runtime overhead significantly in embedded systems
*
* 3. OPTIMIZATION BENEFITS:
* - Eliminates runtime variable initialization
* - Reduces memory footprint in resource-constrained environments
* - Allows aggressive compiler optimizations (loop unrolling, dead code elimination)
* - Perfect for embedded systems where every byte and CPU cycle matters
*
* CONSTEXPR IN EMBEDDED C++:
* ==========================
*
* In microcontroller programming, constexpr provides several critical advantages:
*
* โข MEMORY EFFICIENCY: Values stored in flash (program memory) instead of RAM
* โข PERFORMANCE: No runtime initialization or memory access penalties
* โข PREDICTABILITY: Compile-time evaluation ensures deterministic behavior
* โข CODE CLARITY: Clearly distinguishes between compile-time and runtime data
*
* Traditional const vs constexpr:
* - const int x = 5; // May be stored in RAM, initialized at runtime
* - constexpr int x = 5; // Guaranteed compile-time constant, stored in flash
*
* RAYCASTING PRINCIPLES AND IMPLEMENTATION:
* =========================================
*
* Raycasting is a rendering technique that creates pseudo-3D graphics by casting
* rays from the camera through each column of the screen into a 2D world map.
*
* MATHEMATICAL FOUNDATION:
*
* 1. RAY EQUATION:
* For each screen column x, calculate ray direction:
* rayDirX = cameraDirX + planeX * cameraX_normalized
* rayDirY = cameraDirY + planeY * cameraX_normalized
*
* โ cameraX_normalized = 2.0 * x / screenWidth - 1.0 (range: -1 to +1)
*
* 2. DDA ALGORITHM (Digital Differential Analyzer):
* - Efficiently steps through grid cells along the ray path
* - Calculates deltaDistX = |1/rayDirX| and deltaDistY = |1/rayDirY|
* - Uses sideDist variables to track distance to next grid line
* - Determines step direction (+1 or -1) for X and Y axes
*
* 3. WALL HEIGHT CALCULATION:
* wallHeight = WALL_HEIGHT / distance
* - Implements perspective projection using 1/distance relationship
* - Creates illusion of depth through size variation
*
* 4. PERSPECTIVE CORRECTION:
* distance = (side == 0) ? (mapX - startX + (1-stepX)/2) / rayDirX :
* (mapY - startY + (1-stepY)/2) / rayDirY
* - Prevents fisheye distortion by using perpendicular distance
* - Maintains realistic proportions across field of view
*
* ST7735 Display Tab Configuration Guide
* =====================================
*
* The ST7735 controller comes in different variants, each requiring specific
* initialization settings called "tabs". The tab determines memory mapping,
* color ordering, and display offsets.
*
* TAB TYPES:
* ----------
* INITR_BLACKTAB: - Most common variant
* - Often has 1-2 pixel offset issues on edges
* - RGB color order
* - May show dirty lines on 1.44" displays
*
* INITR_GREENTAB: - Alternative variant
* - Different memory mapping
* - Better for some 1.44" displays
* - May have different color calibration
*
* INITR_REDTAB: - Third variant
* - BGR color order (colors may appear swapped)
* - Different offset configuration
* - Less common for 1.44" displays
*
* INITR_144GREENTAB: - Specific variant for 1.44" ST7735 displays
* - Optimized memory mapping for 128x128 resolution
* - Eliminates edge artifacts and dirty lines
* - Best choice for genuine 1.44" ST7735R displays
*
* WHEN TO USE 144GREENTAB:
* -----------------------
* - Use for 1.44" ST7735 displays (128x128 resolution)
* - When experiencing random pixels or lines at screen edges
* - When BLACKTAB/GREENTAB show offset or boundary issues
* - For displays with "ST7735R" controller specifically
*
* WHY 144GREENTAB TO BE PREFERRED for 1.44 Display:
* ------------------------------------------------
* - Correct memory window addressing for 1.44" variants
* - Eliminates/accounts for errors like off-by-one pixel boundary
* - Proper hardware tailored RGB color mapping (or more like, the manufacturers follow the said standard).
*
* TROUBLESHOOTING:
* ---------------
* If display shows:
* - Wrong colors โ Try REDTAB or different tab
* - Offset image โ Try GREENTAB or 144GREENTAB
* - Edge artifacts โ Use 144GREENTAB for 1.44" displays
* - Completely blank โ Check wiring and tab compatibility
*
* NOTE: Always match the tab to your specific display variant.
* When in doubt, try 144GREENTAB first for 1.44" displays.
*
* ================================================================================================
* ADDITIONAL RESOURCES AND REFERENCES:
* ================================================================================================
* While in the 21st Century almost everything is available at our fingertips,
* here are some specific references and sources that adhere to academic rigour or have proven materials,
* including resources for procedural techniques and raycasting techniques used in this firmware sketch:
*
* TECHNICAL REFERENCES:
* โข Lode Vandevenne's Raycasting Tutorial: Comprehensive guide on implementing raycasting
* for pseudo-3D rendering, covering DDA algorithms and perspective projection.
* https://lodev.org/cgtutor/raycasting.html
* โข Permadi's Ray-Casting Tutorial: Detailed explanation of raycasting mechanics, including
* ray generation, grid traversal, and perspective correction techniques used in the code.
* https://permadi.com/1996/05/ray-casting-tutorial/
* โข ESP8266 Arduino Core Documentation: Official documentation for programming the ESP8266
* with Arduino, detailing memory management and optimization techniques.
* https://arduino-esp8266.readthedocs.io/
* โข ST7735 Display Driver Reference: Technical reference for the ST7735 display driver,
* used for optimized rendering in the raycasting pipeline.
* https://github.com/adafruit/Adafruit-ST7735-Library
* โข NTP Protocol Specification: RFC 5905, detailing Network Time Protocol for time
* synchronization used in the dynamic color system.
* https://tools.ietf.org/html/rfc5905
* โข C++11 constexpr Reference: ISO/IEC 14882:2011 Standard, explaining compile-time
* constants used for performance optimization in the code.
* ISO/IEC 14882:2011 Standard
* โข OpenWeatherMap API Documentation: Reference for integrating real-time environmental
* data, potentially used for dynamic lighting or atmospheric effects.
* https://openweathermap.org/api
*
* MATHEMATICAL REFERENCES:
* โข Computer Graphics: Principles and Practice (Foley, van Dam, Feiner, Hughes):
* Foundational text covering raycasting mathematics, projection techniques, and 3D
* rendering principles.
* โข Real-Time Rendering (Akenine-Mรถller, Haines, Hoffman): In-depth resource on real-time
* rendering techniques, including optimization strategies for raycasting and perspective
* correction.
* โข 3D Math Primer for Graphics and Game Development (Dunn, Parberry): Covers vector
* mathematics and camera systems, essential for the orbital motion and direction
* calculations in the code.
* โข Procedural Content Generation in Games (Shaker, Togelius, Nelson): Explores procedural
* techniques for dynamic camera motion and map generation, as used in the orbital and
* unstuck mechanisms.
*
* ESP8266 DEVELOPMENT RESOURCES:
* โข ESP8266 Technical Reference: Official Espressif documentation detailing hardware
* constraints and optimization strategies for the ESP8266 microcontroller.
* https://www.espressif.com/sites/default/files/documentation/esp8266-technical_reference_en.pdf
* โข Arduino IDE ESP8266 Package: Source for ESP8266 Arduino integration, providing tools
* for efficient memory and stack management.
* https://github.com/esp8266/Arduino
* โข ESP8266 Community Forum: Community-driven resource for troubleshooting and optimizing
* ESP8266-based projects.
* https://www.esp8266.com/
*
* TIME SYNCHRONIZATION RESOURCES:
* โข NTP Pool Project: Resource for reliable time synchronization servers, used for accurate
* time-based effects in the rendering system.
* https://www.ntppool.org/
* โข NIST Time Services: Official time services for precise synchronization, supporting the
* dynamic color palette cycling.
* https://www.nist.gov/pml/time-and-frequency-division/time-services
* โข Internet Time Synchronization: RFC 1305, foundational specification for NTP, relevant
* for time-driven procedural effects.
* https://tools.ietf.org/html/rfc1305
*
* PROCEDURAL TECHNIQUES RESOURCES:
* โข Procedural Generation Algorithms (RogueBasin): Wiki resource detailing procedural
* movement and map generation techniques, applicable to the camera's orbital motion and
* dynamic center calculations.
* http://www.roguebasin.com/index.php?title=Procedural_Generation
* โข Game Programming Patterns (Nystrom): Book covering procedural design patterns,
* including techniques for organic camera movement and collision avoidance systems.
* https://gameprogrammingpatterns.com/
* โข The Art of Procedural Content Generation (GDC Talks): Collection of GDC presentations
* on procedural techniques, offering insights into dynamic camera behaviors and
* environmental interaction.
* https://www.gdcvault.com/
*
* ================================================================================================
*
* Copyright (c) 2025 Core1D Automation Labs
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Designed and Developed by Sir Ronnie
* Core1D Automation Labs
* https://core1d.com
*
* Adafruit Library License details:
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.
MIT license, all text above must be included in any redistribution.
*
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*/
More D1 R1 project links:
LOLIN/WeMos/NodeMCU D1 R1 1.44 TFT Hyperclock Firmware Rev 2
LOLIN/WeMos/NodeMCU D1 R1 1.44
LOLIN/WeMos/NodeMCU D1 R1 LCD Keypad Shield Firmware TFT Hyperclock Software
[More coming soon (Under testing and documentation)]
Cheers
Note: 10ฮผF Circuit capacitor and 220 Ohm resistor to VCC is optional if your power supply is from the board itself.
You can control the brightness of the LED display by using the resistor value as your hardware brightness control. Avenues to experiment with variable resistors, or implement a PWM (Pulse Width Modulation) based Backlight control from the microcontroller (by using PWM capable pins and placing the connection end somewhere between the LED+/LED/BL pin and the resistor from the VCC source{3v3 in this case}. Usage of resistor may depend on the board and their GPIO physical capabilities. Refer to official datasheets or datasheet informed source for verification).
Test Update 08/08/25: Approx. 25% battery consumption on 5000 mAh battery on six hours of run.
Patch Update: Added measures to collision detection and world update to prevent camera getting stuck in procedural edge cases,














