updated accidental regression in settings - readded autoadd contacts settings; added wifi on off option to settings

This commit is contained in:
pelgraine
2026-03-03 23:17:41 +11:00
parent 3a6673edea
commit d92fdc9ffe
3 changed files with 343 additions and 85 deletions
+46 -30
View File
@@ -202,7 +202,10 @@
// Returns number of contacts exported, or -1 on error.
// -----------------------------------------------------------------------
int exportContactsToSD() {
if (!sdCardReady) return -1;
if (!sdCardReady) {
Serial.println("Export: SD card not ready");
return -1;
}
// Ensure in-memory contacts are flushed to SPIFFS first
the_mesh.saveContacts();
@@ -210,40 +213,53 @@
if (!SD.exists("/meshcore")) SD.mkdir("/meshcore");
// 1) Binary backup: SPIFFS /contacts3 → SD /meshcore/contacts.bin
if (!SPIFFS.exists("/contacts3")) return -1;
if (!copyFile(SPIFFS, "/contacts3", SD, "/meshcore/contacts.bin")) return -1;
// Non-fatal — text export reads from memory and doesn't need this.
if (SPIFFS.exists("/contacts3")) {
if (copyFile(SPIFFS, "/contacts3", SD, "/meshcore/contacts.bin")) {
Serial.println("Export: binary backup OK");
} else {
Serial.println("Export: binary copy to SD failed (continuing with text export)");
}
} else {
Serial.println("Export: /contacts3 not found on SPIFFS (skipping binary backup)");
}
// 2) Human-readable listing for inspection on a computer
// Reads from in-memory contact table — always works if SD is writable.
int count = 0;
File txt = SD.open("/meshcore/contacts_export.txt", "w", true);
if (txt) {
txt.printf("Meck Contacts Export (%d total)\n", (int)the_mesh.getNumContacts());
txt.printf("========================================\n");
txt.printf("%-5s %-30s %s\n", "Type", "Name", "PubKey (prefix)");
txt.printf("----------------------------------------\n");
ContactInfo c;
for (uint32_t i = 0; i < (uint32_t)the_mesh.getNumContacts(); i++) {
if (the_mesh.getContactByIdx(i, c)) {
const char* typeStr = "???";
switch (c.type) {
case ADV_TYPE_CHAT: typeStr = "Chat"; break;
case ADV_TYPE_REPEATER: typeStr = "Rptr"; break;
case ADV_TYPE_ROOM: typeStr = "Room"; break;
}
// First 8 bytes of pub key as hex identifier
char hexBuf[20];
mesh::Utils::toHex(hexBuf, c.id.pub_key, 8);
txt.printf("%-5s %-30s %s\n", typeStr, c.name, hexBuf);
count++;
}
}
txt.printf("========================================\n");
txt.printf("Total: %d contacts\n", count);
txt.close();
if (!txt) {
Serial.println("Export: failed to open contacts_export.txt for writing");
digitalWrite(SDCARD_CS, HIGH);
return -1;
}
txt.printf("Meck Contacts Export (%d total)\n", (int)the_mesh.getNumContacts());
txt.printf("========================================\n");
txt.printf("%-5s %-30s %s\n", "Type", "Name", "PubKey (prefix)");
txt.printf("----------------------------------------\n");
ContactInfo c;
for (uint32_t i = 0; i < (uint32_t)the_mesh.getNumContacts(); i++) {
if (the_mesh.getContactByIdx(i, c)) {
const char* typeStr = "???";
switch (c.type) {
case ADV_TYPE_CHAT: typeStr = "Chat"; break;
case ADV_TYPE_REPEATER: typeStr = "Rptr"; break;
case ADV_TYPE_ROOM: typeStr = "Room"; break;
}
// First 8 bytes of pub key as hex identifier
char hexBuf[20];
mesh::Utils::toHex(hexBuf, c.id.pub_key, 8);
txt.printf("%-5s %-30s %s\n", typeStr, c.name, hexBuf);
count++;
}
}
txt.printf("========================================\n");
txt.printf("Total: %d contacts\n", count);
txt.close();
digitalWrite(SDCARD_CS, HIGH);
Serial.printf("Contacts exported to SD: %d contacts\n", count);
return count;
@@ -1981,7 +1997,7 @@ void handleKeyboardInput() {
snprintf(alertBuf, sizeof(alertBuf), "Exported %d to SD", exported);
ui_task.showAlert(alertBuf, 2000);
} else {
ui_task.showAlert("Export failed (no SD?)", 2000);
ui_task.showAlert("Export failed (check serial)", 2000);
}
}
break;
+292 -53
View File
@@ -20,6 +20,25 @@ class UITask;
class MyMesh;
extern MyMesh the_mesh;
// ---------------------------------------------------------------------------
// Auto-add config bitmask (mirrored from MyMesh.cpp for UI access)
// ---------------------------------------------------------------------------
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full
#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
// All type bits combined (excludes overwrite flag)
#define AUTO_ADD_ALL_TYPES (AUTO_ADD_CHAT | AUTO_ADD_REPEATER | \
AUTO_ADD_ROOM_SERVER | AUTO_ADD_SENSOR)
// Contact mode indices for picker
#define CONTACT_MODE_AUTO_ALL 0 // Add all contacts automatically
#define CONTACT_MODE_CUSTOM 1 // Per-type toggles
#define CONTACT_MODE_MANUAL 2 // No auto-add, companion app only
#define CONTACT_MODE_COUNT 3
// ---------------------------------------------------------------------------
// Radio presets
// ---------------------------------------------------------------------------
@@ -67,11 +86,19 @@ enum SettingsRowType : uint8_t {
ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle
#ifdef MECK_WIFI_COMPANION
ROW_WIFI_SETUP, // WiFi SSID/password configuration
ROW_WIFI_TOGGLE, // WiFi radio on/off toggle
#endif
#ifdef HAS_4G_MODEM
ROW_MODEM_TOGGLE, // 4G modem enable/disable toggle (4G builds only)
// ROW_RINGTONE, // Incoming call ringtone toggle (4G builds only)
#endif
ROW_CONTACT_HEADER, // "--- Contacts ---" separator
ROW_CONTACT_MODE, // Contact auto-add mode picker (Auto All / Custom / Manual)
ROW_AUTOADD_CHAT, // Toggle: auto-add Chat clients
ROW_AUTOADD_REPEATER,// Toggle: auto-add Repeaters
ROW_AUTOADD_ROOM, // Toggle: auto-add Room Servers
ROW_AUTOADD_SENSOR, // Toggle: auto-add Sensors
ROW_AUTOADD_OVERWRITE, // Toggle: overwrite oldest non-favourite when full
ROW_CH_HEADER, // "--- Channels ---" separator
ROW_CHANNEL, // A channel entry (dynamic, index stored separately)
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
@@ -91,7 +118,7 @@ enum SettingsRowType : uint8_t {
enum EditMode : uint8_t {
EDIT_NONE, // Just browsing
EDIT_TEXT, // Typing into a text buffer (name, channel name)
EDIT_PICKER, // A/D cycles options (radio preset)
EDIT_PICKER, // A/D cycles options (radio preset, contact mode)
EDIT_NUMBER, // W/S adjusts value (freq, BW, SF, CR, TX, UTC)
EDIT_CONFIRM, // Confirmation dialog (delete channel, apply radio)
#ifdef MECK_WIFI_COMPANION
@@ -99,15 +126,15 @@ enum EditMode : uint8_t {
#endif
};
// Max rows in the settings list
// Max rows in the settings list (increased for contact sub-toggles + WiFi)
#if defined(HAS_4G_MODEM) && defined(MECK_WIFI_COMPANION)
#define SETTINGS_MAX_ROWS 48 // Extra rows for IMEI, Carrier, APN, WiFi
#define SETTINGS_MAX_ROWS 56 // Extra rows for IMEI, Carrier, APN, contacts, WiFi
#elif defined(HAS_4G_MODEM)
#define SETTINGS_MAX_ROWS 46 // Extra rows for IMEI, Carrier, APN
#define SETTINGS_MAX_ROWS 54 // Extra rows for IMEI, Carrier, APN + contacts
#elif defined(MECK_WIFI_COMPANION)
#define SETTINGS_MAX_ROWS 42 // Extra row for WiFi
#define SETTINGS_MAX_ROWS 50 // Extra rows for contacts + WiFi
#else
#define SETTINGS_MAX_ROWS 40
#define SETTINGS_MAX_ROWS 48 // Contacts section
#endif
#define SETTINGS_TEXT_BUF 33 // 32 chars + null
@@ -133,7 +160,7 @@ private:
EditMode _editMode;
char _editBuf[SETTINGS_TEXT_BUF];
int _editPos;
int _editPickerIdx; // for preset picker
int _editPickerIdx; // for preset picker / contact mode picker
float _editFloat; // for freq/BW editing
int _editInt; // for SF/CR/TX/UTC editing
int _confirmAction; // 0=none, 1=delete channel, 2=apply radio
@@ -167,6 +194,57 @@ private:
unsigned long _wifiFormLastChar; // For brief password reveal
#endif
// ---------------------------------------------------------------------------
// Contact mode helpers
// ---------------------------------------------------------------------------
// Determine current contact mode from prefs
int getContactMode() const {
if ((_prefs->manual_add_contacts & 1) == 0) {
return CONTACT_MODE_AUTO_ALL;
}
// manual_add_contacts bit 0 is set — check if any type bits are enabled
if ((_prefs->autoadd_config & AUTO_ADD_ALL_TYPES) != 0) {
return CONTACT_MODE_CUSTOM;
}
return CONTACT_MODE_MANUAL;
}
// Get display label for a contact mode
static const char* contactModeLabel(int mode) {
switch (mode) {
case CONTACT_MODE_AUTO_ALL: return "Auto All";
case CONTACT_MODE_CUSTOM: return "Custom";
case CONTACT_MODE_MANUAL: return "Manual Only";
default: return "?";
}
}
// Apply a contact mode selection from picker
void applyContactMode(int mode) {
switch (mode) {
case CONTACT_MODE_AUTO_ALL:
_prefs->manual_add_contacts &= ~1; // clear bit 0 → auto all
break;
case CONTACT_MODE_CUSTOM:
_prefs->manual_add_contacts |= 1; // set bit 0 → selective
// If no type bits are set, default to all types enabled
if ((_prefs->autoadd_config & AUTO_ADD_ALL_TYPES) == 0) {
_prefs->autoadd_config |= AUTO_ADD_ALL_TYPES;
}
break;
case CONTACT_MODE_MANUAL:
_prefs->manual_add_contacts |= 1; // set bit 0 → selective
_prefs->autoadd_config &= ~AUTO_ADD_ALL_TYPES; // clear all type bits
// Note: keeps AUTO_ADD_OVERWRITE_OLDEST bit unchanged
break;
}
the_mesh.savePrefs();
rebuildRows(); // show/hide sub-toggles
Serial.printf("Settings: Contact mode = %s (manual=%d, autoadd=0x%02X)\n",
contactModeLabel(mode), _prefs->manual_add_contacts, _prefs->autoadd_config);
}
// ---------------------------------------------------------------------------
// Row table management
// ---------------------------------------------------------------------------
@@ -185,11 +263,27 @@ private:
addRow(ROW_MSG_NOTIFY);
#ifdef MECK_WIFI_COMPANION
addRow(ROW_WIFI_SETUP);
addRow(ROW_WIFI_TOGGLE);
#endif
#ifdef HAS_4G_MODEM
addRow(ROW_MODEM_TOGGLE);
// addRow(ROW_RINGTONE);
#endif
// --- Contacts section ---
addRow(ROW_CONTACT_HEADER);
addRow(ROW_CONTACT_MODE);
// Show per-type sub-toggles only in Custom mode
if (getContactMode() == CONTACT_MODE_CUSTOM) {
addRow(ROW_AUTOADD_CHAT);
addRow(ROW_AUTOADD_REPEATER);
addRow(ROW_AUTOADD_ROOM);
addRow(ROW_AUTOADD_SENSOR);
addRow(ROW_AUTOADD_OVERWRITE);
}
// --- Channels section ---
addRow(ROW_CH_HEADER);
// Enumerate current channels
@@ -230,7 +324,7 @@ private:
bool isSelectable(int idx) const {
if (idx < 0 || idx >= _numRows) return false;
SettingsRowType t = _rows[idx].type;
return t != ROW_CH_HEADER && t != ROW_INFO_HEADER
return t != ROW_CH_HEADER && t != ROW_INFO_HEADER && t != ROW_CONTACT_HEADER
#ifdef HAS_4G_MODEM
&& t != ROW_IMEI && t != ROW_OPERATOR_INFO
#endif
@@ -565,6 +659,11 @@ public:
}
display.print(tmp);
break;
case ROW_WIFI_TOGGLE:
snprintf(tmp, sizeof(tmp), "WiFi Radio: %s",
(WiFi.getMode() != WIFI_OFF) ? "ON" : "OFF");
display.print(tmp);
break;
#endif
#ifdef HAS_4G_MODEM
@@ -581,6 +680,54 @@ public:
// break;
#endif
// --- Contacts section ---
case ROW_CONTACT_HEADER:
display.setColor(DisplayDriver::YELLOW);
display.print("--- Contacts ---");
break;
case ROW_CONTACT_MODE:
if (editing && _editMode == EDIT_PICKER) {
snprintf(tmp, sizeof(tmp), "< Add Mode: %s >",
contactModeLabel(_editPickerIdx));
} else {
snprintf(tmp, sizeof(tmp), "Add Mode: %s",
contactModeLabel(getContactMode()));
}
display.print(tmp);
break;
case ROW_AUTOADD_CHAT:
snprintf(tmp, sizeof(tmp), " Chat: %s",
(_prefs->autoadd_config & AUTO_ADD_CHAT) ? "ON" : "OFF");
display.print(tmp);
break;
case ROW_AUTOADD_REPEATER:
snprintf(tmp, sizeof(tmp), " Repeater: %s",
(_prefs->autoadd_config & AUTO_ADD_REPEATER) ? "ON" : "OFF");
display.print(tmp);
break;
case ROW_AUTOADD_ROOM:
snprintf(tmp, sizeof(tmp), " Room Server: %s",
(_prefs->autoadd_config & AUTO_ADD_ROOM_SERVER) ? "ON" : "OFF");
display.print(tmp);
break;
case ROW_AUTOADD_SENSOR:
snprintf(tmp, sizeof(tmp), " Sensor: %s",
(_prefs->autoadd_config & AUTO_ADD_SENSOR) ? "ON" : "OFF");
display.print(tmp);
break;
case ROW_AUTOADD_OVERWRITE:
snprintf(tmp, sizeof(tmp), " Overwrite Oldest: %s",
(_prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "ON" : "OFF");
display.print(tmp);
break;
// --- Channels section ---
case ROW_CH_HEADER:
display.setColor(DisplayDriver::YELLOW);
display.print("--- Channels ---");
@@ -1011,62 +1158,81 @@ public:
return true; // consume all keys in text edit
}
// --- Picker mode (radio preset) ---
// --- Picker mode (radio preset or contact mode) ---
if (_editMode == EDIT_PICKER) {
SettingsRowType type = _rows[_cursor].type;
if (c == 'a' || c == 'A') {
_editPickerIdx--;
if (_editPickerIdx < 0) _editPickerIdx = (int)NUM_RADIO_PRESETS - 1;
if (type == ROW_CONTACT_MODE) {
_editPickerIdx--;
if (_editPickerIdx < 0) _editPickerIdx = CONTACT_MODE_COUNT - 1;
} else {
// Radio preset
_editPickerIdx--;
if (_editPickerIdx < 0) _editPickerIdx = (int)NUM_RADIO_PRESETS - 1;
}
return true;
}
if (c == 'd' || c == 'D') {
_editPickerIdx++;
if (_editPickerIdx >= (int)NUM_RADIO_PRESETS) _editPickerIdx = 0;
if (type == ROW_CONTACT_MODE) {
_editPickerIdx++;
if (_editPickerIdx >= CONTACT_MODE_COUNT) _editPickerIdx = 0;
} else {
// Radio preset
_editPickerIdx++;
if (_editPickerIdx >= (int)NUM_RADIO_PRESETS) _editPickerIdx = 0;
}
return true;
}
if (c == '\r' || c == 13) {
// Apply preset
if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) {
const RadioPreset& p = RADIO_PRESETS[_editPickerIdx];
_prefs->freq = p.freq;
_prefs->bw = p.bw;
_prefs->sf = p.sf;
_prefs->cr = p.cr;
_prefs->tx_power_dbm = p.tx_power;
_radioChanged = true;
}
_editMode = EDIT_NONE;
if (_onboarding) {
applyRadioParams();
#ifdef MECK_WIFI_COMPANION
// Move to WiFi setup before finishing onboarding
for (int r = 0; r < _numRows; r++) {
if (_rows[r].type == ROW_WIFI_SETUP) {
_cursor = r;
break;
}
if (type == ROW_CONTACT_MODE) {
applyContactMode(_editPickerIdx);
_editMode = EDIT_NONE;
} else {
// Apply radio preset
if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) {
const RadioPreset& p = RADIO_PRESETS[_editPickerIdx];
_prefs->freq = p.freq;
_prefs->bw = p.bw;
_prefs->sf = p.sf;
_prefs->cr = p.cr;
_prefs->tx_power_dbm = p.tx_power;
_radioChanged = true;
}
// Auto-launch the WiFi scan
_editMode = EDIT_WIFI;
_wifiPhase = WIFI_PHASE_SCANNING;
_wifiSSIDCount = 0;
_wifiSSIDSelected = 0;
WiFi.mode(WIFI_STA);
int n = WiFi.scanNetworks();
if (n > 0) {
_wifiSSIDCount = min(n, 10);
for (int si = 0; si < _wifiSSIDCount; si++) {
_wifiSSIDs[si] = WiFi.SSID(si);
_editMode = EDIT_NONE;
if (_onboarding) {
applyRadioParams();
#ifdef MECK_WIFI_COMPANION
// Move to WiFi setup before finishing onboarding
for (int r = 0; r < _numRows; r++) {
if (_rows[r].type == ROW_WIFI_SETUP) {
_cursor = r;
break;
}
}
_wifiPhase = WIFI_PHASE_SELECT;
} else {
_editMode = EDIT_NONE;
_wifiPhase = WIFI_PHASE_IDLE;
_onboarding = false; // Finish even without WiFi
// Auto-launch the WiFi scan
_editMode = EDIT_WIFI;
_wifiPhase = WIFI_PHASE_SCANNING;
_wifiSSIDCount = 0;
_wifiSSIDSelected = 0;
WiFi.mode(WIFI_STA);
int n = WiFi.scanNetworks();
if (n > 0) {
_wifiSSIDCount = min(n, 10);
for (int si = 0; si < _wifiSSIDCount; si++) {
_wifiSSIDs[si] = WiFi.SSID(si);
}
_wifiPhase = WIFI_PHASE_SELECT;
} else {
_editMode = EDIT_NONE;
_wifiPhase = WIFI_PHASE_IDLE;
_onboarding = false; // Finish even without WiFi
}
WiFi.scanDelete();
#else
_onboarding = false;
#endif
}
WiFi.scanDelete();
#else
_onboarding = false;
#endif
}
return true;
}
@@ -1234,6 +1400,41 @@ public:
WiFi.scanDelete();
break;
}
case ROW_WIFI_TOGGLE:
if (WiFi.getMode() != WIFI_OFF) {
// Turn WiFi OFF
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
Serial.println("Settings: WiFi radio OFF");
} else {
// Turn WiFi ON — reconnect using saved credentials
WiFi.mode(WIFI_STA);
if (SD.exists("/web/wifi.cfg")) {
File f = SD.open("/web/wifi.cfg", FILE_READ);
if (f) {
String ssid = f.readStringUntil('\n'); ssid.trim();
String pass = f.readStringUntil('\n'); pass.trim();
f.close();
digitalWrite(SDCARD_CS, HIGH);
if (ssid.length() > 0) {
WiFi.begin(ssid.c_str(), pass.c_str());
unsigned long timeout = millis() + 8000;
while (WiFi.status() != WL_CONNECTED && millis() < timeout) {
delay(100);
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("Settings: WiFi ON, connected to %s\n", ssid.c_str());
} else {
Serial.println("Settings: WiFi ON, but connection failed");
}
}
} else {
digitalWrite(SDCARD_CS, HIGH);
}
}
Serial.println("Settings: WiFi radio ON");
}
break;
#endif
#ifdef HAS_4G_MODEM
case ROW_MODEM_TOGGLE:
@@ -1261,6 +1462,44 @@ public:
break;
}
#endif
// --- Contact mode picker ---
case ROW_CONTACT_MODE:
startEditPicker(getContactMode());
break;
// --- Contact sub-toggles (flip bit and save) ---
case ROW_AUTOADD_CHAT:
_prefs->autoadd_config ^= AUTO_ADD_CHAT;
the_mesh.savePrefs();
Serial.printf("Settings: Auto-add Chat = %s\n",
(_prefs->autoadd_config & AUTO_ADD_CHAT) ? "ON" : "OFF");
break;
case ROW_AUTOADD_REPEATER:
_prefs->autoadd_config ^= AUTO_ADD_REPEATER;
the_mesh.savePrefs();
Serial.printf("Settings: Auto-add Repeater = %s\n",
(_prefs->autoadd_config & AUTO_ADD_REPEATER) ? "ON" : "OFF");
break;
case ROW_AUTOADD_ROOM:
_prefs->autoadd_config ^= AUTO_ADD_ROOM_SERVER;
the_mesh.savePrefs();
Serial.printf("Settings: Auto-add Room = %s\n",
(_prefs->autoadd_config & AUTO_ADD_ROOM_SERVER) ? "ON" : "OFF");
break;
case ROW_AUTOADD_SENSOR:
_prefs->autoadd_config ^= AUTO_ADD_SENSOR;
the_mesh.savePrefs();
Serial.printf("Settings: Auto-add Sensor = %s\n",
(_prefs->autoadd_config & AUTO_ADD_SENSOR) ? "ON" : "OFF");
break;
case ROW_AUTOADD_OVERWRITE:
_prefs->autoadd_config ^= AUTO_ADD_OVERWRITE_OLDEST;
the_mesh.savePrefs();
Serial.printf("Settings: Overwrite oldest = %s\n",
(_prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "ON" : "OFF");
break;
case ROW_ADD_CHANNEL:
startEditText("");
break;
+5 -2
View File
@@ -233,8 +233,10 @@ lib_deps =
${LilyGo_TDeck_Pro.lib_deps}
densaugeo/base64 @ ~1.4.0
; 4G standalone (audio-player hardware, no BLE/WiFi — maximum battery life)
; No MECK_WEB_READER: WiFi power draw conflicts with zero-radio-power design.
; 4G standalone (4G modem hardware, no BLE — maximum battery + cellular features)
; No BLE_PIN_CODE: BLE never initializes, saving ~30KB heap + radio power.
; MECK_WEB_READER enabled: works better without BLE (no teardown dance needed,
; more free heap from boot). WiFi-first with cellular PPP fallback (future).
; Contacts and sort arrays allocated in PSRAM — 1500 contacts uses ~290KB of 8MB.
[env:meck_4g_standalone]
extends = LilyGo_TDeck_Pro
@@ -245,6 +247,7 @@ build_flags =
-D MAX_GROUP_CHANNELS=20
-D OFFLINE_QUEUE_SIZE=256
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D FIRMWARE_VERSION='"Meck v0.9.8.4G.SA"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>