Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ Screen geometry: **320×170**, a 20px top bar plus a 320×150 content area.
| Toast styling | `main/ui/launcher_toast.cpp` | `kShowDurationMs` |
| Loading overlay | `main/ui/ui_loading.cpp` | `show` / `hide` |
| Lock screen (timeout/unlock/wallpaper) | `main/ui/ui_screensaver.cpp` + `model/lockscreen_state_model.hpp`, `model/screensaver_model.hpp` | constrained by `tests/test_lockscreen_contract.py` |
| Low-battery policy / appearance | `main/ui/low_battery_flow.hpp` / `ui_low_battery.cpp` | the policy is pure logic and unit-tested |
| Low-battery policy / appearance / sound cues | `main/ui/low_battery_flow.hpp` / `ui_low_battery.cpp` | the policy is pure logic and unit-tested; cues register with `RegisterSystemSounds` and play via `cp0_signal_system_play` |
| Settings menu structure | `main/ui/settings/settings_page.cpp` | `create_page_detail` (node order = menu order), `UISettingTreePage` |
| Settings entry policy/gating | `main/ui/settings/settings_tree_types.hpp` | `SettingEntry`, `SettingActivationPolicy`, `activation_gate` |
| Add a settings sub-page | new `*_page.{hpp,cpp}` in `main/ui/settings/` + a factory and tree node in `settings_page.cpp` | three-level value pages derive from `LvSettingValuePage3Base` |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ The project is mainly developed in a Linux environment and is also compatible wi
```bash
sudo apt update
sudo apt install python3 python3-pip build-essential pkg-config libffi-dev libsdl2-dev libfreetype6-dev
sudo apt install libcurl4-openssl-dev

pip3 install parse scons requests tqdm
pip3 install setuptools-rust paramiko scp
Expand Down
1 change: 1 addition & 0 deletions README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ launcher/
```bash
sudo apt update
sudo apt install python3 python3-pip build-essential pkg-config libffi-dev libsdl2-dev libfreetype6-dev
sudo apt install libcurl4-openssl-dev

pip3 install parse scons requests tqdm
pip3 install setuptools-rust paramiko scp
Expand Down
1 change: 1 addition & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ launcher/
```bash
sudo apt update
sudo apt install python3 python3-pip build-essential pkg-config libffi-dev libsdl2-dev libfreetype6-dev
sudo apt install libcurl4-openssl-dev

pip3 install parse scons requests tqdm
pip3 install setuptools-rust paramiko scp
Expand Down
2 changes: 1 addition & 1 deletion SDK
3 changes: 2 additions & 1 deletion docs/OPEN_SOURCE_COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Paths in the evidence column are relative to the repository root.
| smooth_ui_toolkit | `ext_components/SmoothUI/SConstruct`, pinned optional UI helper | [MIT](../ext_components/SmoothUI/LICENSE) |
| spdlog | `ext_components/Spdlog/SConstruct`, pinned optional logging component | [MIT](../ext_components/Spdlog/LICENSE); bundled fmt notice also retained |
| eventpp | cp0_lvgl, APPLaunch, AppStore and ZClaw requirements; SDK eventpp component | [Apache-2.0](../LICENSES/eventpp-Apache-2.0.txt) |
| libhv | AppStore and ZClaw `main/SConstruct` requirement `hv` | [BSD-3-Clause](../LICENSES/libhv-BSD-3-Clause.txt) |
| libhv | ZClaw `main/SConstruct` requirement `hv` | [BSD-3-Clause](../LICENSES/libhv-BSD-3-Clause.txt) |
| fmt | `SDK/components/utilities/SConstruct`, `party/fmt` | [MIT with optional compiled-object exception](../LICENSES/fmt-MIT.txt) |
| nlohmann/json 3.11.3 | `projects/CardputerZero-Emulator/vendor/nlohmann/json.hpp` | [MIT](../LICENSES/nlohmann-json-MIT.txt), Niels Lohmann; original SPDX preserved |
| tinyalsa | Calculator `main/SConstruct` requirement `tinyalsa_component` | [BSD-3-Clause](../LICENSES/tinyalsa-BSD-3-Clause.txt), copied from upstream `NOTICE` (`LICENSE` points there) |
Expand Down Expand Up @@ -52,6 +52,7 @@ Component SConstruct files and application SConstruct/CMake files resolve these
| SDL2, optional SDL2_mixer | Simulator/emulator display, input and audio | zlib; mixer codecs have separate terms |
| GLib, GObject, GIO | Native service/event integration | LGPL-2.1-or-later |
| libdbus; BlueZ daemon | Bluetooth IPC | D-Bus AFL/GPL dual terms; BlueZ GPL/LGPL per component |
| libpulse.so.0; pipewire-pulse daemon | Speaker volume/mute through the PulseAudio-compatible server (`dlopen`, no build-time dependency) | libpulse LGPL-2.1-or-later; PipeWire MIT (Expat) with LGPL-2.1+ parts |
| libnm; NetworkManager daemon | Wi-Fi | libnm LGPL-2.1-or-later; NetworkManager GPL-2.0-or-later |
| libinput, xkbcommon, udev | Native input/device discovery | MIT for libinput/xkbcommon; udev LGPL terms |
| libcamera, libcamera-base | Camera | LGPL-2.1-or-later library; separate tool terms |
Expand Down
2 changes: 1 addition & 1 deletion docs/cp0_lvgl.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ All `args` below are `std::list<std::string>` with the command at element zero.
- `cp0_signal_audio_cap(bool enable)`: one-way start/stop capture.
- `cp0_signal_system_play(std::string name)`: play a system sound name. Register extra names first with `RegisterSystemSounds`.
- `cp0_signal_audio_setup`: `{"set_callback"}`, `{"set_waveform"}` or `{"set_waveform","0"}`, and `{"stop_play"}` set status callbacks, waveform reporting, and stop playback.
- `cp0_signal_audio_api`: `PlayFile path`, `Play asset`, `PlayPause`, `PlayContinue`, `PlayEnd`, `Cap`, `CapPause`, `CapContinue`, `CapEnd`, `CapFileSave path`, `SetCallback`, `VolumeRead`, `VolumeWrite 0..100`, `DefaultSink` (sink name, or code -1 with empty data when the backend has no real sink), `MuteRead`, `MuteToggle`, `SetSystemSoundNames [up to 3 names]`, `RegisterSystemSounds [1..32 names]`, `SystemSoundPlay 0..2`, `SystemSoundSuspend`, `SystemSoundPrepare`, and `SystemSoundEnable [0|1/on/off...]`. `Play` resolves a separator-free name as a resource; `PlayFile` uses the supplied path. Errors are negative codes with text.
- `cp0_signal_audio_api`: `PlayFile path`, `Play asset`, `PlayPause`, `PlayContinue`, `PlayEnd`, `Cap`, `CapPause`, `CapContinue`, `CapEnd`, `CapFileSave path`, `SetCallback`, `Preload`, `VolumeRead`, `VolumeWrite 0..100`, `DefaultSink` (sink name, or code -1 with empty data when the backend has no real sink), `MuteRead`, `MuteToggle`, `SetSystemSoundNames [up to 3 names]`, `RegisterSystemSounds [1..32 names]`, `SystemSoundPlay 0..2`, `SystemSoundSuspend`, `SystemSoundPrepare`, and `SystemSoundEnable [0|1/on/off...]`. `Preload` loads the mixer client ahead of the first volume action; `Play` resolves a separator-free name as a resource; `PlayFile` uses the supplied path. Errors are negative codes with text.

### PTY

Expand Down
2 changes: 1 addition & 1 deletion docs/cp0_lvgl.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ int cp0_file_read_first_line(const char *path, char *out, int out_size);
- `cp0_signal_audio_cap(bool enable)`: 録音の開始/停止。
- `cp0_signal_system_play(std::string name)`: システム音を名前で再生。追加名は `RegisterSystemSounds` で登録します。
- `cp0_signal_audio_setup`: `{"set_callback"}`、`{"set_waveform"}` または `{"set_waveform","0"}`、`{"stop_play"}`。
- `cp0_signal_audio_api`: `PlayFile path`、`Play asset`、`PlayPause`、`PlayContinue`、`PlayEnd`、`Cap`、`CapPause`、`CapContinue`、`CapEnd`、`CapFileSave path`、`SetCallback`、`VolumeRead`、`VolumeWrite 0..100`、`DefaultSink`(sink 名。実 sink がない backend は code -1 と空 data)、`MuteRead`、`MuteToggle`、`SetSystemSoundNames [最大3名]`、`RegisterSystemSounds [1..32名]`、`SystemSoundPlay 0..2`、`SystemSoundSuspend`、`SystemSoundPrepare`、`SystemSoundEnable [0/1 または on/off]`。`Play` は区切り文字のない名前を resource として解決し、`PlayFile` は指定パスを使います。
- `cp0_signal_audio_api`: `PlayFile path`、`Play asset`、`PlayPause`、`PlayContinue`、`PlayEnd`、`Cap`、`CapPause`、`CapContinue`、`CapEnd`、`CapFileSave path`、`SetCallback`、`Preload`、`VolumeRead`、`VolumeWrite 0..100`、`DefaultSink`(sink 名。実 sink がない backend は code -1 と空 data)、`MuteRead`、`MuteToggle`、`SetSystemSoundNames [最大3名]`、`RegisterSystemSounds [1..32名]`、`SystemSoundPlay 0..2`、`SystemSoundSuspend`、`SystemSoundPrepare`、`SystemSoundEnable [0/1 または on/off]`。`Preload` は最初の音量操作より前にミキサークライアントを読み込みます。`Play` は区切り文字のない名前を resource として解決し、`PlayFile` は指定パスを使います。

### PTY

Expand Down
1 change: 1 addition & 0 deletions docs/cp0_lvgl.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ int cp0_time_ntp_set(int enable);
| `Cap`, `CapPause`, `CapContinue`, `CapEnd` | 无 | 录音开始、暂停、继续、停止 |
| `CapFileSave` | 输出文件 | 保存录音 |
| `SetCallback` | 无 | 设置状态回调 |
| `Preload` | 无 | 预加载音频混音客户端(首次调音量前调用可避免加载延迟);data 为 ok |
| `VolumeRead` | 无 | data 为 0..100 音量 |
| `DefaultSink` | 无 | 成功时 data 为默认 sink 名称;SDL 后端没有真实 sink,返回 code=-1 且 data 为空 |
| `VolumeWrite` | 0..100 | data 为实际值 |
Expand Down
90 changes: 8 additions & 82 deletions ext_components/cp0_lvgl/src/cp0/cp0_audio_mixer_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,109 +6,35 @@

#include "cp0_audio_mixer_control.hpp"

#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdio>
#include <cstring>
#include <string>

bool Cp0AudioMixerControl::parse_volume_line(const char *line, int &percent)
{
if (!line)
return false;
const char *marker = std::strchr(line, '%');
if (!marker)
return false;
const char *start = marker;
while (start > line && start[-1] >= '0' && start[-1] <= '9')
--start;
if (start == marker)
return false;
if (start > line && !std::isspace(static_cast<unsigned char>(start[-1])))
return false;

int parsed = 0;
const auto result = std::from_chars(start, marker, parsed);
if (result.ec != std::errc{} || result.ptr != marker || parsed < 0 || parsed > 100)
return false;
percent = parsed;
return true;
}
#include "cp0_pulse_client.hpp"

std::string Cp0AudioMixerControl::default_sink_name() const
{
FILE *pipe = popen("pactl get-default-sink 2>/dev/null", "r");
if (!pipe)
return {};

char buffer[256] = {};
std::string name;
if (fgets(buffer, sizeof(buffer), pipe)) {
name = buffer;
while (!name.empty() &&
std::isspace(static_cast<unsigned char>(name.back())))
name.pop_back();
}
pclose(pipe);
return name;
return cp0::pulse::default_sink_name(name) ? name : std::string();
}

int Cp0AudioMixerControl::read_volume() const
{
FILE *pipe = popen("pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null", "r");
if (!pipe)
return -1;

char buffer[256];
int volume = -1;
while (fgets(buffer, sizeof(buffer), pipe)) {
if (parse_volume_line(buffer, volume))
break;
}
pclose(pipe);
return volume;
return cp0::pulse::read_default_sink_volume();
}

int Cp0AudioMixerControl::write_volume(int percent) const
{
percent = clamp_percent(percent);
char command[128];
std::snprintf(command, sizeof(command),
"pactl set-sink-volume @DEFAULT_SINK@ %d%%", percent);
return std::system(command) == 0 ? percent : -1;
return cp0::pulse::write_default_sink_volume(percent);
}

int Cp0AudioMixerControl::read_mute() const
{
FILE *pipe = popen("pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null", "r");
if (!pipe)
return -1;

char buffer[128] = {};
int muted = -1;
while (fgets(buffer, sizeof(buffer), pipe)) {
if (std::strstr(buffer, "yes")) {
muted = 1;
break;
}
if (std::strstr(buffer, "no")) {
muted = 0;
break;
}
}
pclose(pipe);
return muted;
return cp0::pulse::read_default_sink_mute();
}

int Cp0AudioMixerControl::toggle_mute() const
{
if (std::system("pactl set-sink-mute @DEFAULT_SINK@ toggle >/dev/null 2>&1") != 0)
return -1;
return read_mute();
return cp0::pulse::toggle_default_sink_mute();
}

int Cp0AudioMixerControl::clamp_percent(int percent)
bool Cp0AudioMixerControl::preload() const
{
return std::max(0, std::min(100, percent));
return cp0::pulse::preload();
}
21 changes: 12 additions & 9 deletions ext_components/cp0_lvgl/src/cp0/cp0_audio_mixer_control.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
class Cp0AudioMixerControl
{
public:
static bool parse_volume_line(const char *line, int &percent);
// The device implementation talks to the PulseAudio-compatible server
// (pipewire-pulse) in process through libpulse.so.0; see
// cp0_pulse_client.cpp. It never spawns a process, so a missing library or
// server simply makes every call return an error.
// "auto_null" is PipeWire's placeholder sink. A volume written to it is
// discarded when the real hardware sink appears, so callers that must
// reach the speaker have to tell the two apart. While no sink exists at
// all, pipewire-pulse reports the special name "@DEFAULT_SINK@" instead.
// Defined inline because shared callers such as commount.cpp are also built
// by the emulator and AppStore targets, which do not compile the pactl
// implementation in cp0_audio_mixer_control.cpp.
// by the emulator and AppStore targets, which do not compile the
// libpulse-backed implementation in cp0_audio_mixer_control.cpp.
static bool is_hardware_sink_name(const char *name)
{
if (!name || !name[0] || name[0] == '@')
Expand All @@ -33,17 +36,17 @@ class Cp0AudioMixerControl
int write_volume(int percent) const;
int read_mute() const;
int toggle_mute() const;

private:
static int clamp_percent(int percent);
// Loads the PulseAudio client library ahead of the first use. Idempotent;
// returns false when the client library is unavailable.
bool preload() const;
};

// Decides one pass of the startup volume restore. The loop itself lives in
// the launcher, but the transitions are the part that usually breaks, so they
// stay pure: no clock, no pactl, no signals.
// stay pure: no clock, no audio server, no signals.
// Defined inline for the same reason as is_hardware_sink_name: commount.cpp is
// built by the emulator and AppStore targets, which do not compile the pactl
// implementation in cp0_audio_mixer_control.cpp.
// built by the emulator and AppStore targets, which do not compile the
// libpulse-backed implementation in cp0_audio_mixer_control.cpp.
struct Cp0VolumeRestoreStep
{
enum class Action
Expand Down
8 changes: 8 additions & 0 deletions ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,13 @@ class AudioSystem
capture_.set_status_callback(callback);
}

void Preload(arg_t arg, callback_t callback)
{
(void)arg;
const bool ready = mixer_.preload();
report(callback, ready ? 0 : -1, ready ? "ok\n" : "pulse unavailable\n");
}

void VolumeRead(arg_t arg, callback_t callback)
{
(void)arg;
Expand Down Expand Up @@ -546,6 +553,7 @@ class AudioSystem
map_fun(CapEnd),
map_fun(CapFileSave),
map_fun(SetCallback),
map_fun(Preload),
map_fun(VolumeRead),
map_fun(DefaultSink),
map_fun(MuteRead),
Expand Down
Loading
Loading