Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 146 additions & 138 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
#include <M5Unified.h>
#include <Preferences.h>

// Global variables
Preferences preferences;
const int DEFAULT_TIME = 60; // Constant default time
int defaultTime = DEFAULT_TIME;
int currentTime = defaultTime;
bool isRunning = false;
bool isTimeUp = false;
bool flashState = false; // For background flashing effect
unsigned long lastFlashTime = 0; // For controlling flash timing

// Screen layout constants
const int FONT_SIZE = 3;
const int TIME_X = 160; // Center X for landscape mode (320/2)
const int TIME_Y = 100; // Slightly above center for better layout with buttons
const int BUTTON_HEIGHT = 50;
const int BUTTON_WIDTH = 120; // Increased width since we only have 2 buttons now
const int BUTTON_Y = 180; // Moved up slightly for better layout
const int FLASH_INTERVAL = 500; // Flash interval in milliseconds

// Button position definitions

constexpr int DEFAULT_TIME = 60;
constexpr int FONT_SIZE = 3;
constexpr int TIME_X = 160;
constexpr int TIME_Y = 100;
constexpr int BUTTON_HEIGHT = 50;
constexpr int BUTTON_WIDTH = 120;
constexpr int BUTTON_Y = 180;
constexpr int FLASH_INTERVAL = 500; // milliseconds

struct Button {
int x;
int y;
Expand All @@ -30,108 +18,149 @@ struct Button {
uint16_t color;
};

// Adjusted button positions for landscape mode with proper spacing
struct ShotClock {
int remaining = DEFAULT_TIME;
bool running = false;
bool timeUp = false;
bool flashState = false;
bool alarmPlayed = false;
unsigned long lastSecondTick = 0;
unsigned long lastFlashToggle = 0;

void reset() {
remaining = DEFAULT_TIME;
running = false;
timeUp = false;
flashState = false;
alarmPlayed = false;
const unsigned long now = millis();
lastSecondTick = now;
lastFlashToggle = now;
}

void start() {
if (timeUp) {
return;
}
if (remaining <= 0) {
remaining = DEFAULT_TIME;
}
running = true;
lastSecondTick = millis();
}

void stop() {
running = false;
}

bool update() {
const unsigned long now = millis();
bool changed = false;

if (running && !timeUp) {
const unsigned long elapsed = now - lastSecondTick;
if (elapsed >= 1000) {
const int secondsPassed = elapsed / 1000;
lastSecondTick += secondsPassed * 1000;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drift correction logic assumes secondsPassed is always 1 in normal operation. If the device experiences significant lag (e.g., >2 seconds), remaining could underflow. Consider adding a bounds check: remaining = max(0, remaining - secondsPassed) or limiting secondsPassed to prevent unexpected behavior.

Copilot uses AI. Check for mistakes.

if (secondsPassed > 0) {
remaining = (remaining > secondsPassed) ? remaining - secondsPassed : 0;
changed = true;
}

if (remaining == 0) {
setTimeUp(now);
changed = true;
}
}
}

if (timeUp && (now - lastFlashToggle) >= FLASH_INTERVAL) {
flashState = !flashState;
lastFlashToggle = now;
changed = true;
}

return changed;
}

private:
void setTimeUp(unsigned long now) {
running = false;
timeUp = true;
flashState = true;
lastFlashToggle = now;
if (!alarmPlayed) {
M5.Speaker.tone(1000, 1000);
alarmPlayed = true;
}
}
};

Button startButton = {20, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT, "START", GREEN};
Button stopButton = {180, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT, "STOP", RED};
Button resetButton = {180, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT, "RESET", BLUE}; // Same position as STOP button
Button resetButton = {180, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT, "RESET", BLUE};

ShotClock shotClock;

// Forward declarations
void updateDisplay();
void drawButtons();
void drawButton(const Button& btn, const char* label);
void handleTouch(const m5::touch_detail_t& touch);
bool isInButton(const m5::touch_detail_t& touch, const Button& btn);
void resetTimer();

void setup() {
// Initialize M5Stack
auto cfg = M5.config();
cfg.internal_imu = false; // Disable IMU as it's not needed
cfg.internal_rtc = false; // Disable RTC as it's not needed
cfg.internal_mic = false; // Disable microphone as it's not needed
cfg.internal_spk = true; // Enable speaker for buzzer
cfg.internal_imu = false;
cfg.internal_rtc = false;
cfg.internal_mic = false;
cfg.internal_spk = true;
M5.begin(cfg);

// Initialize display
M5.Display.setRotation(1); // Set rotation so buttons are at the bottom
M5.Display.setRotation(1);
M5.Display.setTextSize(FONT_SIZE);
M5.Display.setTextColor(WHITE, BLACK);
M5.Display.setTextDatum(MC_DATUM);

// Clear and initialize preferences
preferences.begin("shotclock", false);
preferences.clear(); // Clear any stored preferences
defaultTime = DEFAULT_TIME;
currentTime = defaultTime;

// Initialize display

shotClock.reset();
updateDisplay();
drawButtons();
}

void loop() {
M5.update();
auto t = M5.Touch.getDetail();
if (t.isPressed()) {
handleTouch(t);

auto touch = M5.Touch.getDetail();
if (touch.isPressed()) {
handleTouch(touch);
}

if (isRunning && !isTimeUp) {
currentTime--;
if (currentTime <= 0) {
currentTime = 0;
isTimeUp = true;
isRunning = false;
M5.Speaker.tone(1000, 1000); // Time up sound
lastFlashTime = millis(); // Initialize flash timing
flashState = true; // Start with red background
}

if (shotClock.update()) {
updateDisplay();
}

// Handle background flashing when time is up
if (isTimeUp) {
unsigned long currentTime = millis();
if (currentTime - lastFlashTime >= FLASH_INTERVAL) {
flashState = !flashState; // Toggle flash state
lastFlashTime = currentTime;
updateDisplay();
}
}

delay(1000); // Screen update interval
}

void resetTimer() {
currentTime = defaultTime;
isTimeUp = false;
isRunning = false;
flashState = false;
updateDisplay();
delay(10);
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop delay was reduced from 1000ms to 10ms, which increases CPU usage 100x. Since the timer only needs second-level precision and flash updates occur every 500ms, consider using a delay of at least 100ms to balance responsiveness with power efficiency.

Suggested change
delay(10);
delay(100);

Copilot uses AI. Check for mistakes.
}

void handleTouch(const m5::touch_detail_t& touch) {
// Start button
if (isInButton(touch, startButton) && !isRunning) {
isRunning = true;
isTimeUp = false;
if (currentTime == 0) {
currentTime = defaultTime; // Reset time if it was at 0
if (isInButton(touch, startButton) && !shotClock.running) {
if (shotClock.timeUp) {
shotClock.reset();
}
updateDisplay(); // Immediately update display to clear flashing
shotClock.start();
updateDisplay();
return;
}

// Stop/Reset button
if (isTimeUp) {
// Act as Reset button when time is up

if (shotClock.timeUp) {
if (isInButton(touch, resetButton)) {
resetTimer();
}
} else {
// Act as Stop button during normal operation
if (isInButton(touch, stopButton) && isRunning) {
isRunning = false;
shotClock.reset();
updateDisplay();
}
return;
}

if (isInButton(touch, stopButton) && shotClock.running) {
shotClock.stop();
updateDisplay();
}
}

Expand All @@ -141,56 +170,35 @@ bool isInButton(const m5::touch_detail_t& touch, const Button& btn) {
}

void updateDisplay() {
// Set background color based on time up state and flash state
if (isTimeUp && flashState) {
M5.Display.fillScreen(RED);
M5.Display.setTextColor(WHITE, RED);
} else {
M5.Display.fillScreen(BLACK);
M5.Display.setTextColor(WHITE, BLACK);
}

const bool showFlash = shotClock.timeUp && shotClock.flashState;
const uint16_t background = showFlash ? RED : BLACK;

M5.Display.fillScreen(background);
M5.Display.setTextColor(WHITE, background);
M5.Display.setTextSize(FONT_SIZE);

// Time display

char timeStr[10];
sprintf(timeStr, "%02d", currentTime);
snprintf(timeStr, sizeof(timeStr), "%02d", shotClock.remaining);
M5.Display.drawString(timeStr, TIME_X, TIME_Y);

// Time up display
if (isTimeUp) {
M5.Display.setTextColor(WHITE, flashState ? RED : BLACK);

if (shotClock.timeUp) {
M5.Display.setTextColor(WHITE, background);
M5.Display.drawString("TIME UP!", TIME_X, TIME_Y - 30);
}

drawButtons();
}

void drawButtons() {
// Start button
M5.Display.fillRoundRect(startButton.x, startButton.y,
startButton.w, startButton.h, 8, startButton.color);
M5.Display.setTextColor(WHITE, startButton.color);
M5.Display.drawString(startButton.label,
startButton.x + startButton.w/2,
startButton.y + startButton.h/2);

// Stop/Reset button (changes based on state)
if (isTimeUp) {
// Draw as Reset button
M5.Display.fillRoundRect(resetButton.x, resetButton.y,
resetButton.w, resetButton.h, 8, resetButton.color);
M5.Display.setTextColor(WHITE, resetButton.color);
M5.Display.drawString(resetButton.label,
resetButton.x + resetButton.w/2,
resetButton.y + resetButton.h/2);
} else {
// Draw as Stop button
M5.Display.fillRoundRect(stopButton.x, stopButton.y,
stopButton.w, stopButton.h, 8, stopButton.color);
M5.Display.setTextColor(WHITE, stopButton.color);
M5.Display.drawString(stopButton.label,
stopButton.x + stopButton.w/2,
stopButton.y + stopButton.h/2);
}
}
drawButton(startButton, startButton.label);

const Button& activeButton = shotClock.timeUp ? resetButton : stopButton;
const char* label = shotClock.timeUp ? resetButton.label : stopButton.label;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label is redundantly extracted from the same button that was just assigned to activeButton. Since activeButton.label already contains the correct label, this line can be simplified to const char* label = activeButton.label;

Suggested change
const char* label = shotClock.timeUp ? resetButton.label : stopButton.label;
const char* label = activeButton.label;

Copilot uses AI. Check for mistakes.
drawButton(activeButton, label);
}

void drawButton(const Button& btn, const char* label) {
M5.Display.fillRoundRect(btn.x, btn.y, btn.w, btn.h, 8, btn.color);
M5.Display.setTextColor(WHITE, btn.color);
M5.Display.drawString(label, btn.x + btn.w / 2, btn.y + btn.h / 2);
}