-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHistoryCache.cpp
More file actions
169 lines (148 loc) · 7.16 KB
/
Copy pathHistoryCache.cpp
File metadata and controls
169 lines (148 loc) · 7.16 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
#include "HistoryCache.h"
#include <ctime>
#include <deque>
#include <functional>
#include <map>
#include <utility>
#include "Config.h"
#include "DeviceStore.h"
#include "HistoryLogger.h"
#include "SensorHub.h"
#include "TimeSync.h"
namespace {
// 機器ごとのリングバッファも、それを保持する連想配列も、どちらもPSRAM上に
// 置きます(理由はHistoryCache.hのPsramAllocatorを参照)。
using SampleDeque = std::deque<HistorySample, PsramAllocator<HistorySample>>;
using BufferMap = std::map<String, SampleDeque, std::less<String>,
PsramAllocator<std::pair<const String, SampleDeque>>>;
BufferMap g_buffers;
size_t g_capacity = 0;
time_t g_lastCommittedMinute = 0;
// その項目を、この1分の間に受け取ったか。
//
// 0は「起動してから一度も受け取っていない」ことを表すので、別に見ます。
// 起動直後はmillis()自体が小さく、引き算だけでは新しいものと区別できません。
bool receivedRecently(uint32_t stampMillis, uint32_t nowMillis) {
return stampMillis != 0 && (nowMillis - stampMillis) < HISTORY_FRESH_WINDOW_MS;
}
void commitOneDevice(const String &mac, time_t nowEpoch) {
auto &buf = g_buffers[mac]; // 初回アクセス時に空のバッファが自動で作られます
HistorySample sample;
sample.timestamp = uint32_t(nowEpoch);
// 項目ごとに、この1分の間に受け取ったものだけを書きます。受け取っていない
// 項目は「データ無し」のままにします。
//
// 機器単位ではなく項目単位で見るのは、1回の受信で全ての項目が届くわけでは
// ないからです。まとめて判定すると、短い発信が届いているというだけで、
// 何時間も前のCO2を「今の値」として書き続けることになります。
//
// 記録には日時が入っているので、書かない分があっても後で困りません。
// グラフは日時を見て位置を決めており、順番では数えていないためです。
SensorReading reading;
if (SensorHub::getReading(mac, reading)) {
uint32_t nowMillis = millis();
if (receivedRecently(reading.tempHumMillis, nowMillis)) {
sample.tempX10 = int16_t(round(reading.temperature * 10.0f));
// 記録の形式は0.1%刻みなので、10倍して整数にします。
sample.humidityX10 = uint16_t(lroundf(reading.humidity * 10.0f));
}
if (reading.hasBattery && receivedRecently(reading.batteryMillis, nowMillis)) {
sample.battery = reading.battery;
}
if (reading.hasCo2 && receivedRecently(reading.co2Millis, nowMillis)) {
// CO2の枠には、空気質の学習状態も一緒に入れます(HistoryCache.hを参照)。
// 学習状態を持たない機器(SwitchBotのCO2計)では0になります。
historySetCo2(sample, reading.co2, reading.hasIaq ? reading.iaqAccuracy : 0);
}
if (!isnan(reading.pressureHpa) && receivedRecently(reading.pressureMillis, nowMillis)) {
// 記録の形式は0.1hPa刻みです。1013.2hPaなら10132になります。
sample.pressureX10 = uint16_t(lroundf(reading.pressureHpa * 10.0f));
}
if (!isnan(reading.vocPpm) && receivedRecently(reading.vocMillis, nowMillis)) {
// 記録の形式は0.01ppm刻みです。0xFFFFはデータ無しの印なので使えません。
long v = lroundf(reading.vocPpm * 100.0f);
if (v > 0xFFFE) v = 0xFFFE;
if (v < 0) v = 0;
sample.vocX100 = uint16_t(v);
}
if (reading.hasIaq && receivedRecently(reading.iaqMillis, nowMillis)) {
// 記録の形式はIAQの半分の値です(HistoryCache.hのiaqHalfを参照)。
// 500を超える値は来ませんが、0xFFはデータ無しの印なので使えません。
uint16_t half = uint16_t((reading.iaq + 1) / 2);
if (half > 0xFE) half = 0xFE;
sample.iaqHalf = uint8_t(half);
}
if (reading.light >= 0 && receivedRecently(reading.lightMillis, nowMillis)) {
// 明るさの指標をそのまま記録します。ルクスへの換算はしていません。
// 記録できるのは65534までなので、それを超える分は頭打ちにします
// (0xFFFFはデータ無しの印なので使えません)。
long v = reading.light;
if (v > 0xFFFE) v = 0xFFFE;
sample.illuminance = uint16_t(v);
}
}
buf.push_back(sample);
while (buf.size() > g_capacity) buf.pop_front();
HistoryLogger::queueSample(mac, sample);
}
// 登録から外れた機器のバッファを解放します。
//
// バッファは機器ごとに必要になった時点で作られるので、削除した機器の分を
// 片付けないと、1台あたり約15KBの領域が使われたまま残り続けます。
// 登録数はMAX_REGISTERED_DEVICES以下なので、毎分の確認は軽い処理です。
void pruneUnregisteredBuffers() {
for (auto it = g_buffers.begin(); it != g_buffers.end();) {
if (DeviceStore::isRegistered(it->first)) {
++it;
} else {
it = g_buffers.erase(it);
}
}
}
} // namespace
namespace HistoryCache {
void begin() { g_capacity = size_t(HISTORY_RAM_CAPACITY_HOURS) * 60; }
void update() {
// 時計が合っていない間は記録しません。
//
// 電源投入直後の時計は0ではなく、実機では2045年を指していました。そのまま
// 記録すると、19年ほど未来の日時を持つデータがファイルの先頭に残ります。
// これはグラフの期間指定から外れて表示されないうえ、ファイル内の日時が
// 古い順に並んでいるという前提も崩します。
//
// 記録が始まるのは時刻合わせが済んでからで、それまでの数十秒は履歴が
// 空きます。誤った日時の記録より、記録が無いほうがましです。
if (!TimeSync::isClockPlausible()) return;
time_t nowEpoch = time(nullptr);
time_t nowMinute = nowEpoch / 60;
// 分が変わっていなければ何もしません。
if (nowMinute == g_lastCommittedMinute) return;
g_lastCommittedMinute = nowMinute;
for (const auto &d : DeviceStore::devices()) {
commitOneDevice(d.mac, nowEpoch);
}
pruneUnregisteredBuffers();
}
void clear() {
g_buffers.clear();
// 「最後に記録した分」も0に戻します。
// 現在の分のままにしておくと、次の分の区切りが来るまで記録が始まらず、
// 消去直後にグラフを開いたときに何も表示されないままになります。
g_lastCommittedMinute = 0;
}
time_t oldestTimestamp(const String &mac) {
auto it = g_buffers.find(mac);
if (it == g_buffers.end() || it->second.empty()) return 0;
return time_t(it->second.front().timestamp);
}
size_t getRecent(const String &mac, HistorySample *outBuf, size_t maxCount) {
auto it = g_buffers.find(mac);
if (it == g_buffers.end()) return 0;
const auto &buf = it->second;
size_t n = min(maxCount, buf.size());
// 新しい方からn件を、古い順に並べてコピーします。
size_t startIdx = buf.size() - n;
for (size_t i = 0; i < n; i++) outBuf[i] = buf[startIdx + i];
return n;
}
} // namespace HistoryCache