-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUnitSensor.cpp
More file actions
260 lines (224 loc) · 9.01 KB
/
Copy pathUnitSensor.cpp
File metadata and controls
260 lines (224 loc) · 9.01 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
259
260
#include "UnitSensor.h"
#include <M5Unified.h>
#include <M5UnitENV.h>
#include <Wire.h>
#include "Config.h"
#include "DeviceStore.h"
#include "SettingsStore.h"
namespace {
// 読み出しの進み具合。
// Idle ... 次の読み出しの時刻を待っている
// Measuring ... 温湿度センサに測定を指示し、結果が出るのを待っている
enum class Phase { Idle, Measuring };
int g_type = GROVE_SENSOR_NONE;
bool g_ready = false; // センサが見つかり、初期化できている
Phase g_phase = Phase::Idle;
uint32_t g_phaseEnteredMillis = 0;
uint32_t g_lastPollMillis = 0;
SensorReading g_reading;
bool g_everRead = false;
// 読み出しの成否の数。5分ごとの状態ログに出します。
// 記録に欠けが出たとき、原因がセンサの読み出しかどうかを見分けるためです。
uint32_t g_pollCount = 0;
uint32_t g_tempHumFailures = 0;
uint32_t g_pressureFailures = 0;
uint32_t g_startFailures = 0;
QMP6988 g_qmp;
// 気圧センサ(QMP6988)は、この製品のライブラリをそのまま使います。
// 測定値から気圧を求めるには、個体ごとの補正値を使った長い計算が必要で、
// 自前で書き直す利点がないためです。読み出し自体に待ち時間はありません。
bool g_hasQmp = false;
// SHT30が返すデータの誤り検出符号(CRC-8)を計算します。
//
// 2バイトの測定値ごとに1バイト付いてきます。確かめずに使うと、通信が乱れた
// ときに、あり得ない温度や湿度をそのまま記録してしまいます。
uint8_t sht30Crc(const uint8_t *data, size_t len) {
uint8_t crc = 0xFF;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (int b = 0; b < 8; b++) {
crc = (crc & 0x80) ? uint8_t((crc << 1) ^ 0x31) : uint8_t(crc << 1);
}
}
return crc;
}
// 温湿度センサに測定を指示します。
//
// 使うのは「クロックストレッチ無し」の指示(0x2400)です。有りにすると、
// センサが測定を終えるまでI2Cの信号線を押さえ続けるため、読み出しの呼び出しが
// そのぶん戻ってこなくなります。無しなら、指示だけ出して一度loop()に戻れます。
bool sht30StartMeasure() {
Wire.beginTransmission(GROVE_SHT30_ADDR);
Wire.write(0x24);
Wire.write(0x00);
return Wire.endTransmission() == 0;
}
// 測定の結果を読みます。温度(℃)と湿度(%)を受け取ります。
bool sht30Read(float *outTemp, float *outHum) {
if (Wire.requestFrom(uint8_t(GROVE_SHT30_ADDR), size_t(6)) != 6) return false;
uint8_t b[6];
for (int i = 0; i < 6; i++) b[i] = uint8_t(Wire.read());
if (sht30Crc(&b[0], 2) != b[2] || sht30Crc(&b[3], 2) != b[5]) return false;
uint16_t rawT = uint16_t(b[0]) << 8 | b[1];
uint16_t rawH = uint16_t(b[3]) << 8 | b[4];
// データシートに載っている変換式です。
*outTemp = -45.0f + 175.0f * float(rawT) / 65535.0f;
*outHum = 100.0f * float(rawH) / 65535.0f;
return true;
}
void releaseSensors() {
g_ready = false;
g_hasQmp = false;
g_everRead = false;
g_reading = SensorReading();
g_phase = Phase::Idle;
}
// 選ばれた種類のセンサを初期化します。
void setupSensors() {
releaseSensors();
if (g_type == GROVE_SENSOR_NONE) return;
int sda = M5.Ex_I2C.getSDA();
int scl = M5.Ex_I2C.getSCL();
if (sda < 0 || scl < 0) {
Serial.println("[UNIT] Grove端子のピンが分かりません");
return;
}
Wire.begin(sda, scl, 400000);
if (g_type == GROVE_SENSOR_ENV3) {
// 温湿度センサは応答があるかだけ確かめます。設定は不要で、測定のたびに
// 指示を出す使い方だからです。
Wire.beginTransmission(GROVE_SHT30_ADDR);
bool sht = (Wire.endTransmission() == 0);
g_hasQmp = g_qmp.begin(&Wire, GROVE_QMP6988_ADDR, uint8_t(sda), uint8_t(scl), 400000);
g_ready = sht;
Serial.printf("[UNIT] ENV III: 温湿度センサ %s / 気圧センサ %s\n", sht ? "あり" : "なし",
g_hasQmp ? "あり" : "なし");
if (!g_ready) Serial.println("[UNIT] センサが応答しません -- Grove端子の接続を確認してください");
} else {
Serial.println("[UNIT] この種類にはまだ対応していません");
}
}
// 疑似的な機器をDeviceStoreへ登録します。既に登録されていれば何もしません。
void registerDevice() {
String id = UnitSensor::deviceId();
if (id.length() == 0 || DeviceStore::isRegistered(id)) return;
const char *name = (g_type == GROVE_SENSOR_ENV3) ? "ENV III" : "ENV Pro";
if (DeviceStore::addDevice(id, name, name)) {
Serial.printf("[UNIT] %s を機器として登録しました (%s)\n", name, id.c_str());
}
}
} // namespace
namespace UnitSensor {
String deviceId() {
switch (g_type) {
case GROVE_SENSOR_ENV3: return String(GROVE_DEVICE_ID_ENV3);
case GROVE_SENSOR_ENVPRO: return String(GROVE_DEVICE_ID_ENVPRO);
default: return String();
}
}
bool isUnitDevice(const String &id) {
String mine = deviceId();
return mine.length() > 0 && id == mine;
}
void begin() {
g_type = SettingsStore::groveSensorType();
// 対応していない種類が保存されていたら「無し」に戻します。
// 設定画面に選択肢が無い値が残っていると、どのボタンも選ばれていない状態に
// 見えてしまいます(試用中にENV Proを選んだ場合などに起こります)。
if (g_type != GROVE_SENSOR_NONE && g_type != GROVE_SENSOR_ENV3) {
Serial.printf("[UNIT] 対応していない種類が保存されていました (%d) -- 無しに戻します\n", g_type);
g_type = GROVE_SENSOR_NONE;
SettingsStore::setGroveSensorType(g_type);
}
setupSensors();
if (g_ready) registerDevice();
}
void applySensorType(int type) {
if (type == g_type) return;
// 前の種類の疑似機器を登録から外します。履歴のファイルはカードに残るので、
// 繋ぎ直せば続きから記録されます。
String previous = deviceId();
if (previous.length() > 0) DeviceStore::removeDevice(previous);
g_type = type;
SettingsStore::setGroveSensorType(type);
setupSensors();
if (g_ready) registerDevice();
}
void update() {
if (!g_ready) return;
switch (g_phase) {
case Phase::Idle:
if (millis() - g_lastPollMillis < GROVE_POLL_INTERVAL_MS && g_everRead) return;
if (!sht30StartMeasure()) {
g_lastPollMillis = millis();
g_pollCount++;
g_startFailures++;
return;
}
g_phase = Phase::Measuring;
g_phaseEnteredMillis = millis();
break;
case Phase::Measuring: {
if (millis() - g_phaseEnteredMillis < GROVE_SHT30_MEASURE_WAIT_MS) return;
g_phase = Phase::Idle;
g_lastPollMillis = millis();
uint32_t now = millis();
// Groveから給電されているので電池はありません。
g_reading.hasBattery = false;
g_reading.battery = 0;
g_reading.hasCo2 = false;
// 温湿度と気圧は別々のセンサなので、片方が読めなくてももう片方は読みます。
// まとめて諦めると、温湿度の一度の失敗で気圧の記録まで欠けてしまいます。
float temp = NAN;
float hum = NAN;
if (sht30Read(&temp, &hum)) {
g_reading.temperature = temp;
g_reading.humidity = hum;
g_reading.tempHumMillis = now;
g_reading.lastSeenMillis = now;
g_everRead = true;
} else {
g_tempHumFailures++;
}
if (g_hasQmp) {
g_qmp.update();
// ライブラリが返す気圧の単位はPaです。表示にも記録にもhPaを使うので、
// ここで直しておきます。
float hpa = g_qmp.pressure / 100.0f;
// 気圧センサのライブラリは失敗しても真を返すので、値そのものを見ます。
// 読めていないときは0付近になります。
if (hpa > GROVE_PRESSURE_MIN_HPA && hpa < GROVE_PRESSURE_MAX_HPA) {
g_reading.pressureHpa = hpa;
g_reading.pressureMillis = now;
g_reading.lastSeenMillis = now;
g_everRead = true;
} else {
g_pressureFailures++;
}
}
g_pollCount++;
g_reading.online = true;
break;
}
}
}
void takeStats(uint32_t *outPolls, uint32_t *outTempHumFail, uint32_t *outPressureFail,
uint32_t *outStartFail) {
*outPolls = g_pollCount;
*outTempHumFail = g_tempHumFailures;
*outPressureFail = g_pressureFailures;
*outStartFail = g_startFailures;
g_pollCount = 0;
g_tempHumFailures = 0;
g_pressureFailures = 0;
g_startFailures = 0;
}
bool isReady() { return g_ready; }
bool getReading(const String &id, SensorReading &out) {
if (!isUnitDevice(id) || !g_everRead) return false;
// 読み出しが途絶えたら「未受信」にします。判定の時間はBLEの機器と同じです。
g_reading.online = (millis() - g_reading.lastSeenMillis) < DEVICE_OFFLINE_TIMEOUT_MS;
out = g_reading;
return true;
}
} // namespace UnitSensor