|
1 | 1 | #pragma once |
2 | | -/** |
3 | | - * @file App.h |
4 | | - * @brief サイクルコンピュータのメインアプリケーションクラス |
5 | | - * |
6 | | - * このクラスは、サイクルコンピュータのすべての機能を統合・制御します。 |
7 | | - * - GNSSからの位置情報取得 |
8 | | - * - 速度・距離・時間の計算 |
9 | | - * - ユーザー入力(ボタン操作)の処理 |
10 | | - * - OLED画面への表示 |
11 | | - * - EEPROMへのデータ永続化 |
12 | | - * |
13 | | - * ダブルバッファリングを使用して、効率的なデータ更新と表示を実現しています。 |
14 | | - */ |
15 | 2 |
|
16 | 3 | #include <Arduino.h> |
17 | 4 | #include <stddef.h> |
18 | 5 |
|
| 6 | +#include "common/DoubleBuffer.h" |
19 | 7 | #include "domain/DataStore.h" |
20 | | -#include "domain/DisplayLogic.h" |
21 | | -#include "domain/GnssAdapter.h" |
22 | | -#include "domain/InputLogic.h" |
23 | | -#include "domain/PersistenceLogic.h" |
24 | 8 | #include "domain/TripLogic.h" |
25 | 9 | #include "domain/VoltageMonitor.h" |
26 | 10 | #include "hardware/Clock.h" |
27 | 11 | #include "hardware/Gnss.h" |
28 | 12 | #include "ui/FrameLogic.h" |
29 | 13 | #include "ui/UI.h" |
30 | 14 |
|
31 | | -/** |
32 | | - * @class App |
33 | | - * @brief アプリケーションのメインクラス |
34 | | - * |
35 | | - * setup()でbegin()を呼び、loop()でupdate()を呼ぶだけで動作します。 |
36 | | - */ |
37 | 15 | class App { |
38 | 16 | private: |
39 | | - // ==================== ハードウェア関連 ==================== |
40 | | - Gnss gnss; ///< GNSSモジュール制御 |
41 | | - Clock systemClock; ///< RTC(リアルタイムクロック) |
42 | | - DataStore dataStore; ///< EEPROMデータ管理 |
43 | | - VoltageMonitor voltageMonitor; ///< バッテリー電圧監視 |
44 | | - UI userInterface; ///< ユーザーインターフェース(ボタン + OLED) |
45 | | - |
46 | | - // ==================== 状態管理 ==================== |
47 | | - Mode currentMode = Mode::SPD_TIM; ///< 現在の表示モード |
48 | | - |
49 | | - /** |
50 | | - * GNSSから取得した最新データ |
51 | | - */ |
52 | | - GnssData gnssData; |
53 | | - |
54 | | - /** |
55 | | - * 走行状態のダブルバッファ |
56 | | - * [0]と[1]を交互に使用し、前回値との差分を効率的に検出します。 |
57 | | - */ |
58 | | - TripState tripState[2]; |
59 | | - |
60 | | - /** |
61 | | - * 表示フレームのダブルバッファ |
62 | | - * 画面更新が必要かどうかを判定するために使用します。 |
63 | | - */ |
64 | | - DisplayFrame frames[2]; |
65 | | - |
66 | | - /** |
67 | | - * 保存データのダブルバッファ |
68 | | - * 前回保存時からの変更有無を判定するために使用します。 |
69 | | - */ |
70 | | - SaveData saveBuffers[2]; |
71 | | - |
72 | | - int currentIdx = 0; ///< 現在の走行状態バッファインデックス |
73 | | - int frameIdx = 0; ///< 現在の表示フレームバッファインデックス |
74 | | - int saveIdx = 0; ///< 現在の保存データバッファインデックス |
75 | | - |
76 | | - unsigned long lastSaveMs = 0; ///< 最後にEEPROMに保存した時刻 |
77 | | - unsigned long lastUiUpdateMs = 0; ///< 最後にUI更新した時刻 |
| 17 | + Gnss gnss; |
| 18 | + Clock systemClock; |
| 19 | + DataStore dataStore; |
| 20 | + VoltageMonitor voltageMonitor; |
| 21 | + UI userInterface; |
| 22 | + |
| 23 | + Mode currentMode = Mode::SPD_TIM; |
| 24 | + |
| 25 | + DoubleBuffer<TripState> tripBuffer; |
| 26 | + DoubleBuffer<DisplayFrame> frameBuffer; |
| 27 | + DoubleBuffer<SaveData> saveBuffer; |
| 28 | + |
| 29 | + unsigned long currentTime = 0; |
| 30 | + GnssData currentGnss = {}; |
| 31 | + Input::Event currentButton = Input::Event::NONE; |
| 32 | + SpGnssTime currentClock = {}; |
| 33 | + float currentVoltage = 0.0f; |
| 34 | + |
| 35 | + unsigned long lastSaveMs = 0; |
| 36 | + unsigned long lastUiUpdateMs = 0; |
78 | 37 |
|
79 | 38 | public: |
80 | | - App() = default; |
81 | | - |
82 | | - /** |
83 | | - * @brief アプリケーションの初期化 |
84 | | - * |
85 | | - * setup()で1回だけ呼び出してください。 |
86 | | - * - 各ハードウェアの初期化 |
87 | | - * - EEPROMからの保存データ読み込み |
88 | | - * - 走行状態の初期化 |
89 | | - */ |
90 | 39 | void begin() { |
91 | 40 | gnss.begin(); |
92 | 41 | systemClock.begin(); |
93 | 42 | voltageMonitor.begin(); |
94 | 43 | userInterface.begin(); |
| 44 | + loadFromStorage(); |
| 45 | + } |
95 | 46 |
|
96 | | - // EEPROMから前回の走行データを読み込む |
| 47 | + void update() { |
| 48 | + collectInputs(); |
| 49 | + updateState(); |
| 50 | + processOutputs(); |
| 51 | + } |
| 52 | + |
| 53 | +private: |
| 54 | + void loadFromStorage() { |
97 | 55 | SaveData saved = dataStore.load(); |
98 | 56 |
|
99 | | - // 両方のバッファに同じ初期値を設定 |
100 | | - for (auto &state : tripState) { |
101 | | - state.resetAll(); |
102 | | - state.distance.total = saved.totalDistance; |
103 | | - state.distance.trip = saved.tripDistance; |
104 | | - state.time.moving = saved.movingTimeMs; |
105 | | - state.speed.max = saved.maxSpeed; |
| 57 | + TripState state; |
| 58 | + state.resetAll(); |
| 59 | + state.distance.total = saved.totalDistance; |
| 60 | + state.distance.trip = saved.tripDistance; |
| 61 | + state.time.moving = saved.movingTimeMs; |
| 62 | + state.speed.max = saved.maxSpeed; |
| 63 | + |
| 64 | + tripBuffer.initialize(state); |
| 65 | + saveBuffer.initialize(saved); |
| 66 | + lastSaveMs = millis(); |
| 67 | + } |
| 68 | + |
| 69 | + void collectInputs() { |
| 70 | + currentTime = millis(); |
| 71 | + currentButton = userInterface.getInputEvent(); |
| 72 | + currentClock = systemClock.now(); |
| 73 | + currentVoltage = voltageMonitor.update(); |
| 74 | + |
| 75 | + bool updated = gnss.update(); |
| 76 | + currentGnss.status = updated ? UpdateStatus::Updated : UpdateStatus::NoChange; |
| 77 | + currentGnss.navData = gnss.navData; |
| 78 | + |
| 79 | + if (updated && (SpFixMode)currentGnss.navData.posFixMode != FixInvalid) { |
| 80 | + systemClock.sync(currentGnss.navData.time); |
106 | 81 | } |
| 82 | + } |
107 | 83 |
|
108 | | - saveBuffers[0] = saved; |
109 | | - saveBuffers[1] = saved; |
| 84 | + void updateState() { |
| 85 | + tripBuffer.prepare(); |
| 86 | + tripBuffer.current().resetMeta(); |
110 | 87 |
|
111 | | - lastSaveMs = millis(); |
| 88 | + if (currentButton != Input::Event::NONE) { |
| 89 | + if (handleButton()) return; |
| 90 | + } |
| 91 | + |
| 92 | + TripLogic::computeTrip(tripBuffer.current(), currentGnss, currentTime); |
112 | 93 | } |
113 | 94 |
|
114 | | - /** |
115 | | - * @brief メインループ処理 |
116 | | - * |
117 | | - * loop()で繰り返し呼び出してください。 |
118 | | - * 以下の処理を順番に実行します: |
119 | | - * 1. GNSSデータの取得 |
120 | | - * 2. ボタン入力の処理 |
121 | | - * 3. RTCの同期(GPS時刻から) |
122 | | - * 4. 走行データの計算 |
123 | | - * 5. データの保存(一定間隔で) |
124 | | - * 6. 画面の更新(必要な場合のみ) |
125 | | - */ |
126 | | - void update() { |
127 | | - const unsigned long now = millis(); |
| 95 | + bool handleButton() { |
| 96 | + TripState &state = tripBuffer.current(); |
128 | 97 |
|
129 | | - // ダブルバッファのインデックスを切り替え |
130 | | - // prevIdx: 前回の状態, currIdx: 今回更新する状態 |
131 | | - const int prevIdx = currentIdx; |
132 | | - const int currIdx = 1 - currentIdx; |
| 98 | + switch (currentButton) { |
| 99 | + case Input::Event::SELECT: |
| 100 | + currentMode = static_cast<Mode>((static_cast<int>(currentMode) + 1) % 3); |
| 101 | + state.forceUpdate(); |
| 102 | + break; |
133 | 103 |
|
134 | | - // 前回の状態をコピーしてから更新 |
135 | | - tripState[currIdx] = tripState[prevIdx]; |
136 | | - tripState[currIdx].resetMeta(); // 更新フラグをリセット |
| 104 | + case Input::Event::PAUSE: |
| 105 | + state.status = |
| 106 | + state.isPaused() ? TripStateBase::Status::Stopped : TripStateBase::Status::Paused; |
| 107 | + state.forceUpdate(); |
| 108 | + break; |
137 | 109 |
|
138 | | - // GNSSデータを取得 |
139 | | - gnssData = GnssAdapter::collect(gnss); |
140 | | - Input::Event event = userInterface.getInputEvent(); |
| 110 | + case Input::Event::RESET: |
| 111 | + applyReset(currentMode); |
| 112 | + break; |
141 | 113 |
|
142 | | - // GPS信号が有効なら、RTCをGPS時刻で同期 |
143 | | - if (gnssData.status == UpdateStatus::Updated && |
144 | | - (SpFixMode)gnssData.navData.posFixMode != FixInvalid) { |
145 | | - systemClock.sync(gnssData.navData.time); |
| 114 | + case Input::Event::RESET_LONG: |
| 115 | + state.resetAll(); |
| 116 | + dataStore.clear(); |
| 117 | + userInterface.showResetMessage(); |
| 118 | + frameBuffer.initialize(DisplayFrame()); |
| 119 | + saveBuffer.initialize(createSaveData(state, 0.0f)); |
| 120 | + return true; |
| 121 | + |
| 122 | + default: |
| 123 | + break; |
146 | 124 | } |
| 125 | + return false; |
| 126 | + } |
147 | 127 |
|
148 | | - // ボタンイベントの処理 |
149 | | - if (event != Input::Event::NONE) { |
150 | | - auto result = InputLogic::handleEvent(tripState[currIdx], currentMode, event); |
151 | | - currentMode = result.newMode; |
152 | | - |
153 | | - // 全データリセットが要求された場合 |
154 | | - if (result.shouldClearStorage) { |
155 | | - dataStore.clear(); |
156 | | - userInterface.showResetMessage(); |
157 | | - |
158 | | - // 表示フレームをクリア |
159 | | - frames[0] = DisplayFrame(); |
160 | | - frames[1] = DisplayFrame(); |
161 | | - |
162 | | - // 保存バッファもクリア |
163 | | - TripState emptyState; |
164 | | - emptyState.resetAll(); |
165 | | - SaveData emptySave = PersistenceLogic::create(emptyState, 0.0f); |
166 | | - saveBuffers[0] = emptySave; |
167 | | - saveBuffers[1] = emptySave; |
168 | | - } |
| 128 | + void applyReset(Mode mode) { |
| 129 | + TripState &state = tripBuffer.current(); |
| 130 | + switch (mode) { |
| 131 | + case Mode::SPD_TIM: |
| 132 | + state.resetTrip(); |
| 133 | + break; |
| 134 | + case Mode::AVG_ODO: |
| 135 | + state.resetAll(); |
| 136 | + break; |
| 137 | + case Mode::MAX_CLK: |
| 138 | + state.resetMaxSpeed(); |
| 139 | + break; |
169 | 140 | } |
| 141 | + } |
170 | 142 |
|
171 | | - // 走行データを計算(速度・距離・時間) |
172 | | - TripLogic::computeTrip(tripState[currIdx], gnssData, now); |
| 143 | + void processOutputs() { |
| 144 | + outputToDisplay(); |
| 145 | + outputToStorage(); |
| 146 | + } |
173 | 147 |
|
174 | | - // 定期的にデータを保存 |
175 | | - handleSave(tripState[currIdx], now); |
| 148 | + void outputToDisplay() { |
| 149 | + if (!shouldUpdateUI()) return; |
176 | 150 |
|
177 | | - // 必要に応じて画面を更新 |
178 | | - handleUI(tripState[prevIdx], tripState[currIdx], now); |
| 151 | + DisplayFrame nextFrame = |
| 152 | + FrameLogic::buildFrame(tripBuffer.current(), currentGnss, currentClock, currentMode); |
179 | 153 |
|
180 | | - // 現在のバッファインデックスを更新(次回は逆のバッファを使う) |
181 | | - currentIdx = currIdx; |
| 154 | + if (frameBuffer.apply(nextFrame)) { |
| 155 | + userInterface.draw(frameBuffer.current()); |
| 156 | + lastUiUpdateMs = currentTime; |
| 157 | + } |
182 | 158 | } |
183 | 159 |
|
184 | | -private: |
185 | | - /** |
186 | | - * @brief データ保存処理 |
187 | | - * @param state 現在の走行状態 |
188 | | - * @param now 現在時刻(ミリ秒) |
189 | | - * |
190 | | - * 以下の条件がすべて満たされた場合にのみ保存します: |
191 | | - * - 前回の保存から一定時間(30秒)経過 |
192 | | - * - GNSSがアイドル状態(更新処理中でない) |
193 | | - * - 前回保存時からデータに変更がある |
194 | | - */ |
195 | | - void handleSave(const TripState &state, unsigned long now) { |
196 | | - // 保存間隔のチェック |
197 | | - if (now - lastSaveMs < DataStore::SAVE_INTERVAL_MS) return; |
198 | | - |
199 | | - // GNSS更新中は保存しない(CPUリソースを節約) |
200 | | - if (gnssData.status != UpdateStatus::NoChange) return; |
201 | | - |
202 | | - // 現在のバッテリー電圧を取得 |
203 | | - float v = voltageMonitor.update(); |
204 | | - SaveData pData = PersistenceLogic::create(state, v); |
205 | | - |
206 | | - // ダブルバッファで変更を検出 |
207 | | - const int prevSaveIdx = saveIdx; |
208 | | - saveIdx = 1 - saveIdx; |
209 | | - saveBuffers[saveIdx] = pData; |
210 | | - |
211 | | - // 変更があった場合のみ実際に保存(EEPROM書き込み回数を削減) |
212 | | - if (saveBuffers[saveIdx] != saveBuffers[prevSaveIdx]) dataStore.save(saveBuffers[saveIdx]); |
213 | | - lastSaveMs = now; |
| 160 | + bool shouldUpdateUI() const { |
| 161 | + return (currentButton != Input::Event::NONE) || (currentTime - lastUiUpdateMs >= 500) || |
| 162 | + TripLogic::isChanged(tripBuffer.previous(), tripBuffer.current()) || |
| 163 | + (currentGnss.status == UpdateStatus::Updated); |
214 | 164 | } |
215 | 165 |
|
216 | | - /** |
217 | | - * @brief UI更新処理 |
218 | | - * @param prev 前回の走行状態 |
219 | | - * @param curr 現在の走行状態 |
220 | | - * @param now 現在時刻(ミリ秒) |
221 | | - * |
222 | | - * 以下のいずれかの条件で画面を更新します: |
223 | | - * - 走行データに変更があった |
224 | | - * - 強制更新フラグが設定されている |
225 | | - * - GNSSデータが更新された |
226 | | - * - 前回の更新から500ms以上経過(定期更新) |
227 | | - */ |
228 | | - void handleUI(const TripState &prev, const TripState &curr, unsigned long now) { |
229 | | - bool periodic = (now - lastUiUpdateMs >= 500); // 500ms間隔の定期更新 |
230 | | - bool changed = TripLogic::isChanged(prev, curr); // データ変更検出 |
231 | | - bool forced = (curr.updateStatus == UpdateStatus::ForceUpdate); // 強制更新 |
232 | | - bool gnssUpd = (gnssData.status == UpdateStatus::Updated); // GNSS更新 |
233 | | - |
234 | | - if (changed || forced || gnssUpd || periodic) { |
235 | | - // RTCから現在時刻を取得 |
236 | | - SpGnssTime currentTime = systemClock.now(); |
237 | | - |
238 | | - // 表示用データを生成 |
239 | | - DisplayState dData = DisplayLogic::create(curr, gnssData, currentTime, currentMode); |
240 | | - |
241 | | - // フレームを生成(ダブルバッファリング) |
242 | | - const int prevFrameIdx = frameIdx; |
243 | | - frameIdx = 1 - frameIdx; |
244 | | - frames[frameIdx] = FrameLogic::buildFrame(dData); |
245 | | - |
246 | | - // フレームに変更があった場合のみ描画 |
247 | | - if (frames[frameIdx] != frames[prevFrameIdx]) { |
248 | | - userInterface.draw(frames[frameIdx]); |
249 | | - lastUiUpdateMs = now; |
250 | | - } |
251 | | - } |
| 166 | + void outputToStorage() { |
| 167 | + if (!shouldSave()) return; |
| 168 | + |
| 169 | + SaveData nextSave = createSaveData(tripBuffer.current(), currentVoltage); |
| 170 | + |
| 171 | + if (saveBuffer.apply(nextSave)) { dataStore.save(saveBuffer.current()); } |
| 172 | + lastSaveMs = currentTime; |
| 173 | + } |
| 174 | + |
| 175 | + bool shouldSave() const { |
| 176 | + const bool shouldUpdate = (currentTime - lastSaveMs >= DataStore::SAVE_INTERVAL_MS); |
| 177 | + const bool gnssStable = (currentGnss.status == UpdateStatus::NoChange); |
| 178 | + return shouldUpdate && gnssStable; |
| 179 | + } |
| 180 | + |
| 181 | + static SaveData createSaveData(const TripState &state, float voltage) { |
| 182 | + SaveData data; |
| 183 | + data.magicNumber = SAVE_DATA_MAGIC_NUMBER; |
| 184 | + data.totalDistance = state.distance.total; |
| 185 | + data.tripDistance = state.distance.trip; |
| 186 | + data.movingTimeMs = state.time.moving; |
| 187 | + data.maxSpeed = state.speed.max; |
| 188 | + data.voltage = voltage; |
| 189 | + data.updateStatus = state.updateStatus; |
| 190 | + data.crc = 0; |
| 191 | + return data; |
252 | 192 | } |
253 | 193 | }; |
0 commit comments