// === AIRSOFT POINT – stabilna različica (utrjena) + meni "Prikaz časa" ===
// PATCHI (samo 1,2,4):
// (1) RTC robustnost: okno sprejema temelji na pričakovanem času (millis drift), ne na +5s.
// (2) I2C/RTC recovery: ob zaporednih napakah poskusi re-init I2C + LCD + RTC; ko rtcOk=false periodično retry.
// (4) drawEditInterval(): poenostavljen, čist in 100% pravilen izris vrstic OD/DO (brez krhkih izračunov).
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <EEPROM.h>
#include <avr/pgmspace.h>
#define USE_WDT 1
#if USE_WDT
#include <avr/wdt.h>
#include <avr/interrupt.h>
#include <avr/io.h>
uint8_t reset_cause __attribute__((section(".noinit")));
void store_reset_cause() __attribute__((naked)) __attribute__((section(".init3")));
void store_reset_cause(){ reset_cause = MCUSR; MCUSR = 0; }
#endif
// ------------------ KONSTANTE ------------------
const uint8_t MAX_INTERVALS = 9;
// ------------------ TIPI ------------------
struct Interval { uint8_t sh, sm, eh, em; };
struct SettingsEE {
uint8_t magic, version;
uint8_t gameEndH, gameEndM;
uint8_t ivlCount;
Interval ivl[MAX_INTERVALS];
uint8_t modeOpenBase;
uint8_t teamCount;
uint8_t showHoldTime; // NOVO
uint8_t crc;
};
struct RtSnap {
uint8_t magic;
uint8_t version;
uint32_t ts;
uint32_t secs[4];
int8_t active;
uint8_t crc;
};
// ------------------ PROTOTIPI ------------------
uint8_t crc8_dallas(const uint8_t* data, size_t len);
template<typename T> void eepromWriteUpdate(int address, const T &value);
template<typename T> void eepromRead(int address, T &value);
uint8_t snap_crcRt(const RtSnap& s);
int findLatestSnapSlot();
void writeRtSnap();
bool readRtSnap(RtSnap &out);
uint16_t toMin(uint8_t h, uint8_t m);
void sortIntervals(Interval arr[], uint8_t n);
bool validateIntervals(Interval arr[], uint8_t n, char* msg20);
int8_t containingIvlIdx(uint8_t h, uint8_t m);
bool insideAnyInterval(uint8_t h, uint8_t m);
bool isPointOpenNow(uint8_t h, uint8_t m);
bool getOpenHint(uint8_t h, uint8_t m, uint8_t &outH, uint8_t &outM);
bool pastGameEnd(uint8_t h, uint8_t m);
void drawSetTeamCount();
void handleSetTeamCount();
void drawSetShowTime(); // NOVO
void handleSetShowTime(); // NOVO
void drawSetClosureCount();
void handleSetClosureCount();
bool anyTeamHasTimeOrActive();
void showOpenStartScreen();
void showCurrentHolder();
void showClosedScreen(uint8_t openH, uint8_t openM);
void showHolding();
void goDrawPrompt();
void goDrawResults();
void drawReadyStart();
void handleReadyStart();
void drawAskMode();
void handleAskMode();
void drawSetGameEnd();
void handleSetGameEnd();
void drawSetRtcHM();
void handleSetRtcHM();
void drawEditInterval();
void handleEditInterval();
void showRefResults();
void drawReview();
void handleReview();
// ------------------ GLOBALI ------------------
LiquidCrystal_I2C lcd(0x27, 20, 4);
RTC_DS1307 rtc; // v realnem HW lahko zamenjaš na RTC_DS3231
bool rtcOk = false;
// --- PATCH (2): I2C/RTC recovery & retry ---
static unsigned long rtcRetryAtMs = 0;
static const unsigned long RTC_RETRY_MS = 15000UL; // vsakih 15s poskusi znova
static bool i2cRecoveryInProgress = false;
// --- ROBUSTNA URA (BREZ SKOKA NA 00:00:00) ---
static uint32_t lastUnixGood = 0; // zadnji potrjen UNIX čas
static unsigned long lastUnixMs = 0; // millis() ob zadnji potrditvi
static uint8_t rtcBadStreak = 0; // zaporedne slabe meritve
// dvojno branje za potrditev in monotono naraščanje
bool rtcDoubleRead(uint32_t &unixOut){
DateTime a = rtc.now();
DateTime b = rtc.now();
uint32_t ua = a.unixtime(), ub = b.unixtime();
if (ub >= ua) { unixOut = ub; return true; }
return false;
}
const uint8_t BTN_PINS[4] = {2,3,4,5};
const char* TEAM_NAME[4] = {"RDECI","MODRI","ZELENI","RUMENI"};
const uint8_t BUZZER_PIN = 6;
unsigned long lastBeepAt = 0;
const unsigned long BEEP_GAP_MS = 150;
const uint16_t DEBOUNCE_MS = 20;
const uint16_t REPEAT_DELAY = 400;
const uint16_t REPEAT_RATE = 150;
const uint8_t HOLD_SECONDS = 5;
// UI state
enum UiState {
INTRO_GREETING, SHOW_CLOCK, SET_RTC_HM, SET_TEAM_COUNT, SET_SHOWTIME, SET_GAMEEND_Q,
ASK_MODE, SET_CLOSURE_COUNT, EDIT_INTERVAL, REVIEW, READY_START,
GAME, HOLDING, SUCCESS, GAME_OVER
};
UiState ui = INTRO_GREETING;
enum GameOverSub { GO_PROMPT, GO_RESULTS };
GameOverSub goSub = GO_PROMPT;
bool modeOpenBase = true;
uint8_t gameEndH=0, gameEndM=0;
uint8_t teamCount = 4;
bool showHoldTime = true; // NOVO
Interval ivl[MAX_INTERVALS];
uint8_t ivlCount = 0;
bool ivlUsed[MAX_INTERVALS];
int8_t lastIvlIdx = -1;
void resetIvlUsed(){ for(uint8_t i=0;i<MAX_INTERVALS;i++) ivlUsed[i]=false; lastIvlIdx=-1; }
const uint8_t EE_MAGIC = 0xA5;
const uint8_t EE_VERSION = 6; // NOVO: bump zaradi novega polja
const int SNAP_BASE = 128;
const int SNAP_SLOTS = 16;
uint32_t snapCounter = 0;
unsigned long lastSnapAt=0;
uint32_t teamSeconds[4]={0,0,0,0};
int8_t activeTeam=-1;
bool firstCaptureDone = false;
bool lastClosed=false;
int8_t candidateTeam=-1;
unsigned long activeStartMs=0;
unsigned long holdStartMs=0, lastHoldTick=0;
unsigned long lastUiTick=0;
int8_t lastShownTeam = -2;
bool refViewActive=false;
unsigned long refHoldStartMs=0;
char lineCache[4][21];
void resetLineCache(){ for(uint8_t r=0;r<4;r++){ for(uint8_t c=0;c<20;c++) lineCache[r][c]=' '; lineCache[r][20]=0; } }
// RTC UREJANJE
uint8_t rtcEditH=0, rtcEditM=0, rtcDigitIndex=0; bool rtcBlinkOn=true;
unsigned long rtcBlinkT=0; const uint16_t RTC_BLINK_MS=400;
// REVIEW stanje
uint8_t reviewIdx = 0;
bool reviewNeedsClear = true;
// Globali za prevzem
uint8_t holdBeepSec = 0;
int16_t holdLastSteps = -1;
// --------------- Ura: cache + blokada ----------------
char lastGoodClockStr[9] = "00:00:00";
unsigned long lastClockDraw = 0;
bool clockBlocked = false; // skrita/ustavljena ura
bool clockArmedForUnblock = false; // odblokiraj šele ob prvem GAME-izrisu
// --- NOVO: da glavo PREVZEM narišemo le enkrat (odpravi utripanje) ---
bool holdHeaderDrawn = false;
// ------------------ CRC-8 Dallas/Maxim ------------------
uint8_t crc8_dallas(const uint8_t* data, size_t len){
uint8_t crc = 0x00;
while (len--){
uint8_t inbyte = *data++;
for (uint8_t i=0;i<8;i++){
uint8_t mix = (crc ^ inbyte) & 0x01;
crc >>= 1;
if (mix) crc ^= 0x8C;
inbyte >>= 1;
}
}
return crc;
}
// ------------------ EEPROM helperji ------------------
template<typename T>
void eepromWriteUpdate(int address, const T &value){
const uint8_t* p = (const uint8_t*)&value;
for (unsigned i=0; i<sizeof(T); ++i){
EEPROM.update(address+i, p[i]);
}
}
template<typename T>
void eepromRead(int address, T &value){
uint8_t* p = (uint8_t*)&value;
for (unsigned i=0; i<sizeof(T); ++i){
p[i] = EEPROM.read(address+i);
}
}
// ------------------ Nastavitve v EEPROM ------------------
void saveSettings(){
SettingsEE s{};
s.magic=EE_MAGIC; s.version=EE_VERSION;
s.gameEndH=gameEndH; s.gameEndM=gameEndM;
s.ivlCount=ivlCount;
for(uint8_t i=0;i<MAX_INTERVALS;i++) s.ivl[i]=ivl[i];
s.modeOpenBase = modeOpenBase?1:0;
s.teamCount = teamCount;
s.showHoldTime = showHoldTime ? 1 : 0; // NOVO
s.crc = crc8_dallas((const uint8_t*)&s, sizeof(SettingsEE)-1);
eepromWriteUpdate(0, s);
}
bool loadSettings(){
SettingsEE s; eepromRead(0, s);
if(s.magic!=EE_MAGIC) return false;
if(s.version!=EE_VERSION) return false;
if(crc8_dallas((const uint8_t*)&s, sizeof(SettingsEE)-1)!=s.crc) return false;
gameEndH=s.gameEndH; gameEndM=s.gameEndM;
ivlCount=s.ivlCount; if(ivlCount>MAX_INTERVALS) ivlCount=0;
for(uint8_t i=0;i<MAX_INTERVALS;i++) ivl[i]=s.ivl[i];
modeOpenBase=(s.modeOpenBase!=0);
teamCount = (s.teamCount<2 || s.teamCount>4) ? 4 : s.teamCount;
showHoldTime = (s.showHoldTime!=0); // NOVO
return true;
}
void saveDefaultsToEEPROM(){
SettingsEE s{}; s.magic=EE_MAGIC; s.version=EE_VERSION;
s.gameEndH=0; s.gameEndM=0; s.ivlCount=0;
for(uint8_t i=0;i<MAX_INTERVALS;i++) s.ivl[i]={0,0,0,0};
s.modeOpenBase=1;
s.teamCount=4;
s.showHoldTime=1; // NOVO
s.crc=crc8_dallas((const uint8_t*)&s, sizeof(SettingsEE)-1);
eepromWriteUpdate(0, s);
}
// ------------------ Wear-level SNAPSHOT ------------------
uint8_t snap_crcRt(const RtSnap& s){
return crc8_dallas((const uint8_t*)&s, sizeof(RtSnap)-1);
}
int findLatestSnapSlot(){
RtSnap best{}; uint32_t bestTs=0; int bestIdx=-1;
for(int i=0;i<SNAP_SLOTS;i++){
RtSnap r{}; eepromRead(SNAP_BASE + i*sizeof(RtSnap), r);
if(r.magic==0x5A && r.version==1 && snap_crcRt(r)==r.crc){
if(r.ts>=bestTs){ bestTs=r.ts; best=r; bestIdx=i; }
}
}
if(bestIdx!=-1) snapCounter=bestTs+1;
return bestIdx;
}
void writeRtSnap(){
RtSnap s{};
s.magic=0x5A; s.version=1; s.ts=snapCounter++;
for(uint8_t i=0;i<4;i++) s.secs[i]=teamSeconds[i];
s.active=activeTeam;
s.crc=snap_crcRt(s);
int slot = s.ts % SNAP_SLOTS;
eepromWriteUpdate(SNAP_BASE + slot*sizeof(RtSnap), s);
}
bool readRtSnap(RtSnap &out){
int idx = findLatestSnapSlot();
if(idx<0) return false;
eepromRead(SNAP_BASE + idx*sizeof(RtSnap), out);
return (out.magic==0x5A && out.version==1 && out.crc==snap_crcRt(out));
}
// --------- Helperji ---------
unsigned long nowMs(){ return millis(); }
inline bool timePassed(unsigned long ts) { return (long)(nowMs() - ts) >= 0; }
void safeBeep(uint16_t freq=2000, uint16_t ms=120){
unsigned long now = millis();
if ((now - lastBeepAt) < BEEP_GAP_MS) return;
noTone(BUZZER_PIN);
tone(BUZZER_PIN, freq, ms);
lastBeepAt = now;
}
void safeDelay(uint16_t ms){
unsigned long t=nowMs();
while((long)(nowMs()-t) < (long)ms){
#if USE_WDT
wdt_reset();
#endif
}
}
// --------- LCD helpers ---------
void lcdPrintFixed(uint8_t row, const char *txt){
char out[21]; uint8_t i=0;
for (; i<20 && txt[i]; i++) out[i] = txt[i];
while (i < 20) out[i++] = ' ';
out[20] = 0;
bool diff = false;
for (uint8_t k=0; k<20; k++){
if (out[k] != lineCache[row][k]) { diff = true; break; }
}
if (diff){
lcd.setCursor(0, row);
lcd.print(out);
for (uint8_t m=0; m<21; m++) lineCache[row][m] = out[m];
}
}
void lcdPrintFixed(uint8_t row, const __FlashStringHelper *txtFlash){
char out[21]; uint8_t i=0;
PGM_P p = reinterpret_cast<PGM_P>(txtFlash);
while (i < 20){
char c = pgm_read_byte(p++);
if (!c) break;
out[i++] = c;
}
while (i < 20) out[i++] = ' ';
out[20] = 0;
bool diff = false;
for (uint8_t k=0; k<20; k++){
if (out[k] != lineCache[row][k]) { diff = true; break; }
}
if (diff){
lcd.setCursor(0, row);
lcd.print(out);
for (uint8_t m=0; m<21; m++) lineCache[row][m] = out[m];
}
}
void lcdCenterRam(uint8_t row, const char* txt){
char line[21]; for(uint8_t i=0;i<20;i++) line[i]=' '; line[20]=0;
uint8_t len=0; while(txt[len] && len<20) len++;
uint8_t col=(20-len)/2;
for(uint8_t i=0;i<len;i++) line[col+i]=txt[i];
lcdPrintFixed(row, line);
}
void lcdCentered(uint8_t row, const __FlashStringHelper* textFlash){
char tmp[21]; uint8_t i=0; PGM_P p = reinterpret_cast<PGM_P>(textFlash);
while(i<20){ char c=pgm_read_byte(p++); if(!c) break; tmp[i++]=c; }
tmp[i]=0; lcdCenterRam(row, tmp);
}
// --- Row0 helpers: piši/čisti samo levo stran (0..11) ---
void lcdClearRow0Left(){
lcd.setCursor(0, 0);
lcd.print(" "); // 12 presledkov
for(uint8_t i=0;i<12;i++) lineCache[0][i]=' ';
}
void lcdWriteRow0Left(const char* txt){
char buf[13]; uint8_t i=0;
for(; i<12 && txt[i]; ++i) buf[i]=txt[i];
for(; i<12; ++i) buf[i]=' ';
buf[12]=0;
bool diff=false;
for(uint8_t k=0;k<12;k++) if(buf[k]!=lineCache[0][k]) { diff=true; break; }
if(diff){
lcd.setCursor(0,0);
lcd.print(buf);
for(uint8_t k=0;k<12;k++) lineCache[0][k]=buf[k];
}
}
// ------------------ Gumbi ------------------
struct Btn{
uint8_t pin; bool stable,lastStable,raw;
unsigned long lastChange;
bool repeating; unsigned long firstPressTime,lastRepeatTime;
} btn[4];
unsigned long inputBlockUntil=0;
void btnInit(uint8_t i, uint8_t pin){
pinMode(pin, INPUT_PULLUP);
bool r = (digitalRead(pin)==LOW);
btn[i]={pin,r,r,r, nowMs(), false, 0,0};
}
void btnUpdate(uint8_t i){
bool r=(digitalRead(btn[i].pin)==LOW);
if(r!=btn[i].raw){ btn[i].raw=r; btn[i].lastChange=nowMs(); }
if(nowMs()-btn[i].lastChange>=DEBOUNCE_MS){
btn[i].lastStable=btn[i].stable;
btn[i].stable=btn[i].raw;
}
if(btn[i].stable){
if(!btn[i].repeating){
if(btn[i].firstPressTime==0) btn[i].firstPressTime=nowMs();
if(nowMs()-btn[i].firstPressTime>=REPEAT_DELAY){ btn[i].repeating=true; btn[i].lastRepeatTime=nowMs(); }
} else if(nowMs()-btn[i].lastRepeatTime>=REPEAT_RATE){
btn[i].lastRepeatTime=nowMs();
}
} else { btn[i].repeating=false; btn[i].firstPressTime=0; btn[i].lastRepeatTime=0; }
}
bool btnPressed(uint8_t i){ return (btn[i].stable && !btn[i].lastStable); }
bool btnRepeatEvent(uint8_t i){
if(!btn[i].stable || !btn[i].repeating) return false;
static unsigned long lastEmit[4]={0,0,0,0};
if(btn[i].lastRepeatTime!=lastEmit[i]){ lastEmit[i]=btn[i].lastRepeatTime; return true; }
return false;
}
bool menuClick(uint8_t i){ return btnPressed(i) || btnRepeatEvent(i); }
bool up(){return menuClick(0);} bool down(){return menuClick(1);} bool ok(){return btnPressed(2);} bool back(){return btnPressed(3);}
void syncButtons(){ for(int i=0;i<4;i++) btn[i].lastStable=btn[i].stable; }
// ------------------ Čas/intervali ------------------
uint16_t toMin(uint8_t h, uint8_t m){ return (uint16_t)h*60+m; }
void seedIntervalsFromNow(uint8_t count){
DateTime cur; // bo nastavljeno z nowRTC()
// nowRTC() je spodaj (zaradi patcha), zato samo deklaracija tukaj ni dovolj.
// Klic bo ostal enak kot prej (funkcija je definirana kasneje).
cur = DateTime(946684800UL); // placeholder; takoj prepišemo spodaj v definiciji funkcij (C++ dopušča)
(void)cur;
DateTime cur2 = nowRTC();
uint16_t base=toMin(cur2.hour(),cur2.minute());
for(uint8_t i=0;i<count;i++){
bool allZero=(ivl[i].sh|ivl[i].sm|ivl[i].eh|ivl[i].em)==0;
if(allZero){
uint16_t s=(base + i*30)%(24*60), e=(s+30)%(24*60);
ivl[i]={(uint8_t)(s/60),(uint8_t)(s%60),(uint8_t)(e/60),(uint8_t)(e%60)};
} else if(ivl[i].eh==0 && ivl[i].em==0){
uint16_t s=toMin(ivl[i].sh,ivl[i].sm), e=(s+30)%(24*60);
ivl[i].eh=e/60; ivl[i].em=e%60;
}
}
}
void sortIntervals(Interval arr[], uint8_t n){
for(uint8_t i=1;i<n;i++){
Interval key=arr[i]; int j=i-1;
while(j>=0 && toMin(arr[j].sh,arr[j].sm) > toMin(key.sh,key.sm)){
arr[j+1]=arr[j];
if(j==0){ j=-1; break; }
j--;
}
arr[j+1]=key;
}
}
bool validateIntervals(Interval arr[], uint8_t n, char* msg20){
if(n==0) return true;
for(uint8_t i=0;i<n;i++){
if(arr[i].sh>23||arr[i].eh>23||arr[i].sm>59||arr[i].em>59){
if(msg20){ msg20[0]='B'; msg20[1]='A'; msg20[2]='D'; msg20[3]=0; }
return false;
}
}
static uint8_t used[(24*60+7)/8];
for(uint16_t i=0;i<sizeof(used);i++) used[i]=0;
auto GETBIT = [](const uint8_t* b, uint16_t idx)->bool { return (b[idx>>3]>>(idx&7))&1; };
auto SETBIT = [](uint8_t* b, uint16_t idx){ b[idx>>3]|=(uint8_t)(1u<<(idx&7)); };
for(uint8_t i=0;i<n;i++){
uint16_t s=toMin(arr[i].sh,arr[i].sm), e=toMin(arr[i].eh,arr[i].em);
if(e>s){
for(uint16_t m=s;m<e;m++){ if(GETBIT(used,m)) return false; SETBIT(used,m); }
} else {
for(uint16_t m=s;m<24*60;m++){ if(GETBIT(used,m)) return false; SETBIT(used,m); }
for(uint16_t m=0;m<e;m++){ if(GETBIT(used,m)) return false; SETBIT(used,m); }
}
}
return true;
}
int8_t containingIvlIdx(uint8_t h, uint8_t m){
uint16_t nm=h*60+m;
for(uint8_t i=0;i<ivlCount;i++){
if(i>=MAX_INTERVALS) break;
if(ivlUsed[i]) continue;
uint16_t s=toMin(ivl[i].sh,ivl[i].sm), e=toMin(ivl[i].eh,ivl[i].em);
if(e>s){ if(nm>=s && nm<e) return i; }
else { if(nm>=s || nm<e) return i; }
}
return -1;
}
bool insideAnyInterval(uint8_t h, uint8_t m){ return (containingIvlIdx(h,m)!=-1); }
bool isPointOpenNow(uint8_t h, uint8_t m){
if(ivlCount==0) return modeOpenBase;
bool inside=insideAnyInterval(h,m);
return modeOpenBase ? (!inside) : inside;
}
bool getOpenHint(uint8_t h, uint8_t m, uint8_t &outH, uint8_t &outM){
uint16_t nm=toMin(h,m);
if(modeOpenBase){
int8_t idx=containingIvlIdx(h,m);
if(idx!=-1){ outH=ivl[idx].eh; outM=ivl[idx].em; return true; }
return false;
} else {
int16_t best=32767; bool found=false; uint8_t bh=0,bm=0;
for(uint8_t i=0;i<ivlCount;i++){
if(i>=MAX_INTERVALS) break;
if(ivlUsed[i]) continue;
uint16_t s=toMin(ivl[i].sh,ivl[i].sm);
int16_t d=(int16_t)s-(int16_t)nm;
if(d>=0 && d<best){ bh=ivl[i].sh; bm=ivl[i].sm; found=true; }
}
if(found){ outH=bh; outM=bm; return true; }
}
return false;
}
bool pastGameEnd(uint8_t h, uint8_t m){
if (gameEndH==0 && gameEndM==0) return false;
return (h*60+m)>=(gameEndH*60+gameEndM);
}
// ------------------ Ura: helperji ------------------
void hideClockTopRight(){
lcd.setCursor(12, 0);
lcd.print(" "); // 8 presledkov
for(uint8_t i=0;i<8;i++) lineCache[0][12+i] = ' ';
}
// --- custom bar chars ---
bool barCharsInited=false;
void initBarChars(){
if(barCharsInited) return;
byte bar1[8]={B00000,B00000,B00000,B00000,B00000,B10000,B10000,B10000};
byte bar2[8]={B00000,B00000,B00000,B00000,B00000,B11000,B11000,B11000};
byte bar3[8]={B00000,B00000,B00000,B00000,B00000,B11100,B11100,B11100};
byte bar4[8]={B00000,B00000,B00000,B00000,B00000,B11110,B11110,B11110};
byte bar5[8]={B00000,B00000,B00000,B00000,B00000,B11111,B11111,B11111};
lcd.createChar(0, bar1);
lcd.createChar(1, bar2);
lcd.createChar(2, bar3);
lcd.createChar(3, bar4);
lcd.createChar(4, bar5);
barCharsInited=true;
}
// --- PATCH (2): I2C recovery helper (ne spreminja ostalih funkcij) ---
void i2cRecover(){
if(i2cRecoveryInProgress) return;
i2cRecoveryInProgress = true;
// reinit Wire
Wire.begin();
Wire.setClock(100000);
#if defined(TWOWIRE_HAS_TIMEOUT) || defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(25000 /*us*/, true);
#endif
// reinit LCD (brez spreminjanja logike UI – samo stabilnost)
lcd.init();
lcd.backlight();
lcd.noCursor();
lcd.noBlink();
resetLineCache();
// custom chars se po init() izgubijo
barCharsInited = false;
initBarChars();
// retry RTC begin
rtcOk = rtc.begin();
rtcBadStreak = 0;
i2cRecoveryInProgress = false;
}
// --- PATCH (1)+(2): robust nowRTC() z drift oknom + retry RTC ---
DateTime nowRTC(){
// inicializacija ob prvem klicu
if (lastUnixMs == 0){
if (rtcOk){
uint32_t u;
if (rtcDoubleRead(u)){
lastUnixGood = u;
} else {
lastUnixGood = 946684800UL; // 2000-01-01
rtcBadStreak = 1;
}
} else {
lastUnixGood = 946684800UL; // 2000-01-01
}
lastUnixMs = millis();
}
// če RTC trenutno ni OK -> periodični retry
if(!rtcOk){
if(rtcRetryAtMs == 0) rtcRetryAtMs = millis();
if((long)(millis() - rtcRetryAtMs) >= (long)RTC_RETRY_MS){
rtcRetryAtMs = millis();
rtcOk = rtc.begin();
if(rtcOk){
uint32_t u;
if(rtcDoubleRead(u)){
lastUnixGood = u;
lastUnixMs = millis();
rtcBadStreak = 0;
} else {
rtcOk = false;
}
}
}
}
// poskusi svež RTC odčitek
if (rtcOk){
uint32_t u;
if (rtcDoubleRead(u)){
uint32_t elapsed = (millis() - lastUnixMs) / 1000UL;
uint32_t expected = lastUnixGood + elapsed;
// PATCH (1): sprejmi RTC, če je blizu pričakovanemu času (in ne “samo +5s”)
// toleranca: -2s .. +3s (lahko prilagodiš), in dodatno varovalo pred velikimi skoki naprej (> 2 minuti)
const int32_t NEG_TOL = 2;
const int32_t POS_TOL = 3;
const uint32_t MAX_JUMP_FWD = 120UL;
int32_t diff = (int32_t)u - (int32_t)expected;
bool okWindow = (diff >= -NEG_TOL && diff <= POS_TOL);
bool okMonotonic = (u >= lastUnixGood); // nikoli nazaj
bool okJump = (u <= expected + MAX_JUMP_FWD);
if(okWindow && okMonotonic && okJump){
lastUnixGood = u;
lastUnixMs = millis();
rtcBadStreak = 0;
} else {
if(++rtcBadStreak >= 5){
// PATCH (2): najprej poskusi I2C recovery, šele nato “odklopi” RTC
i2cRecover();
// če po recovery še vedno ni ok, preklopi na soft in pusti retry mehanizmu, da ga vrne
if(!rtcOk) {
rtcOk = false;
}
rtcBadStreak = 0;
}
}
} else {
if(++rtcBadStreak >= 5){
i2cRecover();
rtcBadStreak = 0;
}
}
}
// izračun trenutka z drsenjem po millis()
uint32_t elapsed = (millis() - lastUnixMs) / 1000UL;
return DateTime(lastUnixGood + elapsed);
}
void updateClockCacheIfAllowed(){
if (clockBlocked) return;
DateTime t = nowRTC();
uint8_t H = t.hour(), M = t.minute(), S = t.second();
lastGoodClockStr[0] = '0' + (H/10);
lastGoodClockStr[1] = '0' + (H%10);
lastGoodClockStr[2] = ':';
lastGoodClockStr[3] = '0' + (M/10);
lastGoodClockStr[4] = '0' + (M%10);
lastGoodClockStr[5] = ':';
lastGoodClockStr[6] = '0' + (S/10);
lastGoodClockStr[7] = '0' + (S%10);
lastGoodClockStr[8] = 0;
}
void drawClockTopRight(){
if (clockBlocked) return;
lcd.setCursor(12, 0);
lcd.print(lastGoodClockStr);
for(uint8_t i=0;i<8;i++) lineCache[0][12+i] = lastGoodClockStr[i];
}
// ------------------ Igra helperji ------------------
void flushActiveTeam(){
if(activeTeam!=-1){
teamSeconds[activeTeam] += (nowMs()-activeStartMs)/1000UL;
activeTeam=-1;
}
}
// Inicializacija / preklic / commit prevzema
void startHolding(int8_t team){
candidateTeam=(team<teamCount?team:-1);
holdStartMs=nowMs();
lastHoldTick=nowMs();
holdBeepSec = 0;
holdLastSteps = -1;
noTone(BUZZER_PIN);
// Ura skrita v celotnem HOLDING
clockBlocked = true;
clockArmedForUnblock = false;
hideClockTopRight();
// NOVO: glavo narišemo le enkrat
holdHeaderDrawn = false;
ui=HOLDING;
}
void cancelHolding(){
ui=GAME;
candidateTeam=-1;
holdBeepSec = 0;
holdLastSteps = -1;
lastShownTeam = -2;
noTone(BUZZER_PIN);
inputBlockUntil = nowMs() + 120UL;
// *** KLJUČNO: po prekinitvi takoj vrni uro ***
clockBlocked = false;
clockArmedForUnblock = false;
updateClockCacheIfAllowed();
drawClockTopRight();
// NOVO: resetiraj flag
holdHeaderDrawn = false;
}
void commitHolding(int8_t team){
flushActiveTeam();
activeTeam=(team<teamCount?team:-1);
if(activeTeam!=-1) activeStartMs=nowMs();
firstCaptureDone = true;
lastShownTeam = -2;
holdBeepSec = 0;
holdLastSteps = -1;
noTone(BUZZER_PIN);
safeBeep(2200,120);
// Ura ostane skrita (tudi v SUCCESS)
clockBlocked = true;
clockArmedForUnblock = false;
hideClockTopRight();
// NOVO: resetiraj flag
holdHeaderDrawn = false;
ui=SUCCESS;
writeRtSnap();
}
// ------------------ UI / rezultati ------------------
void printCenteredWithPrefix(uint8_t row, char prefix, const char* text){
char line[21]; for(uint8_t i=0;i<20;i++) line[i]=' '; line[20]=0;
line[0] = prefix;
uint8_t len=0; while(text[len] && len<19) len++;
uint8_t start = 1 + (uint8_t)((19 - len)/2);
for(uint8_t i=0;i<len;i++) line[start+i]=text[i];
lcdPrintFixed(row, line);
}
void buildResultText(uint8_t teamIdx, char* out){
char name[7]; uint8_t p=0;
for(uint8_t i=0; TEAM_NAME[teamIdx][i] && i<6; ++i) name[p++]=TEAM_NAME[teamIdx][i];
name[p]=0;
uint32_t s=teamSeconds[teamIdx];
uint32_t h=s/3600, m=(s%3600)/60, sec=s%60;
char t8[9];
t8[0]='0'+(h/10%10); t8[1]='0'+(h%10);
t8[2]=':'; t8[3]='0'+(m/10); t8[4]='0'+(m%10);
t8[5]=':'; t8[6]='0'+(sec/10); t8[7]='0'+(sec%10); t8[8]=0;
char buf[20]; uint8_t k=0;
for(uint8_t i=0; name[i] && k<19; ++i) buf[k++]=name[i];
if(k<19) buf[k++]=' ';
if(k<19) buf[k++]=' ';
for(uint8_t i=0; t8[i] && k<19; ++i) buf[k++]=t8[i];
buf[k]=0;
uint8_t j=0; for(; buf[j]; ++j) out[j]=buf[j]; out[j]=0;
}
// ------------------ UI zasloni ------------------
void drawGreeting(){
lcd.clear(); resetLineCache();
lcdCentered(0, F("AIRSOFT"));
lcdCentered(1, F("EKIPA"));
lcdCentered(2, F("SVIZCI"));
}
void softClearRuntime(){
for(uint8_t i=0;i<4;i++) teamSeconds[i]=0;
activeTeam=-1; candidateTeam=-1;
lastClosed=false; lastUiTick=0;
inputBlockUntil=0;
firstCaptureDone=false;
lastShownTeam = -2;
refViewActive=false; refHoldStartMs=0;
resetIvlUsed();
}
void goToGreeting(){
softClearRuntime();
drawGreeting(); safeDelay(600);
lcd.clear(); resetLineCache();
ui=SHOW_CLOCK; inputBlockUntil=nowMs()+300; syncButtons();
}
void twoDigits(char* dst, uint8_t v){ dst[0]='0'+(v/10); dst[1]='0'+(v%10); dst[2]=0; }
void printClockScreen(){
DateTime t=nowRTC();
char h2[3],m2[3],s2[3]; twoDigits(h2,t.hour()); twoDigits(m2,t.minute()); twoDigits(s2,t.second());
char line0[21]; uint8_t p=0; const char* prefix="URA: ";
for(uint8_t i=0; prefix[i]; ++i) line0[p++]=prefix[i];
line0[p++]=h2[0]; line0[p++]=h2[1]; line0[p++]=':';
line0[p++]=m2[0]; line0[p++]=m2[1]; line0[p++]=':';
line0[p++]=s2[0]; line0[p++]=s2[1]; line0[p]=0;
lcdCenterRam(0, line0);
lcdCentered(1, F("OK = Nastavi HH:MM"));
lcdCentered(2, F("RUMENI = Preskoci"));
lcdPrintFixed(3, F(" "));
}
void bumpBlink(bool &flag, unsigned long &tRef, uint16_t period){ if(nowMs()-tRef>=period){ tRef=nowMs(); flag=!flag; } }
void drawSetRtcHM(){
static const char title[] PROGMEM = "Nastavi uro (HH:MM)";
char h2[3],m2[3]; twoDigits(h2,rtcEditH); twoDigits(m2,rtcEditM);
char disp[6]={h2[0],h2[1],':',m2[0],m2[1],0};
bumpBlink(rtcBlinkOn, rtcBlinkT, RTC_BLINK_MS);
if(!rtcBlinkOn){
if(rtcDigitIndex==0) disp[0]=' ';
else if(rtcDigitIndex==1) disp[1]=' ';
else if(rtcDigitIndex==2) disp[3]=' ';
else if(rtcDigitIndex==3) disp[4]=' ';
}
lcdCentered(0, (const __FlashStringHelper*)title);
char dispLine[16]="URA: ";
uint8_t q=5; for(uint8_t i=0;i<5;i++) dispLine[q++]=disp[i]; dispLine[q]=0;
lcdCenterRam(1, dispLine);
lcdCentered(2, F("UP/DOWN spremeni"));
lcdCentered(3, F("OK naprej RUM=Levo"));
}
void adjustRtcDigit(int d){
uint8_t Ht=rtcEditH/10, Ho=rtcEditH%10, Mt=rtcEditM/10, Mo=rtcEditM%10;
switch(rtcDigitIndex){
case 0:{ int v=Ht+d; if(v<0)v=2; if(v>2)v=0; Ht=v; if(Ht==2 && Ho>3) Ho=3; }break;
case 1:{ int maxHo=(Ht==2)?3:9; int v=Ho+d; if(v<0)v=maxHo; if(v>maxHo)v=0; Ho=v; }break;
case 2:{ int v=Mt+d; if(v<0)v=5; if(v>5)v=0; Mt=v; }break;
case 3:{ int v=Mo+d; if(v<0)v=9; if(v>9)v=0; Mo=v; }break;
}
rtcEditH=Ht*10+Ho; rtcEditM=Mt*10+Mo;
}
void handleSetRtcHM(){
if(up()) adjustRtcDigit(+1);
if(down()) adjustRtcDigit(-1);
if(ok()){
rtcDigitIndex++;
if(rtcDigitIndex>=4){
DateTime cur=nowRTC();
if(rtcOk) rtc.adjust(DateTime(cur.year(),cur.month(),cur.day(),rtcEditH,rtcEditM,0));
ui=SET_TEAM_COUNT; rtcDigitIndex=0; return;
}
}
if(back()){
if(rtcDigitIndex>0){ rtcDigitIndex--; return; }
ui=SHOW_CLOCK; rtcDigitIndex=0;
}
}
// ------- Število ekip (2-4) -------
void drawSetTeamCount(){
lcdCentered(0, F("Stevilo ekip"));
char line[21]="Izbor: (2-4)";
line[8] = '0' + teamCount;
lcdCenterRam(1, line);
lcdCentered(2, F("UP/DOWN spremeni"));
lcdCentered(3, F("OK naprej RUM=Levo"));
}
void handleSetTeamCount(){
if(up() && teamCount<4) teamCount++;
if(down() && teamCount>2) teamCount--;
if(ok()){ saveSettings(); ui=SET_SHOWTIME; return; } // NOVO
if(back()){ ui=SET_RTC_HM; return; }
}
// ------- NOVO: Prikaz časa držanja (ON/OFF) -------
void drawSetShowTime(){
lcdCentered(0, F("Prikaz casa drzanja"));
lcdCentered(1, showHoldTime ? F("Trenutno: VKLOP") : F("Trenutno: IZKLOP"));
lcdCentered(2, F("UP/DOWN preklopi"));
lcdCentered(3, F("OK naprej RUM=Levo"));
}
void handleSetShowTime(){
if(up() || down()) showHoldTime = !showHoldTime;
if(ok()){ saveSettings(); ui=SET_GAMEEND_Q; return; }
if(back()){ ui=SET_TEAM_COUNT; return; }
}
// ------- Konec igre (čas) -------
uint8_t geEditH=0, geEditM=0, geDigitIndex=0; bool geBlinkOn=true;
unsigned long geBlinkT=0; const uint16_t GE_BLINK_MS=400;
void drawSetGameEnd(){
char h2[3],m2[3]; twoDigits(h2,geEditH); twoDigits(m2,geEditM);
char disp[6]={h2[0],h2[1],':',m2[0],m2[1],0};
bumpBlink(geBlinkOn, geBlinkT, GE_BLINK_MS);
if(!geBlinkOn){
if(geDigitIndex==0) disp[0]=' ';
else if(geDigitIndex==1) disp[1]=' ';
else if(geDigitIndex==2) disp[3]=' ';
else if(geDigitIndex==3) disp[4]=' ';
}
lcdCentered(0, F("KONEC IGRE"));
char dispLine[16]="URA: ";
uint8_t q=5; for(uint8_t i=0;i<5;i++) dispLine[q++]=disp[i]; dispLine[q]=0;
lcdCenterRam(1, dispLine);
lcdCentered(2, F("UP/DOWN spremeni"));
lcdCentered(3, F("OK naprej RUM=Levo"));
}
void adjustGeDigit(int d){
uint8_t Ht=geEditH/10, Ho=geEditH%10, Mt=geEditM/10, Mo=geEditM%10;
switch(geDigitIndex){
case 0:{ int v=Ht+d; if(v<0)v=2; if(v>2)v=0; Ht=v; if(Ht==2 && Ho>3) Ho=3; }break;
case 1:{ int maxHo=(Ht==2)?3:9; int v=Ho+d; if(v<0)v=maxHo; if(v>maxHo)v=0; Ho=v; }break;
case 2:{ int v=Mt+d; if(v<0)v=5; if(v>5)v=0; Mt=v; }break;
case 3:{ int v=Mo+d; if(v<0)v=9; if(v>9)v=0; Mo=v; }break;
}
geEditH=Ht*10+Ho; geEditM=Mt*10+Mo;
}
void handleSetGameEnd(){
static bool initOnce=false;
if(!initOnce){ geEditH=gameEndH; geEditM=gameEndM; geDigitIndex=0; initOnce=true; }
if(up()) adjustGeDigit(+1);
if(down()) adjustGeDigit(-1);
if(ok()){
geDigitIndex++;
if(geDigitIndex>=4){
gameEndH=geEditH; gameEndM=geEditM; saveSettings();
initOnce=false; geDigitIndex=0; ui=ASK_MODE; return;
}
}
if(back()){
if(geDigitIndex>0){ geDigitIndex--; return; }
initOnce=false; geDigitIndex=0; ui=SET_SHOWTIME;
}
}
// ------- Izbor režima -------
void drawAskMode(){
lcdCentered(0, F("Izberi rezim tocke"));
lcdCentered(1, F("OK = Zapiram tocko"));
lcdCentered(2, F("RDECI = Odpiram tocko"));
lcdCentered(3, F("RUMENI = Start brez"));
}
void handleAskMode(){
if(ok()){ modeOpenBase=true; if(ivlCount==0) ivlCount=1; seedIntervalsFromNow(ivlCount); ui=SET_CLOSURE_COUNT; return; }
if(btnPressed(0)){ modeOpenBase=false; if(ivlCount==0) ivlCount=1; seedIntervalsFromNow(ivlCount); ui=SET_CLOSURE_COUNT; return; }
if(back()){ ivlCount=0; ui=READY_START; return; }
}
// ------- Urejanje intervalov -------
bool ieInit=false; uint8_t ieIdx=0, iePhase=0, ieH=0, ieM=0, ieDigit=0; bool ieBlinkOn=true;
unsigned long ieBlinkT=0; const uint16_t IE_BLINK_MS=400;
void ieBlinkTick(){ bumpBlink(ieBlinkOn, ieBlinkT, IE_BLINK_MS); }
void ieAdjustDigit(int d){
uint8_t Ht=ieH/10, Ho=ieH%10, Mt=ieM/10, Mo=ieM%10;
switch(ieDigit){
case 0:{ int v=Ht+d; if(v<0)v=2; if(v>2)v=0; Ht=v; if(Ht==2 && Ho>3) Ho=3; }break;
case 1:{ int maxHo=(Ht==2)?3:9; int v=Ho+d; if(v<0)v=maxHo; if(v>maxHo)v=0; Ho=v; }break;
case 2:{ int v=Mt+d; if(v<0)v=5; if(v>5)v=0; Mt=v; }break;
case 3:{ int v=Mo+d; if(v<0)v=9; if(v>9)v=0; Mo=v; }break;
}
ieH=Ht*10+Ho; ieM=Mt*10+Mo;
}
void ieLoadFromCurrent(){ if(iePhase==0){ ieH=ivl[ieIdx].sh; ieM=ivl[ieIdx].sm; } else { ieH=ivl[ieIdx].eh; ieM=ivl[ieIdx].em; } }
void ieSaveToCurrent(){ if(iePhase==0){ ivl[ieIdx].sh=ieH; ivl[ieIdx].sm=ieM; } else { ivl[ieIdx].eh=ieH; ivl[ieIdx].em=ieM; } }
// --- PATCH (4): čist izris OD/DO ---
void drawEditInterval(){
// blink za trenutno urejanje
char h2[3], m2[3]; twoDigits(h2, ieH); twoDigits(m2, ieM);
char disp[6] = {h2[0], h2[1], ':', m2[0], m2[1], 0};
if(!ieBlinkOn){
if(ieDigit==0) disp[0]=' ';
else if(ieDigit==1) disp[1]=' ';
else if(ieDigit==2) disp[3]=' ';
else if(ieDigit==3) disp[4]=' ';
}
// naslov
char top[21];
for(uint8_t i=0;i<21;i++) top[i]=' ';
if(modeOpenBase){ memcpy_P(top, PSTR("ZAPORA "), 7); }
else { memcpy_P(top, PSTR("ODPRTO "), 7); }
uint8_t pos=7;
uint8_t idx1 = (uint8_t)(ieIdx+1);
if(idx1>=10){ top[pos++]='1'; top[pos++]='0'+(idx1%10); } else { top[pos++]='0'+idx1; }
top[pos++]=' '; top[pos++]='/'; top[pos++]=' ';
if(ivlCount>=10){ top[pos++]='1'; top[pos++]='0'+(ivlCount%10); } else { top[pos++]='0'+ivlCount; }
top[20]=0;
lcdPrintFixed(0, top);
// pripravi sh/sm in eh/em kot stringe
char sh2[3], sm2[3], eh2[3], em2[3];
twoDigits(sh2, ivl[ieIdx].sh);
twoDigits(sm2, ivl[ieIdx].sm);
twoDigits(eh2, ivl[ieIdx].eh);
twoDigits(em2, ivl[ieIdx].em);
// vrstica OD:
char l1[21]; for(uint8_t i=0;i<21;i++) l1[i]=' ';
l1[0]='O'; l1[1]='D'; l1[2]=':'; l1[3]=' ';
if(iePhase==0){
l1[4]=disp[0]; l1[5]=disp[1]; l1[6]=':'; l1[7]=disp[3]; l1[8]=disp[4];
} else {
l1[4]=sh2[0]; l1[5]=sh2[1]; l1[6]=':'; l1[7]=sm2[0]; l1[8]=sm2[1];
}
l1[20]=0;
lcdPrintFixed(1, l1);
// vrstica DO:
char l2[21]; for(uint8_t i=0;i<21;i++) l2[i]=' ';
l2[0]='D'; l2[1]='O'; l2[2]=':'; l2[3]=' ';
if(iePhase==1){
l2[4]=disp[0]; l2[5]=disp[1]; l2[6]=':'; l2[7]=disp[3]; l2[8]=disp[4];
} else {
l2[4]=eh2[0]; l2[5]=eh2[1]; l2[6]=':'; l2[7]=em2[0]; l2[8]=em2[1];
}
l2[20]=0;
lcdPrintFixed(2, l2);
lcdCentered(3, F("UP/DOWN OK->naprej"));
}
void handleEditInterval(){
if(!ieInit){
if(ivlCount==0) ivlCount=1;
seedIntervalsFromNow(ivlCount);
ieIdx=0; iePhase=0; ieDigit=0; ieBlinkOn=true; ieBlinkT=nowMs();
ieLoadFromCurrent(); ieInit=true;
}
ieBlinkTick();
if(up()) ieAdjustDigit(+1);
if(down()) ieAdjustDigit(-1);
if(ok()){
ieDigit++;
if(ieDigit>=4){
ieSaveToCurrent(); ieDigit=0; ieBlinkOn=true;
if(iePhase==0){
iePhase=1; ieLoadFromCurrent();
} else {
iePhase=0;
if(ieIdx+1<ivlCount){ ieIdx++; ieLoadFromCurrent(); }
else {
ieInit=false;
sortIntervals(ivl, ivlCount);
char msg[20];
if(!validateIntervals(ivl,ivlCount,msg)){
lcd.clear(); resetLineCache();
lcdCentered(0, F("Napaka v intervalih"));
lcdCentered(1, F("Prekrivanje/format"));
lcdCentered(2, F("Popravi in potrdi"));
lcdCentered(3, F("OK nadaljuj"));
ui=EDIT_INTERVAL;
return;
}
saveSettings();
reviewIdx = 0;
reviewNeedsClear = true;
ui=REVIEW; inputBlockUntil=nowMs()+200; syncButtons();
return;
}
}
}
}
if(back()){
if(ieDigit>0){ ieDigit--; ieBlinkOn=true; return; }
if(iePhase==1){ iePhase=0; ieLoadFromCurrent(); ieDigit=3; ieBlinkOn=true; return; }
else{
if(ieIdx>0){ ieIdx--; iePhase=1; ieLoadFromCurrent(); ieDigit=3; ieBlinkOn=true; return; }
ieInit=false; ui=SET_CLOSURE_COUNT; return;
}
}
}
// ------- READY/START -------
void drawReadyStart(){
if(ivlCount==0){
lcdCentered(0, F("Brez intervalov"));
lcdCentered(1, F("RDECI = START"));
} else {
lcdCentered(0, modeOpenBase? F("Rezim: Zapiram") : F("Rezim: Odpiram"));
lcdCentered(1, F("OK = START"));
}
lcdCentered(2, F("RUMENI drz 3s = RST"));
lcdPrintFixed(3, F(" "));
}
void hardResetSettings(){
ivlCount=0;
for(uint8_t i=0;i<MAX_INTERVALS;i++) ivl[i]={0,0,0,0};
gameEndH=0; gameEndM=0;
for(uint8_t i=0;i<4;i++) teamSeconds[i]=0;
activeTeam=-1; candidateTeam=-1;
modeOpenBase=true; firstCaptureDone=false; lastShownTeam=-2; teamCount=4;
showHoldTime=true; // reset na privzeto
saveDefaultsToEEPROM();
safeBeep(1800,90);
}
void handleReadyStart(){
// --- Zanesljiv 3s reset z rumenim gumbom ---
static unsigned long yHoldStart = 0;
if (btn[3].stable) {
if (yHoldStart == 0) yHoldStart = nowMs();
if (nowMs() - yHoldStart >= 3000UL) {
hardResetSettings();
goToGreeting();
inputBlockUntil = nowMs() + 800UL;
syncButtons();
yHoldStart = 0;
return;
}
} else {
yHoldStart = 0;
}
// -------------------------------------------
if(ivlCount>0 && ok()){
char msg[20];
sortIntervals(ivl,ivlCount);
if(!validateIntervals(ivl,ivlCount,msg)){
lcdCentered(0, F("Napaka v intervalih"));
lcdCentered(1, F("Popravi nastavitve"));
lcdPrintFixed(2, F(" "));
lcdCentered(3, F("Nazaj"));
return;
}
ui=GAME; for(uint8_t i=0;i<4;i++) teamSeconds[i]=0; activeTeam=-1;
firstCaptureDone=false; lastShownTeam=-2; resetIvlUsed();
inputBlockUntil=nowMs()+600; syncButtons();
DateTime t=nowRTC();
bool openNow=isPointOpenNow(t.hour(),t.minute());
lastClosed=!openNow;
lcd.clear(); resetLineCache();
if(!openNow){
uint8_t oh,om;
if(getOpenHint(t.hour(),t.minute(),oh,om)){
lcdClearRow0Left();
lcdCentered(1, F("*** TOCKA ZAPRTA ***"));
lcdPrintFixed(2, F(" "));
char HH[3],MM[3]; twoDigits(HH,oh); twoDigits(MM,om);
char msg2[21]="Odpre ob 00:00";
msg2[9]=HH[0]; msg2[10]=HH[1]; msg2[12]=MM[0]; msg2[13]=MM[1];
lcdCenterRam(3, msg2);
}
} else {
showOpenStartScreen();
}
// Ura dovoljena
clockBlocked=false; updateClockCacheIfAllowed(); drawClockTopRight();
return;
} else if(ivlCount==0 && btnPressed(0)){
ui=GAME; for(uint8_t i=0;i<4;i++) teamSeconds[i]=0; activeTeam=-1;
firstCaptureDone=false; lastShownTeam=-2; resetIvlUsed();
inputBlockUntil=nowMs()+600; syncButtons();
lcd.clear(); resetLineCache();
showOpenStartScreen();
clockBlocked=false; updateClockCacheIfAllowed(); drawClockTopRight();
return;
}
if(back()){
ui=REVIEW; inputBlockUntil=nowMs()+150; syncButtons();
return;
}
}
// ------- Nastavitev št. intervalov -------
void drawSetClosureCount(){
lcdCentered(0, modeOpenBase? F("Uredi ZAPORE") : F("Uredi ODPIRANJA"));
char line[21]="Stevilo interv.: 00";
if(ivlCount>=10){ line[18]='1'; line[19]='0'+(ivlCount%10); }
else{ line[19]='0'+ivlCount; }
lcdCenterRam(1,line);
lcdCentered(2, F("UP/DOWN spremeni"));
lcdCentered(3, F("OK uredi Nazaj"));
}
void handleSetClosureCount(){
if(up() && ivlCount<MAX_INTERVALS) ivlCount++;
if(down() && ivlCount>1) ivlCount--;
if(ok()){ seedIntervalsFromNow(ivlCount); ieInit=false; ieIdx=0; iePhase=0; ui=EDIT_INTERVAL; }
if(back()){ ui=ASK_MODE; }
}
// ===== Prikazi =====
void showOpenStartScreen(){
lcdClearRow0Left();
lcdCentered(1, F("*** TOCKA ODPRTA ***"));
lcdPrintFixed(2, F(" "));
lcdCentered(3, F("Pridrzite gumb"));
// Ura: odblokiraj le ob prvem GAME po SUCCESS (armed)
if (clockBlocked) {
if (clockArmedForUnblock) {
clockBlocked = false; clockArmedForUnblock = false;
updateClockCacheIfAllowed(); drawClockTopRight();
}
} else {
updateClockCacheIfAllowed(); drawClockTopRight();
}
}
bool anyTeamHasTimeOrActive(){
if(activeTeam!=-1) return true;
for(uint8_t i=0;i<teamCount;i++) if(teamSeconds[i]>0) return true;
return false;
}
void showCurrentHolder(){
if(activeTeam!=-1){
lcdClearRow0Left();
if(lastShownTeam != activeTeam){
char title[21] = "DRZI: ";
uint8_t p=6; for(uint8_t i=0;i<6 && TEAM_NAME[activeTeam][i]; ++i) title[p++]=TEAM_NAME[activeTeam][i];
title[p]=0;
lcdCenterRam(1, title);
lastShownTeam = activeTeam;
}
if(showHoldTime){
uint32_t shown = teamSeconds[activeTeam] + (nowMs()-activeStartMs)/1000UL;
uint32_t h=shown/3600, m=(shown%3600)/60, sec=shown%60;
char t8[9];
t8[0]='0'+(h/10%10); t8[1]='0'+(h%10);
t8[2]=':'; t8[3]='0'+(m/10); t8[4]='0'+(m%10);
t8[5]=':'; t8[6]='0'+(sec/10); t8[7]='0'+(sec%10); t8[8]=0;
char l2[21]="CAS: 00:00:00";
l2[5]=t8[0]; l2[6]=t8[1]; l2[8]=t8[3]; l2[9]=t8[4]; l2[11]=t8[6]; l2[12]=t8[7]; l2[13]=0;
lcdCenterRam(2,l2);
} else {
lcdPrintFixed(2, F(" "));
}
lcdPrintFixed(3, F(" "));
} else {
lastShownTeam = -2;
lcdClearRow0Left();
lcdCentered(1, F("*** TOCKA ODPRTA ***"));
lcdPrintFixed(2, F(" "));
lcdCentered(3, F("Pridrzite gumb"));
}
if (clockBlocked) {
if (clockArmedForUnblock) {
clockBlocked = false; clockArmedForUnblock = false;
updateClockCacheIfAllowed(); drawClockTopRight();
}
} else {
updateClockCacheIfAllowed(); drawClockTopRight();
}
}
// ------- ANIMACIJA PREVZEMA -------
void showHolding(){
initBarChars();
// Glavo "PREVZEM: <TEAM>" narišemo le enkrat
if (!holdHeaderDrawn) {
char head[21]="PREVZEM: ";
uint8_t p=9; for(uint8_t i=0;i<6 && TEAM_NAME[candidateTeam][i]; ++i) head[p++]=TEAM_NAME[candidateTeam][i];
head[p]=0;
lcdCenterRam(0, head);
holdHeaderDrawn = true;
}
const uint32_t HOLD_MS = (uint32_t)HOLD_SECONDS*1000UL;
uint32_t elapsed = nowMs() - holdStartMs;
if(elapsed > HOLD_MS) elapsed = HOLD_MS;
// Piski
uint8_t secDone = elapsed / 1000;
if(secDone > holdBeepSec){
bool lastOne = (secDone >= HOLD_SECONDS);
if(lastOne) safeBeep(2200, 150);
else safeBeep(1800, 80);
holdBeepSec = secDone;
}
// Trak 5 segmentov
const uint8_t BAR_SEGS = 5;
const uint8_t SUB = 5;
const uint16_t TOTAL_STEPS = BAR_SEGS * SUB;
uint16_t steps = (uint32_t)elapsed * TOTAL_STEPS / HOLD_MS;
uint8_t full = steps / SUB;
uint8_t part = steps % SUB;
static int16_t holdLastStepsLocal = -1;
if((int16_t)steps != holdLastStepsLocal){
char line[21]; for(uint8_t i=0;i<20;i++) line[i]=' ';
uint8_t startCol = (20 - BAR_SEGS)/2;
uint8_t col = startCol;
for(uint8_t i=0;i<full && i<BAR_SEGS; ++i) line[col++] = (char)4;
if(col < startCol + BAR_SEGS){
if(part>0) line[col++] = (char)(part-1);
}
while(col < startCol + BAR_SEGS) line[col++] = ' ';
line[20]=0;
lcdPrintFixed(1, line);
holdLastStepsLocal = (int16_t)steps;
}
lcdCentered(2, F("Drzi gumb 5 sekund"));
lcdCentered(3, F("Spusti = preklic"));
// ura ostane skrita (clockBlocked=true)
}
void showClosedScreen(uint8_t openH, uint8_t openM){
lcdClearRow0Left();
lcdCentered(1, F("*** TOCKA ZAPRTA ***"));
lcdPrintFixed(2, F(" "));
char HH[3],MM[3]; twoDigits(HH,openH); twoDigits(MM,openM);
char msg2[21]="Odpre ob 00:00";
msg2[9]=HH[0]; msg2[10]=HH[1]; msg2[12]=MM[0]; msg2[13]=MM[1];
lcdCenterRam(3, msg2);
if (clockBlocked) {
if (clockArmedForUnblock) {
clockBlocked = false; clockArmedForUnblock = false;
updateClockCacheIfAllowed(); drawClockTopRight();
}
} else {
updateClockCacheIfAllowed(); drawClockTopRight();
}
}
void showSuccess(){
char l0[21]="TOCKO PREVZELI ";
uint8_t p=15; for(uint8_t i=0;i<6 && TEAM_NAME[candidateTeam][i]; i++) l0[p++]=TEAM_NAME[candidateTeam][i];
l0[p]=0; lcdCenterRam(0,l0);
char l1[21]="Cas tece: ";
p=10; for(uint8_t i=0;i<6 && TEAM_NAME[candidateTeam][i]; i++) l1[p++]=TEAM_NAME[candidateTeam][i];
l1[p]=0; lcdCenterRam(1,l1);
lcdPrintFixed(2, F(" "));
lcdPrintFixed(3, F(" "));
// ura ostane skrita
clockBlocked = true;
clockArmedForUnblock = false;
hideClockTopRight();
}
void showRefResults(){
for(uint8_t i=0;i<teamCount;i++){
uint32_t shown=teamSeconds[i];
if(activeTeam==i) shown += (nowMs()-activeStartMs)/1000UL;
uint32_t h=shown/3600, m=(shown%3600)/60, sec=shown%60;
char t8[9];
t8[0]='0'+(h/10%10); t8[1]='0'+(h%10);
t8[2]=':'; t8[3]='0'+(m/10); t8[4]='0'+(m%10);
t8[5]=':'; t8[6]='0'+(sec/10); t8[7]='0'+(sec%10); t8[8]=0;
char text[21]; uint8_t p=0;
for(uint8_t k=0; TEAM_NAME[i][k] && p<19; ++k) text[p++]=TEAM_NAME[i][k];
if(p<19) text[p++]=' '; if(p<19) text[p++]=' ';
for(uint8_t k=0; t8[k] && p<19; ++k) text[p++]=t8[k];
text[p]=0;
lcdCenterRam(i, text);
}
for(uint8_t i=teamCount;i<4;i++) lcdPrintFixed(i, F(" "));
}
// ------- PREGLED NASTAVITEV -------
void drawReview(){
if(reviewNeedsClear){ lcd.clear(); resetLineCache(); reviewNeedsClear=false; }
lcdCentered(0, F("PREGLED NASTAVITEV"));
char l1[21]; for(uint8_t i=0;i<21;i++) l1[i]=' '; l1[20]=0;
const char* rz = modeOpenBase ? "Zapiram" : "Odpiram";
const char* a="Rezim:"; const char* sep=" "; const char* b="Ek:";
uint8_t p=0;
for(uint8_t i=0;a[i]&&p<20;i++) l1[p++]=a[i];
for(uint8_t i=0;rz[i]&&p<20;i++) l1[p++]=rz[i];
for(uint8_t i=0;sep[i]&&p<20;i++) l1[p++]=sep[i];
for(uint8_t i=0;b[i]&&p<20;i++) l1[p++]=b[i];
if(p<20) l1[p++]='0'+teamCount;
lcdPrintFixed(1,l1);
char l2[21]; for(uint8_t i=0;i<21;i++) l2[i]=' '; l2[20]=0;
const char* k="Konec igre: ";
p=0; for(uint8_t i=0;k[i]&&p<20;i++) l2[p++]=k[i];
if(gameEndH==0 && gameEndM==0){
const char* br="brez";
for(uint8_t i=0;br[i]&&p<20;i++) l2[p++]=br[i];
}else{
char HH[3],MM[3]; twoDigits(HH,gameEndH); twoDigits(MM,gameEndM);
if(p<20) l2[p++]=HH[0]; if(p<20) l2[p++]=HH[1];
if(p<20) l2[p++]=':'; if(p<20) l2[p++]=MM[0]; if(p<20) l2[p++]=MM[1];
}
lcdPrintFixed(2,l2);
// prikaz stanja showHoldTime
char l3a[21]; for(uint8_t i=0;i<21;i++) l3a[i]=' '; l3a[20]=0;
const char* sht="Prikaz casa: ";
uint8_t p2=0; for(uint8_t i=0;sht[i]&&p2<20;i++) l3a[p2++]=sht[i];
const char* onoff = showHoldTime? "VKLOP" : "IZKLOP";
for(uint8_t i=0;onoff[i]&&p2<20;i++) l3a[p2++]=onoff[i];
lcdPrintFixed(3,l3a);
}
void handleReview(){
if(up() && ivlCount>0){ if(reviewIdx>0) reviewIdx--; drawReview(); }
if(down() && ivlCount>0){ if(reviewIdx+1<ivlCount) reviewIdx++; drawReview(); }
if(ok()){
lcd.clear(); resetLineCache();
ui=READY_START; inputBlockUntil=nowMs()+150; syncButtons();
return;
}
if(back()){
ieInit=false; ui=EDIT_INTERVAL; inputBlockUntil=nowMs()+120; syncButtons(); return;
}
if(btnPressed(2)){
ui=SET_GAMEEND_Q; inputBlockUntil=nowMs()+120; syncButtons(); return;
}
}
// ------- GAME OVER -------
bool screenDrawn=false;
void goDrawPrompt(){
lcd.clear(); resetLineCache();
lcdCentered(0, F("*** IGRA KONCANA ***"));
lcdCentered(1, F("OK = Rezultati"));
lcdCentered(2, F("RUMENI = Restart"));
lcdPrintFixed(3, F(" "));
screenDrawn=true;
}
void goDrawResults(){
lcd.clear(); resetLineCache();
uint32_t best = 0;
for(uint8_t i=0;i<teamCount;i++) if(teamSeconds[i] > best) best = teamSeconds[i];
for(uint8_t i=0;i<teamCount;i++){
char text[20]; buildResultText(i, text);
char prefix = (teamSeconds[i]==best && best>0) ? '*' : ' ';
printCenteredWithPrefix(i, prefix, text);
}
for(uint8_t i=teamCount;i<4;i++) lcdPrintFixed(i, F(" "));
screenDrawn=true;
}
// ------------------ SETUP ------------------
void setup(){
Wire.begin();
// bolj konzervativen I2C in timeout
Wire.setClock(100000);
#if defined(TWOWIRE_HAS_TIMEOUT) || defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(25000 /*us*/, true);
#endif
for(uint8_t i=0;i<4;i++) btnInit(i, BTN_PINS[i]);
pinMode(BUZZER_PIN, OUTPUT); noTone(BUZZER_PIN);
lcd.init(); lcd.backlight(); lcd.clear(); lcd.noCursor(); lcd.noBlink();
resetLineCache();
initBarChars();
// Pozdrav
lcdCentered(0, F("AIRSOFT"));
lcdCentered(1, F("EKIPA"));
lcdCentered(2, F("SVIZCI"));
safeDelay(600);
lcd.clear(); resetLineCache();
rtcOk = rtc.begin();
if (rtcOk && !rtc.isrunning()){
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
} else if (!rtcOk){
lcdPrintFixed(3, F("RTC ni zaznan! "));
safeDelay(500);
lcdPrintFixed(3, F(" "));
}
if(!loadSettings()){
ivlCount=0; for(uint8_t i=0;i<MAX_INTERVALS;i++) ivl[i]={0,0,0,0};
gameEndH=0; gameEndM=0; modeOpenBase=true; teamCount=4; showHoldTime=true;
saveDefaultsToEEPROM();
}
RtSnap r;
if(readRtSnap(r)){
for(uint8_t i=0;i<4;i++) teamSeconds[i]=r.secs[i];
activeTeam=r.active;
if(activeTeam>=0 && activeTeam<teamCount) activeStartMs=nowMs();
else activeTeam=-1;
}
resetIvlUsed();
#if USE_WDT
wdt_enable(WDTO_2S);
#endif
#if USE_WDT
if (reset_cause & _BV(WDRF)) {
lcdCentered(3, F("WDT reset"));
safeDelay(400);
lcdPrintFixed(3, F(" "));
}
#endif
ui=SHOW_CLOCK;
}
// ------------------ LOOP ------------------
void loop(){
#if USE_WDT
wdt_reset();
#endif
for(uint8_t i=0;i<4;i++) btnUpdate(i);
if(ui==SHOW_CLOCK){
if(nowMs()-lastUiTick>=250){ lastUiTick=nowMs(); printClockScreen(); }
if(ok()){ DateTime c=nowRTC(); rtcEditH=c.hour(); rtcEditM=c.minute(); rtcDigitIndex=0; ui=SET_RTC_HM; }
if(timePassed(inputBlockUntil) && back()){ ui=SET_TEAM_COUNT; }
return;
}
if(ui==SET_RTC_HM){ if(nowMs()-lastUiTick>=100){ lastUiTick=nowMs(); drawSetRtcHM(); } handleSetRtcHM(); return; }
if(ui==SET_TEAM_COUNT){ if(nowMs()-lastUiTick>=120){ lastUiTick=nowMs(); drawSetTeamCount(); } handleSetTeamCount(); return; }
if(ui==SET_SHOWTIME){ if(nowMs()-lastUiTick>=150){ lastUiTick=nowMs(); drawSetShowTime(); } handleSetShowTime(); return; }
if(ui==SET_GAMEEND_Q){ if(nowMs()-lastUiTick>=100){ lastUiTick=nowMs(); drawSetGameEnd(); } handleSetGameEnd(); return; }
if(ui==ASK_MODE){ if(nowMs()-lastUiTick>=150){ lastUiTick=nowMs(); drawAskMode(); } handleAskMode(); return; }
if(ui==SET_CLOSURE_COUNT){ if(nowMs()-lastUiTick>=150){ lastUiTick=nowMs(); drawSetClosureCount(); } handleSetClosureCount(); return; }
if(ui==EDIT_INTERVAL){ if(nowMs()-lastUiTick>=100){ lastUiTick=nowMs(); drawEditInterval(); } handleEditInterval(); return; }
if(ui==REVIEW){ if(nowMs()-lastUiTick>=200){ lastUiTick=nowMs(); drawReview(); } handleReview(); return; }
if(ui==READY_START){ if(nowMs()-lastUiTick>=200){ lastUiTick=nowMs(); drawReadyStart(); } handleReadyStart(); return; }
// -------- IGRA --------
DateTime t=nowRTC(); uint8_t hh=t.hour(), mm=t.minute();
int8_t curIdx=containingIvlIdx(hh,mm);
if(curIdx!=lastIvlIdx){
if(lastIvlIdx!=-1 && !ivlUsed[lastIvlIdx]) ivlUsed[lastIvlIdx]=true;
lastIvlIdx=curIdx;
}
bool openNow=isPointOpenNow(hh,mm);
bool ended = pastGameEnd(hh,mm);
if(!openNow && !lastClosed){
flushActiveTeam();
if(ui==HOLDING) cancelHolding();
activeTeam = -1;
lastClosed=true;
lcd.clear(); resetLineCache();
uint8_t oh,om; if(getOpenHint(hh,mm,oh,om)) showClosedScreen(oh,om);
safeBeep(1200,110);
} else if(openNow && lastClosed){
lastClosed=false;
activeTeam=-1;
lcd.clear(); resetLineCache();
if(!firstCaptureDone && !anyTeamHasTimeOrActive()){
showOpenStartScreen();
}
safeBeep(1800,110);
}
if(ended && ui!=GAME_OVER){
flushActiveTeam(); ui=GAME_OVER; goSub=GO_PROMPT; screenDrawn=false;
inputBlockUntil=nowMs()+200; syncButtons();
}
if(ui==GAME_OVER){
if(goSub==GO_PROMPT){
if(!screenDrawn) goDrawPrompt();
if(timePassed(inputBlockUntil)){
if(ok()){ goSub=GO_RESULTS; screenDrawn=false; inputBlockUntil=nowMs()+120; syncButtons(); }
if(back()){ goToGreeting(); }
}
return;
} else {
if(!screenDrawn) goDrawResults();
if(back()){ goSub=GO_PROMPT; screenDrawn=false; inputBlockUntil=nowMs()+120; syncButtons(); }
return;
}
}
if(ended) return;
bool bothRefBtns = btn[0].stable && btn[3].stable;
if (bothRefBtns){
if(refHoldStartMs==0) refHoldStartMs = nowMs();
if(!refViewActive && nowMs()-refHoldStartMs >= 2000UL){
refViewActive=true;
lcd.clear(); resetLineCache();
}
} else {
refHoldStartMs=0;
if(refViewActive){
refViewActive=false;
lcd.clear(); resetLineCache();
lastShownTeam=-2;
inputBlockUntil = nowMs()+120;
}
}
if(refViewActive){
if(nowMs()-lastUiTick>=200){
lastUiTick=nowMs();
showRefResults();
}
return;
}
if(!openNow){
if(nowMs()-lastUiTick>=800){
lastUiTick=nowMs();
uint8_t oh,om;
if(getOpenHint(hh,mm,oh,om)) showClosedScreen(oh,om);
else{
lcdClearRow0Left();
lcdCentered(1, F("*** TOCKA ZAPRTA ***"));
lcdPrintFixed(2, F(" "));
lcdCentered(3, F("Ni vec odpiranj"));
if (clockBlocked) {
if (clockArmedForUnblock) {
clockBlocked=false; clockArmedForUnblock=false;
updateClockCacheIfAllowed(); drawClockTopRight();
}
} else {
updateClockCacheIfAllowed(); drawClockTopRight();
}
}
}
return;
}
if(ui==GAME){
if(!firstCaptureDone && !anyTeamHasTimeOrActive()){
if(nowMs()-lastUiTick>=400){ lastUiTick=nowMs(); showOpenStartScreen(); }
} else {
if(nowMs()-lastUiTick>=200){ lastUiTick=nowMs(); showCurrentHolder(); }
}
}
int8_t pressedEdge=-1;
if(timePassed(inputBlockUntil))
for(uint8_t i=0;i<teamCount;i++) if(btnPressed(i)){ pressedEdge=i; break; }
if(ui==GAME){
if(pressedEdge!=-1 && pressedEdge!=activeTeam) startHolding(pressedEdge);
} else if(ui==HOLDING){
for(uint8_t i=0;i<teamCount;i++) if(i!=candidateTeam) (void)btnPressed(i);
bool stillHolding = (candidateTeam>=0 && candidateTeam<teamCount && btn[candidateTeam].stable);
if(!stillHolding){
cancelHolding();
lcd.clear(); resetLineCache();
lastUiTick = 0;
return;
}
if(nowMs()-holdStartMs >= (unsigned long)HOLD_SECONDS*1000UL){
if(candidateTeam>=0 && candidateTeam<teamCount) commitHolding(candidateTeam);
else cancelHolding();
}
if(nowMs()-lastUiTick>=60){
lastUiTick=nowMs();
showHolding();
}
}
if (ui==SUCCESS && nowMs()-lastUiTick>=250) {
lastUiTick = nowMs();
if(activeTeam>=teamCount) activeTeam=-1;
ui = GAME;
// NE odblokiraj tukaj: le armi za prvi GAME-izris
clockBlocked = true;
clockArmedForUnblock = true;
}
if ((long)(nowMs() - lastSnapAt) >= 60000L) {
lastSnapAt = nowMs();
writeRtSnap();
}
// Periodična ura (samo če ni blokirana)
if(!clockBlocked){
if(nowMs()-lastClockDraw >= 200){
lastClockDraw = nowMs();
updateClockCacheIfAllowed();
drawClockTopRight();
}
}
}