Skip to content

Commit 86821a4

Browse files
authored
Merge branch 'main' into codex/audit-and-update-settings_controller-actions
2 parents ea36c9b + 6943d33 commit 86821a4

17 files changed

Lines changed: 2261 additions & 213 deletions

app/apps/app_launcher/view/view.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <smooth_lvgl.h>
1414
#include <smooth_ui_toolkit.h>
1515

16+
#include "integration/cctv_controller.h"
1617
#include "integration/settings_controller.h"
1718
#include "ui/pages/ui_page_settings.h"
1819
#include "ui/ui_root.h"
@@ -65,6 +66,7 @@ void LauncherView::init()
6566

6667
if (_ui_root != nullptr)
6768
{
69+
_cctv_controller.reset();
6870
_settings_controller.reset();
6971
ui_root_destroy(_ui_root);
7072
_ui_root = nullptr;
@@ -73,6 +75,7 @@ void LauncherView::init()
7375
if (_ui_root != nullptr)
7476
{
7577
_settings_controller = std::make_unique<SettingsController>();
78+
_cctv_controller = std::make_unique<CctvController>();
7679

7780
ui_page_settings_actions_t actions{};
7881
actions.run_connection_test = [](const char* tester_id, void* user_data)
@@ -182,6 +185,10 @@ void LauncherView::init()
182185

183186
ui_page_settings_set_actions(&actions, _settings_controller.get());
184187
_settings_controller->PublishInitialState();
188+
if (_cctv_controller != nullptr)
189+
{
190+
_cctv_controller->PublishInitialState();
191+
}
185192
}
186193
}
187194

@@ -197,6 +204,7 @@ void LauncherView::update()
197204

198205
LauncherView::~LauncherView()
199206
{
207+
_cctv_controller.reset();
200208
_settings_controller.reset();
201209
if (_ui_root != nullptr)
202210
{

app/apps/app_launcher/view/view.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414

1515
#include "integration/settings_controller.h"
1616

17+
namespace custom::integration
18+
{
19+
class CctvController;
20+
}
21+
1722
struct ui_root_t;
1823

1924
namespace launcher_view
@@ -319,6 +324,7 @@ namespace launcher_view
319324
std::vector<std::unique_ptr<PanelBase>> _panels;
320325
ui_root_t* _ui_root = nullptr;
321326
std::unique_ptr<custom::integration::SettingsController> _settings_controller;
327+
std::unique_ptr<custom::integration::CctvController> _cctv_controller;
322328

323329
void update_anim();
324330
};
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
#include "integration/cctv_controller.h"
7+
8+
#include <algorithm>
9+
#include <array>
10+
11+
#include "core/app_trace.h"
12+
13+
namespace custom::integration
14+
{
15+
16+
namespace
17+
{
18+
19+
constexpr const char* kTag = "cctv-controller";
20+
21+
constexpr std::array<const char*, 3> kQualityOptions = {
22+
"1080p60",
23+
"720p30",
24+
"360p15",
25+
};
26+
27+
constexpr ui_page_cctv_camera_t kCameras[] = {
28+
{
29+
.camera_id = "front_porch",
30+
.name = "Front Porch",
31+
.status = "Streaming · 1080p60",
32+
.stream_url = "rtsp://frigate/front_porch/stream",
33+
.snapshot_url = "https://frigate.local/api/front_porch.jpg",
34+
.audio_supported = true,
35+
},
36+
{
37+
.camera_id = "garage",
38+
.name = "Garage Bay",
39+
.status = "Idle · Door closed",
40+
.stream_url = "rtsp://frigate/garage/stream",
41+
.snapshot_url = "https://frigate.local/api/garage.jpg",
42+
.audio_supported = false,
43+
},
44+
{
45+
.camera_id = "backyard",
46+
.name = "Backyard",
47+
.status = "Streaming · 1440p30",
48+
.stream_url = "rtsp://frigate/backyard/stream",
49+
.snapshot_url = "https://frigate.local/api/backyard.jpg",
50+
.audio_supported = true,
51+
},
52+
};
53+
54+
constexpr ui_page_cctv_event_t kEvents[] = {
55+
{
56+
.event_id = "evt_front_1742",
57+
.camera_id = "front_porch",
58+
.title = "Front Porch",
59+
.description = "Person detected · 2m ago",
60+
.timestamp = "Today · 17:42",
61+
.clip_url = "https://frigate.local/api/front_porch/event/evt_front_1742.mp4",
62+
.snapshot_url = "https://frigate.local/api/front_porch/event/evt_front_1742.jpg",
63+
},
64+
{
65+
.event_id = "evt_drive_1737",
66+
.camera_id = "garage",
67+
.title = "Driveway",
68+
.description = "Vehicle detected",
69+
.timestamp = "Today · 17:37",
70+
.clip_url = "https://frigate.local/api/garage/event/evt_drive_1737.mp4",
71+
.snapshot_url = "https://frigate.local/api/garage/event/evt_drive_1737.jpg",
72+
},
73+
{
74+
.event_id = "evt_gate_1715",
75+
.camera_id = "backyard",
76+
.title = "Backyard Gate",
77+
.description = "Package drop-off",
78+
.timestamp = "Today · 17:15",
79+
.clip_url = "https://frigate.local/api/backyard/event/evt_gate_1715.mp4",
80+
.snapshot_url = "https://frigate.local/api/backyard/event/evt_gate_1715.jpg",
81+
},
82+
};
83+
84+
constexpr std::size_t CameraCount = sizeof(kCameras) / sizeof(kCameras[0]);
85+
constexpr std::size_t EventCount = sizeof(kEvents) / sizeof(kEvents[0]);
86+
87+
} // namespace
88+
89+
CctvController::CctvController()
90+
{
91+
page_ = ui_page_cctv_get_obj();
92+
if (page_ != nullptr)
93+
{
94+
lv_obj_add_event_cb(page_, PageEventCb, UI_PAGE_CCTV_EVENT_ACTION, this);
95+
lv_obj_add_event_cb(page_, PageEventCb, UI_PAGE_CCTV_EVENT_OPEN_CLIP, this);
96+
}
97+
}
98+
99+
CctvController::~CctvController()
100+
{
101+
if (page_ != nullptr)
102+
{
103+
lv_obj_remove_event_cb_with_user_data(page_, PageEventCb, this);
104+
}
105+
}
106+
107+
void CctvController::PublishInitialState()
108+
{
109+
PushState();
110+
}
111+
112+
void CctvController::PageEventCb(lv_event_t* event)
113+
{
114+
if (event == nullptr)
115+
{
116+
return;
117+
}
118+
119+
auto* controller = static_cast<CctvController*>(lv_event_get_user_data(event));
120+
if (controller == nullptr)
121+
{
122+
return;
123+
}
124+
125+
lv_event_code_t code = lv_event_get_code(event);
126+
127+
if (code == UI_PAGE_CCTV_EVENT_ACTION)
128+
{
129+
const auto* action =
130+
static_cast<const ui_page_cctv_action_event_t*>(lv_event_get_param(event));
131+
if (action != nullptr)
132+
{
133+
controller->HandleAction(*action);
134+
}
135+
}
136+
+else if (code == UI_PAGE_CCTV_EVENT_OPEN_CLIP) +
137+
{
138+
+const auto* clip =
139+
static_cast<const ui_page_cctv_clip_event_t*>(lv_event_get_param(event));
140+
+if (clip != nullptr) +
141+
{
142+
+controller->HandleClipRequest(*clip);
143+
+
144+
}
145+
+
146+
}
147+
}
148+
149+
void CctvController::HandleAction(const ui_page_cctv_action_event_t& action)
150+
{
151+
if (CameraCount == 0)
152+
{
153+
return;
154+
}
155+
156+
switch (action.action)
157+
{
158+
case UI_PAGE_CCTV_ACTION_PREVIOUS:
159+
if (CameraCount > 0)
160+
{
161+
if (active_index_ == 0)
162+
{
163+
active_index_ = CameraCount - 1;
164+
}
165+
else
166+
{
167+
active_index_--;
168+
}
169+
PushState();
170+
}
171+
break;
172+
173+
case UI_PAGE_CCTV_ACTION_NEXT:
174+
if (CameraCount > 0)
175+
{
176+
active_index_ = (active_index_ + 1) % CameraCount;
177+
PushState();
178+
}
179+
break;
180+
181+
case UI_PAGE_CCTV_ACTION_QUALITY:
182+
quality_index_ = (quality_index_ + 1) % kQualityOptions.size();
183+
PushState();
184+
break;
185+
186+
case UI_PAGE_CCTV_ACTION_TOGGLE_MUTE:
187+
muted_ = !muted_;
188+
PushState();
189+
break;
190+
191+
case UI_PAGE_CCTV_ACTION_OPEN_GATE:
192+
APP_TRACEI(kTag,
193+
"Requested gate open for camera %s",
194+
action.camera_id != nullptr ? action.camera_id : "(none)");
195+
break;
196+
197+
case UI_PAGE_CCTV_ACTION_TALK:
198+
APP_TRACEI(kTag,
199+
"Initiating talkback on %s",
200+
action.camera_id != nullptr ? action.camera_id : "(none)");
201+
break;
202+
203+
case UI_PAGE_CCTV_ACTION_SNAPSHOT:
204+
APP_TRACEI(kTag,
205+
"Snapshot requested for %s",
206+
action.camera_id != nullptr ? action.camera_id : "(none)");
207+
break;
208+
209+
case UI_PAGE_CCTV_ACTION_TIMELINE:
210+
APP_TRACEI(kTag, "Timeline requested (events=%u)", (unsigned int)EventCount);
211+
break;
212+
}
213+
}
214+
215+
void CctvController::HandleClipRequest(const ui_page_cctv_clip_event_t& clip)
216+
{
217+
if (clip.event == nullptr)
218+
{
219+
return;
220+
}
221+
222+
const char* clip_id = clip.event->event_id != nullptr ? clip.event->event_id : "(unknown)";
223+
APP_TRACEI(kTag, "Open clip %s", clip_id);
224+
}
225+
226+
void CctvController::PushState()
227+
{
228+
if (page_ == nullptr)
229+
{
230+
return;
231+
}
232+
233+
ui_page_cctv_state_t state = {
234+
.cameras = kCameras,
235+
.camera_count = CameraCount,
236+
.active_index = std::min(active_index_, CameraCount > 0 ? CameraCount - 1 : 0),
237+
.events = kEvents,
238+
.event_count = EventCount,
239+
.quality_label = kQualityOptions[quality_index_],
240+
.muted = muted_,
241+
};
242+
243+
ui_page_cctv_set_state(&state);
244+
}
245+
246+
} // namespace custom::integration
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
#pragma once
7+
8+
#include <cstddef>
9+
10+
#ifdef __has_include
11+
# if __has_include("lvgl.h")
12+
# ifndef LV_LVGL_H_INCLUDE_SIMPLE
13+
# define LV_LVGL_H_INCLUDE_SIMPLE
14+
# endif
15+
# endif
16+
#endif
17+
18+
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
19+
# include "lvgl.h"
20+
#else
21+
# include "lvgl/lvgl.h"
22+
#endif
23+
24+
#include "ui/pages/ui_page_cctv.h"
25+
26+
namespace custom::integration
27+
{
28+
29+
class CctvController
30+
{
31+
public:
32+
CctvController();
33+
~CctvController();
34+
35+
CctvController(const CctvController&) = delete;
36+
CctvController& operator=(const CctvController&) = delete;
37+
38+
void PublishInitialState();
39+
40+
private:
41+
static void PageEventCb(lv_event_t* event);
42+
43+
void HandleAction(const ui_page_cctv_action_event_t& action);
44+
void HandleClipRequest(const ui_page_cctv_clip_event_t& clip);
45+
void PushState();
46+
47+
lv_obj_t* page_ = nullptr;
48+
std::size_t active_index_ = 0;
49+
std::size_t quality_index_ = 0;
50+
bool muted_ = false;
51+
};
52+
53+
} // namespace custom::integration

0 commit comments

Comments
 (0)