Arnov Sharma
Published © MIT

SerpenTime

Serpentime is a digital internet clock that transforms into a retro Snake game console at the press of a button

BeginnerFull instructions provided1 hour68
SerpenTime

Things used in this project

Hardware components

Espressif esp32 c6 devkit
×1
Adafruit ili9341
×1

Software apps and online services

Fusion
Autodesk Fusion
Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

STEP FILE

Schematics

SCH

Code

code

C/C++
#include <Arduino.h>
#include <WiFi.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <SPI.h>
#include <BLEGamepadClient.h>
#include <time.h>
// ------------------- Hardware -------------------
#define TFT_CS 10
#define TFT_DC 12
#define TFT_RST 11
#define TFT_MOSI 6
#define TFT_SCLK 7
#define BUZZER_PIN 20
#define MODE_BUTTON 15
#define LCD_WIDTH 320
#define LCD_HEIGHT 240
#define CELL_SIZE 8
#define GRID_WIDTH  (LCD_WIDTH / CELL_SIZE)
#define GRID_HEIGHT (LCD_HEIGHT / CELL_SIZE)
// ------------------- WiFi -------------------
const char *ssid = "YOUR SSID";
const char *password = "ADD PASS";
// ------------------- Display / Controller -------------------
Adafruit_ILI9341 gfx = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
XboxController controller;
XboxControlsEvent e;
// ------------------- Snake Game State -------------------
struct SnakeSegment { int x; int y; };
SnakeSegment snake[200];
int snakeLength = 5;
int dx = 1, dy = 0;
int foodX = 0, foodY = 0;
uint16_t foodColor = ILI9341_RED;
int prevTailX = 0, prevTailY = 0;
// ------------------- Mode Control -------------------
enum Mode { MODE_CLOCK, MODE_GAME };
Mode currentMode = MODE_CLOCK;
unsigned long lastButtonPress = 0;
// ------------------- Clock Panel geometry -------------------
const int PANEL_MARGIN = 10;
const int PANEL_W = LCD_WIDTH - 2 * PANEL_MARGIN;
const int PANEL_H_WEEK  = 40;
const int PANEL_H_DATE  = 44;
const int PANEL_H_TIME  = 66;
const int PANEL_Y_WEEK  = 36;
const int PANEL_Y_DATE  = PANEL_Y_WEEK + PANEL_H_WEEK + 8;
const int PANEL_Y_TIME  = PANEL_Y_DATE + PANEL_H_DATE + 8;
// ------------------- Clock cache -------------------
String lastWeekday = "";
String lastDate    = "";
String lastTime    = "";
// ------------------- Utilities -------------------
void drawBorderedCentered(const String &text, int y, int size,
uint16_t mainColor, uint16_t outlineColor) {
gfx.setTextWrap(false);
gfx.setTextSize(size);
int16_t x1, y1; uint16_t w, h;
gfx.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
int cx = (LCD_WIDTH - w) / 2;
gfx.setTextColor(outlineColor);
gfx.setCursor(cx - 1, y); gfx.print(text);
gfx.setCursor(cx + 1, y); gfx.print(text);
gfx.setCursor(cx, y - 1); gfx.print(text);
gfx.setCursor(cx, y + 1); gfx.print(text);
gfx.setTextColor(mainColor);
gfx.setCursor(cx, y); gfx.print(text);
}
bool isOnSnake(int x, int y) {
for (int i = 0; i < snakeLength; i++) {
if (snake[i].x == x && snake[i].y == y) return true;
}
return false;
}
// ------------------- Snake Game -------------------
void placeFood() {
for (int tries = 0; tries < 2000; tries++) {
int fx = random(0, GRID_WIDTH);
int fy = random(0, GRID_HEIGHT);
if (!isOnSnake(fx, fy)) { foodX = fx; foodY = fy; return; }
}
foodX = GRID_WIDTH/3; foodY = GRID_HEIGHT/3;
}
bool checkSelfCollision() {
for (int i = 1; i < snakeLength; i++) {
if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) return true;
}
return false;
}
void beepFood() { tone(BUZZER_PIN, 1200, 100); }
void resetGame() {
gfx.fillScreen(ILI9341_BLACK);
dx = 1; dy = 0;
snakeLength = 5;
int cx = GRID_WIDTH / 2, cy = GRID_HEIGHT / 2;
for (int i = 0; i < snakeLength; i++) {
snake[i].x = cx - i; snake[i].y = cy;
}
placeFood();
}
void runSnakeGame() {
controller.read(e);
const float threshold = 0.5;
if (fabs(e.leftStickX) > fabs(e.leftStickY)) {
if (e.leftStickX > threshold && dx == 0)      { dx = 1;  dy = 0; }
else if (e.leftStickX < -threshold && dx == 0){ dx = -1; dy = 0; }
} else {
if (e.leftStickY > threshold && dy == 0)      { dx = 0;  dy = -1; }
else if (e.leftStickY < -threshold && dy == 0){ dx = 0;  dy = 1;  }
}
prevTailX = snake[snakeLength - 1].x;
prevTailY = snake[snakeLength - 1].y;
for (int i = snakeLength - 1; i > 0; i--) snake[i] = snake[i - 1];
snake[0].x += dx; snake[0].y += dy;
if (snake[0].x >= GRID_WIDTH) snake[0].x = 0;
if (snake[0].x < 0)           snake[0].x = GRID_WIDTH - 1;
if (snake[0].y >= GRID_HEIGHT) snake[0].y = 0;
if (snake[0].y < 0)           snake[0].y = GRID_HEIGHT - 1;
if (checkSelfCollision()) { resetGame(); return; }
gfx.fillRect(prevTailX * CELL_SIZE, prevTailY * CELL_SIZE,
CELL_SIZE, CELL_SIZE, ILI9341_BLACK);
for (int i = 0; i < snakeLength; i++) {
uint8_t b = map(i, 0, snakeLength, 255, 90);
uint16_t col = gfx.color565(b, 255 - b / 3, b / 5);
gfx.fillRoundRect(snake[i].x * CELL_SIZE, snake[i].y * CELL_SIZE,
CELL_SIZE, CELL_SIZE, 2, col);
}
int pulse = (millis() / 120) % 3;
int size = CELL_SIZE - 3 + pulse;
int offset = (CELL_SIZE - size) / 2;
gfx.fillRoundRect(foodX * CELL_SIZE + offset, foodY * CELL_SIZE + offset,
size, size, 2, foodColor);
if (snake[0].x == foodX && snake[0].y == foodY) {
if (snakeLength < (int)(sizeof(snake)/sizeof(snake[0]))) snakeLength++;
placeFood(); beepFood();
}
gfx.fillRect(0, 0, LCD_WIDTH, 16, ILI9341_BLACK);
gfx.setTextSize(2);
gfx.setTextColor(ILI9341_YELLOW);
gfx.setCursor(4, 2);
gfx.print("Score: ");
gfx.print(snakeLength - 5);
delay(120);
}
// ------------------- Clock -------------------
void drawStaticPanels() {
gfx.fillScreen(ILI9341_BLACK);
gfx.fillRect(PANEL_MARGIN, PANEL_Y_WEEK, PANEL_W, PANEL_H_WEEK, ILI9341_WHITE);
gfx.drawRect(PANEL_MARGIN, PANEL_Y_WEEK, PANEL_W, PANEL_H_WEEK, ILI9341_BLACK);
gfx.fillRect(PANEL_MARGIN, PANEL_Y_DATE, PANEL_W, PANEL_H_DATE, ILI9341_WHITE);
gfx.drawRect(PANEL_MARGIN, PANEL_Y_DATE, PANEL_W, PANEL_H_DATE, ILI9341_BLACK);
gfx.fillRect(PANEL_MARGIN, PANEL_Y_TIME, PANEL_W, PANEL_H_TIME, ILI9341_WHITE);
gfx.drawRect(PANEL_MARGIN, PANEL_Y_TIME, PANEL_W, PANEL_H_TIME, ILI9341_BLACK);
}
void updatePanelText(int y, int h, String &lastText,
const String &newText, int textSize) {
if (lastText != newText) {
// Erase old text area
gfx.fillRect(PANEL_MARGIN+2, y+2, PANEL_W-4, h-4, ILI9341_WHITE);
drawBorderedCentered(newText, y + (h/2 - 8),
textSize, ILI9341_BLACK, ILI9341_WHITE);
lastText = newText;
}
}
void runClock() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
// Show error only inside time panel
gfx.fillRect(PANEL_MARGIN+2, PANEL_Y_TIME+2, PANEL_W-4, PANEL_H_TIME-4, ILI9341_WHITE);
drawBorderedCentered("Time Error", PANEL_Y_TIME + (PANEL_H_TIME/2 - 8),
3, ILI9341_BLACK, ILI9341_WHITE);
delay(500);
return;
}
char buf[20];
// Weekday
strftime(buf, sizeof(buf), "%A", &timeinfo);
updatePanelText(PANEL_Y_WEEK, PANEL_H_WEEK, lastWeekday, String(buf), 3);
// Date
strftime(buf, sizeof(buf), "%Y-%m-%d", &timeinfo);
updatePanelText(PANEL_Y_DATE, PANEL_H_DATE, lastDate, String(buf), 3);
// Time
strftime(buf, sizeof(buf), "%H:%M:%S", &timeinfo);
updatePanelText(PANEL_Y_TIME, PANEL_H_TIME, lastTime, String(buf), 4);
delay(200); // smooth refresh, no flicker
}
// ------------------- Setup -------------------
void setup() {
Serial.begin(115200);
pinMode(MODE_BUTTON, INPUT_PULLUP);
SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS);
gfx.begin();
gfx.setRotation(1);
pinMode(BUZZER_PIN, OUTPUT);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(300); }
// Configure NTP with IST offset (UTC+5:30 = 19800 seconds)
configTime(19800, 0, "pool.ntp.org", "time.nist.gov");
controller.begin();
// Initial screen
drawStaticPanels();
runClock();
}
// ------------------- Loop -------------------
void loop() {
// Debounced button toggle
if (digitalRead(MODE_BUTTON) == LOW && millis() - lastButtonPress > 500) {
currentMode = (currentMode == MODE_GAME) ? MODE_CLOCK : MODE_GAME;
lastButtonPress = millis();
if (currentMode == MODE_GAME) {
gfx.fillScreen(ILI9341_BLACK);
resetGame();
} else {
drawStaticPanels();
lastWeekday = ""; lastDate = ""; lastTime = "";
runClock();
}
}
if (currentMode == MODE_GAME) {
runSnakeGame();
} else {
runClock();
}
}

Credits

Arnov Sharma
356 projects • 361 followers
I'm Arnov. I build, design, and experiment with tech—3D printing, PCB design, and retro consoles are my jam.

Comments