|
| 1 | +#include <Arduino.h> |
| 2 | +#include "settings.h" |
| 3 | + |
| 4 | +#include "Playstats.h" |
| 5 | + |
| 6 | +#include "Log.h" |
| 7 | +#include "System.h" |
| 8 | + |
| 9 | +#include <string.h> |
| 10 | +#include <time.h> |
| 11 | + |
| 12 | +// 365-day ring buffer of playback seconds per local calendar day. Indexed by (dayNumber % DAYS), |
| 13 | +// where dayNumber is the count of local days since the epoch. lastDay tracks the most recent day |
| 14 | +// written so the gap since then can be zeroed when the day rolls over (or the device was off). |
| 15 | +static constexpr uint16_t PLAYSTATS_DAYS = 365; |
| 16 | +static constexpr uint32_t PLAYSTATS_MAGIC = 0x504C5331; // "PLS1" |
| 17 | +static constexpr uint32_t PLAYSTATS_MIN_VALID_DAY = 18262; // ~2020-01-01, guards against pre-NTP clock |
| 18 | + |
| 19 | +static uint32_t gDays[PLAYSTATS_DAYS] = {0}; |
| 20 | +static uint32_t gLastDay = 0; // local day number of the most recent tracked day (0 = none yet) |
| 21 | +static bool gDirty = false; |
| 22 | +static uint32_t gLastSaveMs = 0; |
| 23 | + |
| 24 | +// Days since 1970-01-01 for a (proleptic Gregorian) calendar date (Howard Hinnant's algorithm). |
| 25 | +// Avoids needing tm_gmtoff (not available in this newlib build): we feed it the local Y/M/D. |
| 26 | +static long Playstats_DaysFromCivil(int y, unsigned m, unsigned d) { |
| 27 | + y -= m <= 2; |
| 28 | + long era = (y >= 0 ? y : y - 399) / 400; |
| 29 | + unsigned yoe = (unsigned) (y - era * 400); |
| 30 | + unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; |
| 31 | + unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; |
| 32 | + return era * 146097 + (long) doe - 719468; |
| 33 | +} |
| 34 | + |
| 35 | +// Local calendar day number (days since epoch in local time). 0 if the clock is not valid yet. |
| 36 | +static uint32_t Playstats_CurrentDay(void) { |
| 37 | + time_t now = time(nullptr); |
| 38 | + struct tm timeinfo; |
| 39 | + if (!localtime_r(&now, &timeinfo)) { |
| 40 | + return 0; |
| 41 | + } |
| 42 | + long day = Playstats_DaysFromCivil(timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday); |
| 43 | + if (day < (long) PLAYSTATS_MIN_VALID_DAY) { |
| 44 | + return 0; |
| 45 | + } |
| 46 | + return (uint32_t) day; |
| 47 | +} |
| 48 | + |
| 49 | +// Roll the ring buffer forward to <today>, zeroing days that elapsed since gLastDay (so stale |
| 50 | +// slots from ~a year ago don't leak into the windows). Resets everything if the gap exceeds a year. |
| 51 | +static void Playstats_AdvanceTo(uint32_t today) { |
| 52 | + if (today == 0 || today == gLastDay) { |
| 53 | + return; |
| 54 | + } |
| 55 | + if (gLastDay == 0 || (today - gLastDay) >= PLAYSTATS_DAYS) { |
| 56 | + for (uint16_t i = 0; i < PLAYSTATS_DAYS; i++) { |
| 57 | + gDays[i] = 0; |
| 58 | + } |
| 59 | + } else { |
| 60 | + for (uint32_t d = gLastDay + 1; d <= today; d++) { |
| 61 | + gDays[d % PLAYSTATS_DAYS] = 0; |
| 62 | + } |
| 63 | + } |
| 64 | + gLastDay = today; |
| 65 | + gDirty = true; |
| 66 | +} |
| 67 | + |
| 68 | +void Playstats_Init(void) { |
| 69 | + size_t got = gPrefsSettings.getBytesLength("playStats"); |
| 70 | + if (got == sizeof(uint32_t) /*magic*/ + sizeof(uint32_t) /*lastDay*/ + sizeof(gDays)) { |
| 71 | + uint8_t buf[sizeof(uint32_t) * 2 + sizeof(gDays)]; |
| 72 | + gPrefsSettings.getBytes("playStats", buf, sizeof(buf)); |
| 73 | + uint32_t magic; |
| 74 | + memcpy(&magic, buf, sizeof(magic)); |
| 75 | + if (magic == PLAYSTATS_MAGIC) { |
| 76 | + memcpy(&gLastDay, buf + sizeof(uint32_t), sizeof(gLastDay)); |
| 77 | + memcpy(gDays, buf + sizeof(uint32_t) * 2, sizeof(gDays)); |
| 78 | + } |
| 79 | + } |
| 80 | + gLastSaveMs = millis(); |
| 81 | +} |
| 82 | + |
| 83 | +void Playstats_AddSecond(void) { |
| 84 | + uint32_t today = Playstats_CurrentDay(); |
| 85 | + if (today == 0) { |
| 86 | + return; // clock not valid yet |
| 87 | + } |
| 88 | + Playstats_AdvanceTo(today); |
| 89 | + gDays[today % PLAYSTATS_DAYS]++; |
| 90 | + gDirty = true; |
| 91 | + |
| 92 | + // Persist at most once per minute to spare the NVS flash; a final flush happens on shutdown. |
| 93 | + if (millis() - gLastSaveMs >= 60000u) { |
| 94 | + Playstats_Save(); |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +void Playstats_Save(void) { |
| 99 | + if (!gDirty) { |
| 100 | + return; |
| 101 | + } |
| 102 | + uint8_t buf[sizeof(uint32_t) * 2 + sizeof(gDays)]; |
| 103 | + uint32_t magic = PLAYSTATS_MAGIC; |
| 104 | + memcpy(buf, &magic, sizeof(magic)); |
| 105 | + memcpy(buf + sizeof(uint32_t), &gLastDay, sizeof(gLastDay)); |
| 106 | + memcpy(buf + sizeof(uint32_t) * 2, gDays, sizeof(gDays)); |
| 107 | + gPrefsSettings.putBytes("playStats", buf, sizeof(buf)); |
| 108 | + gDirty = false; |
| 109 | + gLastSaveMs = millis(); |
| 110 | +} |
| 111 | + |
| 112 | +uint32_t Playstats_GetToday(void) { |
| 113 | + uint32_t today = Playstats_CurrentDay(); |
| 114 | + if (today == 0) { |
| 115 | + return 0; |
| 116 | + } |
| 117 | + Playstats_AdvanceTo(today); |
| 118 | + return gDays[today % PLAYSTATS_DAYS]; |
| 119 | +} |
| 120 | + |
| 121 | +uint32_t Playstats_GetYesterday(void) { |
| 122 | + uint32_t today = Playstats_CurrentDay(); |
| 123 | + if (today == 0 || gLastDay == 0) { |
| 124 | + return 0; |
| 125 | + } |
| 126 | + Playstats_AdvanceTo(today); |
| 127 | + return (today >= 1) ? gDays[(today - 1) % PLAYSTATS_DAYS] : 0; |
| 128 | +} |
| 129 | + |
| 130 | +uint32_t Playstats_GetLastDays(uint16_t days) { |
| 131 | + uint32_t today = Playstats_CurrentDay(); |
| 132 | + if (today == 0) { |
| 133 | + return 0; |
| 134 | + } |
| 135 | + Playstats_AdvanceTo(today); |
| 136 | + if (days > PLAYSTATS_DAYS) { |
| 137 | + days = PLAYSTATS_DAYS; |
| 138 | + } |
| 139 | + uint32_t sum = 0; |
| 140 | + for (uint16_t i = 0; i < days && i <= today; i++) { |
| 141 | + sum += gDays[(today - i) % PLAYSTATS_DAYS]; |
| 142 | + } |
| 143 | + return sum; |
| 144 | +} |
0 commit comments