Repository navigation
Expand file tree
/
Copy pathSofaScoreLiveModule.hpp
More file actions
258 lines (225 loc) · 10.4 KB
/
Copy pathSofaScoreLiveModule.hpp
File metadata and controls
258 lines (225 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#ifndef SOFASCORE_LIVE_MODULE_HPP
#define SOFASCORE_LIVE_MODULE_HPP
#include <U8g2_for_Adafruit_GFX.h>
#include <vector>
#include <functional>
#include "PsramUtils.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "DrawableModule.hpp"
#include "PixelScroller.hpp"
#include "GeneralTimeConverter.hpp"
class WebClientModule;
struct DeviceConfig;
// UID für SofaScore Live-Match Interrupts
#define SOFASCORE_INTERRUPT_UID_BASE 5000
// Debug feature: Save JSON data to LittleFS for analysis (default: enabled)
#ifndef SOFASCORE_DEBUG_JSON
#define SOFASCORE_DEBUG_JSON 1
#endif
// Tournament information
struct SofaScoreTournament {
int id = 0;
char* name = nullptr;
char* slug = nullptr;
bool isEnabled = false;
SofaScoreTournament();
SofaScoreTournament(const SofaScoreTournament& other);
SofaScoreTournament& operator=(const SofaScoreTournament& other);
SofaScoreTournament(SofaScoreTournament&& other) noexcept;
SofaScoreTournament& operator=(SofaScoreTournament&& other) noexcept;
~SofaScoreTournament();
};
// Match/Event status
enum class MatchStatus {
SCHEDULED, // Not started yet
LIVE, // Currently playing
FINISHED // Completed
};
// Daily match result or live match
struct SofaScoreMatch {
int eventId = 0;
char* homePlayerName = nullptr;
char* awayPlayerName = nullptr;
char* homeCountry = nullptr; // Country name for home player
char* awayCountry = nullptr; // Country name for away player
int homeScore = 0; // Sets won
int awayScore = 0; // Sets won
int homeLegs = 0; // Legs in current set
int awayLegs = 0; // Legs in current set
char* tournamentName = nullptr;
MatchStatus status = MatchStatus::SCHEDULED;
long startTimestamp = 0;
// Statistics (for live matches)
float homeAverage = 0.0f;
float awayAverage = 0.0f;
int home180s = 0;
int away180s = 0;
int homeOver140 = 0;
int awayOver140 = 0;
int homeOver100 = 0;
int awayOver100 = 0;
int homeCheckoutsOver100 = 0;
int awayCheckoutsOver100 = 0;
int homeHighestCheckout = 0; // Highest checkout value
int awayHighestCheckout = 0; // Highest checkout value
float homeCheckoutPercent = 0.0f;
float awayCheckoutPercent = 0.0f;
int homeCheckoutHits = 0; // Checkouts made (numerator in "2/3") - DEPRECATED, use percent only now
int homeCheckoutAttempts = 0; // Total checkout attempts (denominator in "2/3") - DEPRECATED
int awayCheckoutHits = 0; // DEPRECATED
int awayCheckoutAttempts = 0; // DEPRECATED
SofaScoreMatch();
SofaScoreMatch(const SofaScoreMatch& other);
SofaScoreMatch& operator=(const SofaScoreMatch& other);
SofaScoreMatch(SofaScoreMatch&& other) noexcept;
SofaScoreMatch& operator=(SofaScoreMatch&& other) noexcept;
~SofaScoreMatch();
};
// Display mode for the module
enum class SofaScoreDisplayMode {
TOURNAMENT_LIST, // Show available tournaments
DAILY_RESULTS, // Show today's results
LIVE_MATCH // Show live match with stats
};
class SofaScoreLiveModule : public DrawableModule {
public:
SofaScoreLiveModule(U8G2_FOR_ADAFRUIT_GFX& u8g2_ref, GFXcanvas16& canvas_ref,
const GeneralTimeConverter& timeConverter_ref,
WebClientModule* webClient_ptr, DeviceConfig* config);
~SofaScoreLiveModule();
void onUpdate(std::function<void()> callback);
void setConfig(bool enabled, uint32_t fetchIntervalMinutes, unsigned long displaySec,
const PsramString& enabledTournamentSlugs, bool fullscreen, bool interruptOnLive,
uint32_t playNextMinutes, bool continuousLive,
uint32_t liveCheckIntervalSec = 120, uint32_t liveDataFetchIntervalSec = 60,
bool excludeMode = false);
void queueData();
void processData();
// DrawableModule Interface
const char* getModuleName() const override { return "SofaScoreLiveModule"; }
const char* getModuleDisplayName() const override { return "Darts Live (SofaScore)"; }
void draw() override;
void tick() override;
void logicTick() override;
void periodicTick() override;
unsigned long getDisplayDuration() override;
bool isEnabled() override;
void resetPaging() override;
int getCurrentPage() const override { return _currentPage; }
int getTotalPages() const override { return _totalPages; }
// Fullscreen support
bool supportsFullscreen() const override { return true; }
bool wantsFullscreen() const override { return _wantsFullscreen && _fullscreenCanvas != nullptr; }
#if SOFASCORE_DEBUG_JSON
// Debug helpers to save current state on demand (via web interface)
void debugSaveCurrentState();
#endif
private:
U8G2_FOR_ADAFRUIT_GFX& u8g2;
GFXcanvas16& canvas;
// Note: _fullscreenCanvas is inherited from DrawableModule base class
GFXcanvas16* _currentCanvas = nullptr; // Points to either &canvas or _fullscreenCanvas
const GeneralTimeConverter& timeConverter;
WebClientModule* webClient;
DeviceConfig* config;
std::function<void()> updateCallback;
SemaphoreHandle_t dataMutex;
// Configuration
bool _enabled = false;
bool _wantsFullscreen = false;
bool _interruptOnLive = true;
uint32_t _playNextMinutes = 0; // 0 = disabled
unsigned long _displayDuration = 20000; // 20 seconds per page
uint32_t _currentTicksPerPage = 200; // 200 * 100ms = 20 seconds
bool _tournamentExcludeMode = false; // When true, selected tournaments are EXCLUDED instead of INCLUDED
// Live event monitoring
bool _hasLiveEvents = false;
bool _dailySchedulesPaused = false;
bool _continuousLiveDisplay = false; // Show live match continuously while active
unsigned long _lastLiveCheckTime = 0;
unsigned long _lastLiveDataFetchTime = 0;
unsigned long _lastLiveDisplayTime = 0; // For repeating live display every minute
unsigned long _liveCheckIntervalMs = 120000; // Check for live events (configurable, default 120s)
unsigned long _liveDataFetchIntervalMs = 60000; // Fetch live data when active (configurable, default 60s)
const unsigned long LIVE_DISPLAY_REPEAT_MS = 60000; // Repeat live display every 60 seconds
const unsigned long LIVE_MIN_DISPLAY_MS = 20000; // Minimum 20 seconds display for live stats
bool _liveEventsRegistered = false; // Track if live events endpoint is registered to prevent spam
// Paging
int _currentPage = 0;
int _totalPages = 1;
int _currentTournamentIndex = 0; // Which tournament we're currently showing
int _currentTournamentPage = 0; // Which page within the current tournament
uint32_t _logicTicksSincePageSwitch = 0;
uint32_t _logicTicksSinceModeSwitch = 0;
SofaScoreDisplayMode _currentMode = SofaScoreDisplayMode::DAILY_RESULTS;
bool _isFinished = false;
// Interrupt management
bool _hasActiveInterrupt = false;
uint32_t _interruptUID = 0;
unsigned long _lastInterruptCheckTime = 0;
std::vector<int, PsramAllocator<int>> _previousLiveEventIds; // Track previous live matches
// PlayNext management
bool _hasActivePlayNext = false;
uint32_t _playNextUID = 0;
unsigned long _lastPlayNextTime = 0;
// Scrolling
PixelScroller* _nameScroller = nullptr;
std::vector<PixelScroller*, PsramAllocator<PixelScroller*>> _matchScrollers; // One scroller per match line
PixelScroller* _tournamentScroller = nullptr;
// Data buffers for async fetching
char* tournaments_pending_buffer = nullptr;
size_t tournaments_buffer_size = 0;
volatile bool tournaments_data_pending = false;
time_t tournaments_last_processed_update = 0;
char* daily_pending_buffer = nullptr;
size_t daily_buffer_size = 0;
volatile bool daily_data_pending = false;
time_t daily_last_processed_update = 0;
char* live_pending_buffer = nullptr;
size_t live_buffer_size = 0;
volatile bool live_data_pending = false;
time_t live_last_processed_update = 0;
// Parsed data
std::vector<SofaScoreTournament, PsramAllocator<SofaScoreTournament>> availableTournaments;
std::vector<PsramString, PsramAllocator<PsramString>> enabledTournamentSlugs;
std::vector<SofaScoreMatch, PsramAllocator<SofaScoreMatch>> dailyMatches;
std::vector<SofaScoreMatch, PsramAllocator<SofaScoreMatch>> liveMatches;
// Tournament grouping for multi-page display
struct TournamentGroup {
int tournamentId = 0;
PsramString tournamentName;
std::vector<int, PsramAllocator<int>> matchIndices; // Indices into dailyMatches
int pagesNeeded = 0; // Number of pages for this tournament
int matchesPerPage = 0; // Balanced matches per page for this tournament
TournamentGroup() : matchIndices(PsramAllocator<int>()) {}
};
std::vector<TournamentGroup, PsramAllocator<TournamentGroup>> _tournamentGroups;
// Track registered resources to avoid duplicate registrations
PsramString _lastRegisteredDailyUrl;
std::vector<int, PsramAllocator<int>> _registeredEventIds;
// Helper methods
void clearAllData();
void parseTournamentsJson(const char* json, size_t len);
void parseDailyEventsJson(const char* json, size_t len);
void parseLiveEventsJson(const char* json, size_t len); // NEW: Parse live events endpoint
void parseMatchStatistics(int eventId, const char* json, size_t len);
void updateLiveMatchStats();
void checkAndFetchLiveEvents(); // NEW: Check for live events every minute
void fetchLiveData(); // NEW: Fetch live events + statistics
void switchToNextMode();
void checkForLiveMatchInterrupt();
void checkForPlayNext();
void groupMatchesByTournament(); // New: group and calculate pages
int calculateTotalPages(); // New: calculate total pages across all tournaments
bool areAllLiveMatchesFinished() const; // NEW: Check if all live matches are finished
int getMatchesPerPage() const; // Helper: Get matches per page based on fullscreen state
int getBalancedMatchesPerPage(int totalMatches) const; // Helper: Get balanced matches per page for a given total
// Drawing helpers
void drawTournamentList();
void drawDailyResults();
void drawLiveMatch();
void ensureScrollSlots(size_t requiredSize);
uint16_t dimColor(uint16_t color);
};
#endif // SOFASCORE_LIVE_MODULE_HPP