mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
refactor: shared text-entry popup (ui-lvgl) and chat bubble row (ui-new); GPS search clock keeps a found fix
- ui-lvgl: navTextEntry()/TextEntry replace eight copies of popup + field + keyboard wiring; one byte-capped insert callback instead of two. - ui-new: drawHeadedBubble()/BubbleMark replace the duplicated DM and channel bubble drawing in MessagesScreen. - GpsSky::halt(): stopping the receiver ends an unfinished search but keeps the first-fix time; keyboard cursor blink restarts after a key. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VeBrxKyHHxWsmttVkUdBrj
This commit is contained in:
@@ -350,21 +350,12 @@ void UITask::adminValuePopup() {
|
||||
void UITask::adminTextPopup(const char* text, bool digits) {
|
||||
AdminSession& S = _core->admin;
|
||||
const admin::Field* f = S.currentField();
|
||||
lv_obj_t* panel = navPopupPanel(f ? f->label : "Command", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_max_length(_nav_ta, 150);
|
||||
if (digits) lv_textarea_set_accepted_chars(_nav_ta, "0123456789.");
|
||||
if (f && f->isCustom()) lv_textarea_set_placeholder_text(_nav_ta, "e.g. get advert.interval");
|
||||
lv_textarea_set_text(_nav_ta, text);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED);
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
if (digits) kb::apply(_nav_kb, kb::L_SYM);
|
||||
lv_obj_add_event_cb(_nav_kb, onAdminTextKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onAdminTextKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { f ? f->label : "Command", onAdminTextKb };
|
||||
t.text = text;
|
||||
t.max_bytes = 150;
|
||||
if (digits) { t.accepted = "0123456789."; t.symbols = true; }
|
||||
if (f && f->isCustom()) t.hint = "e.g. get advert.interval";
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::adminTextDone(bool ok) {
|
||||
|
||||
@@ -153,13 +153,11 @@ void UITask::botRow(int row) {
|
||||
int cap;
|
||||
if (char* t = botcfg::text(p, r.kind, cap)) {
|
||||
bool reply = !botcfg::isTrigger(r.kind);
|
||||
lv_obj_t* panel = navPopupPanel(r.label, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_max_length(_nav_ta, cap - 1);
|
||||
lv_textarea_set_text(_nav_ta, t);
|
||||
lv_textarea_set_placeholder_text(_nav_ta, botcfg::isTrigger(r.kind) ? "e.g. !hi, or * for any message" : "Reply text");
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED);
|
||||
TextEntry te = { r.label, onBotTextKb };
|
||||
te.text = t;
|
||||
te.max_bytes = cap - 1;
|
||||
te.hint = botcfg::isTrigger(r.kind) ? "e.g. !hi, or * for any message" : "Reply text";
|
||||
lv_obj_t* panel = navTextEntry(te);
|
||||
if (reply) {
|
||||
// Placeholders the bot fills in, as buttons in place of the title (no
|
||||
// room for another row above the keyboard; the field says "Reply").
|
||||
@@ -180,12 +178,6 @@ void UITask::botRow(int row) {
|
||||
if (strcmp(ph, "{batt}") != 0) botChip(ph, c); // already there
|
||||
}, chips);
|
||||
}
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onBotTextKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onBotTextKb, LV_EVENT_CANCEL, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,18 +122,11 @@ void UITask::roomLoginPopup(const uint8_t* pub_key) {
|
||||
ContactInfo ci;
|
||||
if (!MessageHistory::contactByPrefix(pub_key, ci)) return;
|
||||
memcpy(s_login_key, ci.id.pub_key, PUB_KEY_SIZE);
|
||||
lv_obj_t* panel = navPopupPanel(ci.name, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = wifiField(panel, "Room password (may be empty)", true);
|
||||
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
||||
lv_textarea_set_max_length(_nav_ta, 15);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onRoomLoginKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onRoomLoginKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { ci.name, onRoomLoginKb };
|
||||
t.hint = "Room password (may be empty)";
|
||||
t.password = true;
|
||||
t.max_bytes = 15;
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::roomLoginDone(bool ok) {
|
||||
|
||||
@@ -39,14 +39,6 @@ static uint8_t s_pin_key[PUB_KEY_SIZE];
|
||||
|
||||
enum : uint8_t { F_CHANGE, F_REMOVE };
|
||||
|
||||
// Text field whose limit is in UTF-8 bytes (user data: the byte cap).
|
||||
static void onByteCapInsert(lv_event_t* e) {
|
||||
lv_obj_t* ta = (lv_obj_t*)lv_event_get_target(e);
|
||||
const char* ins = (const char*)lv_event_get_param(e);
|
||||
size_t cap = (size_t)(uintptr_t)lv_event_get_user_data(e);
|
||||
if (ins && strlen(lv_textarea_get_text(ta)) + strlen(ins) > cap) lv_textarea_set_insert_replace(ta, "");
|
||||
}
|
||||
|
||||
} // namespace devview
|
||||
|
||||
static void onNodeName(lv_event_t* e) { (void)e; s_ui->nodeNamePopup(); }
|
||||
@@ -116,20 +108,11 @@ static bool pinEdit(char* entry, const char* key) {
|
||||
|
||||
void UITask::nodeNamePopup() {
|
||||
if (!_prefs) return;
|
||||
lv_obj_t* panel = navPopupPanel("Node name", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = wifiField(panel, "Shown to others in adverts", false);
|
||||
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
||||
lv_textarea_set_text(_nav_ta, the_mesh.getNodeName());
|
||||
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT,
|
||||
(void*)(uintptr_t)(sizeof(_prefs->node_name) - 1));
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onNodeNameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onNodeNameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Node name", onNodeNameKb };
|
||||
t.hint = "Shown to others in adverts";
|
||||
t.text = the_mesh.getNodeName();
|
||||
t.max_bytes = sizeof(_prefs->node_name) - 1;
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::nodeNameDone(bool ok) {
|
||||
|
||||
@@ -155,6 +155,7 @@ static const char* s_var_map[13];
|
||||
// on another object) can take that state away: put it back after a pick.
|
||||
static void keepCursor(lv_obj_t* ta) {
|
||||
lv_obj_add_state(ta, LV_STATE_FOCUSED);
|
||||
lv_textarea_set_cursor_pos(ta, lv_textarea_get_cursor_pos(ta)); // restarts the blink (see fieldFocus)
|
||||
lv_obj_invalidate(ta);
|
||||
}
|
||||
|
||||
|
||||
@@ -452,18 +452,10 @@ void UITask::mapAreaAction(uint8_t act) {
|
||||
bool trails = m.flags & mapview::AreaStore::F_TRAILS;
|
||||
switch (act) {
|
||||
case A_RENAME: {
|
||||
lv_obj_t* panel = navPopupPanel("Area name", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_max_length(_nav_ta, sizeof(m.name) - 1);
|
||||
lv_textarea_set_text(_nav_ta, m.name);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED);
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onRenameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onRenameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Area name", onRenameKb };
|
||||
t.text = m.name;
|
||||
t.max_bytes = sizeof(m.name) - 1;
|
||||
navTextEntry(t);
|
||||
return;
|
||||
}
|
||||
case A_FILL:
|
||||
|
||||
@@ -92,15 +92,6 @@ static void onTrailDraw(lv_event_t* e) {
|
||||
lv_draw_line(d.base.layer, &d);
|
||||
}
|
||||
|
||||
// Waypoint labels are 11 bytes of UTF-8 (Waypoint.h): reject input past that,
|
||||
// so a two-byte letter is never cut in half.
|
||||
static void onLabelInsert(lv_event_t* e) {
|
||||
lv_obj_t* ta = (lv_obj_t*)lv_event_get_target(e);
|
||||
const char* ins = (const char*)lv_event_get_param(e);
|
||||
if (ins && strlen(lv_textarea_get_text(ta)) + strlen(ins) > WAYPOINT_LABEL_LEN - 1)
|
||||
lv_textarea_set_insert_replace(ta, "");
|
||||
}
|
||||
|
||||
} // namespace navmap
|
||||
|
||||
static void onNavList(lv_event_t* e) { (void)e; s_ui->navTargetsPopup(); }
|
||||
@@ -603,6 +594,24 @@ void UITask::navClosePopup() {
|
||||
_nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr;
|
||||
}
|
||||
|
||||
lv_obj_t* UITask::navTextEntry(const TextEntry& t) {
|
||||
lv_obj_t* panel = navPopupPanel(t.title, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel, t.hint);
|
||||
if (t.password) lv_textarea_set_password_mode(_nav_ta, true);
|
||||
if (t.accepted) lv_textarea_set_accepted_chars(_nav_ta, t.accepted);
|
||||
if (t.text) lv_textarea_set_text(_nav_ta, t.text); // before the cap: a longer stored text is shown, not wiped
|
||||
if (t.max_bytes) lv_obj_add_event_cb(_nav_ta, onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)t.max_bytes);
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta); // focuses the field: the cursor
|
||||
if (t.symbols) kb::apply(_nav_kb, kb::L_SYM);
|
||||
lv_obj_add_event_cb(_nav_kb, t.done, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, t.done, LV_EVENT_CANCEL, NULL);
|
||||
return panel;
|
||||
}
|
||||
|
||||
static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right) {
|
||||
lv_obj_align(label(row, text, THEME_FONT_SMALL, col), LV_ALIGN_RIGHT_MID, -right, 0);
|
||||
}
|
||||
@@ -742,19 +751,10 @@ void UITask::navWaypointAction(uint8_t act) {
|
||||
|
||||
void UITask::navRenamePopup(int idx) {
|
||||
_nav_wp = idx;
|
||||
lv_obj_t* panel = navPopupPanel("Waypoint name", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_text(_nav_ta, _core->waypoints.at(idx).label);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
lv_obj_add_event_cb(_nav_ta, navmap::onLabelInsert, LV_EVENT_INSERT, NULL);
|
||||
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onNavRenameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onNavRenameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Waypoint name", onNavRenameKb };
|
||||
t.text = _core->waypoints.at(idx).label;
|
||||
t.max_bytes = WAYPOINT_LABEL_LEN - 1;
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::navRenameDone(bool ok) {
|
||||
@@ -1460,19 +1460,10 @@ void UITask::navWaypointsPopup() {
|
||||
// name is optional) -> a new waypoint.
|
||||
void UITask::navCoordsPopup() {
|
||||
if (_core->waypoints.full()) { showToast(waypointsFull()); return; }
|
||||
lv_obj_t* panel = navPopupPanel("Add by coordinates", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_placeholder_text(_nav_ta, "50.06142, 19.93721 Name");
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
kb::apply(_nav_kb, kb::L_SYM); // digits first; "abc" gives letters for the name
|
||||
lv_obj_add_event_cb(_nav_kb, onNavCoordsKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onNavCoordsKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { "Add by coordinates", onNavCoordsKb };
|
||||
t.hint = "50.06142, 19.93721 Name";
|
||||
t.symbols = true; // digits first; "abc" gives letters for the name
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::navCoordsDone(bool ok) {
|
||||
|
||||
@@ -151,7 +151,7 @@ void UITask::quickEdit(int slot) {
|
||||
lv_obj_set_width(_nav_ta, w);
|
||||
lv_obj_set_pos(_nav_ta, 8, theme::STATUS_H + 4);
|
||||
lv_textarea_set_text(_nav_ta, msgtext::quick(_prefs, slot));
|
||||
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)(msgtext::QUICK_LEN - 1));
|
||||
lv_obj_add_event_cb(_nav_ta, onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)(msgtext::QUICK_LEN - 1));
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
|
||||
lv_obj_t* row = chips(_nav_overlay, onQuickEditPh, false);
|
||||
|
||||
@@ -112,19 +112,11 @@ void UITask::radioNamePopup(uint8_t what) {
|
||||
const char* title = what == N_PRESET ? "Preset name" : what == N_SCOPE_ADD ? "New scope" : "Scope name";
|
||||
const char* hint = what == N_PRESET ? "e.g. Hiking" : "Region name, e.g. pl-maz";
|
||||
size_t cap = what == N_PRESET ? sizeof(_prefs->user_radio_presets[0].name) - 1 : sizeof(ScopeEntry::name) - 1;
|
||||
lv_obj_t* panel = navPopupPanel(title, false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = wifiField(panel, hint, false);
|
||||
lv_obj_remove_event_cb(_nav_ta, onWifiField);
|
||||
if (what == N_SCOPE_RENAME) lv_textarea_set_text(_nav_ta, the_mesh.scopeList().name((uint8_t)s_scope));
|
||||
lv_obj_add_event_cb(_nav_ta, devview::onByteCapInsert, LV_EVENT_INSERT, (void*)(uintptr_t)cap);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onRadioNameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onRadioNameKb, LV_EVENT_CANCEL, NULL);
|
||||
TextEntry t = { title, onRadioNameKb };
|
||||
t.hint = hint;
|
||||
t.max_bytes = cap;
|
||||
if (what == N_SCOPE_RENAME) t.text = the_mesh.scopeList().name((uint8_t)s_scope);
|
||||
navTextEntry(t);
|
||||
}
|
||||
|
||||
void UITask::radioNameDone(bool ok) {
|
||||
|
||||
@@ -1056,6 +1056,15 @@ static void fieldFocus(lv_obj_t* ta, bool on) {
|
||||
lv_textarea_set_cursor_pos(ta, lv_textarea_get_cursor_pos(ta));
|
||||
}
|
||||
|
||||
// A field limited in UTF-8 bytes (user data: the cap) -- names and other text
|
||||
// that is stored in a fixed buffer. LVGL's own limit counts characters.
|
||||
static void onByteCapInsert(lv_event_t* e) {
|
||||
lv_obj_t* ta = (lv_obj_t*)lv_event_get_target(e);
|
||||
const char* ins = (const char*)lv_event_get_param(e);
|
||||
size_t cap = (size_t)(uintptr_t)lv_event_get_user_data(e);
|
||||
if (ins && strlen(lv_textarea_get_text(ta)) + strlen(ins) > cap) lv_textarea_set_insert_replace(ta, "");
|
||||
}
|
||||
|
||||
// The primary action of a screen or popup: full amber, dark text (Theme.h);
|
||||
// call after its label is made.
|
||||
static void stylePrimary(lv_obj_t* b) {
|
||||
|
||||
@@ -24,6 +24,20 @@ struct UiEvent;
|
||||
|
||||
namespace mapview { struct TileArea; }
|
||||
|
||||
// A popup that asks for one line of text: the field over the keyboard. `done`
|
||||
// gets the keyboard's READY (OK) and CANCEL; the field is _nav_ta, the keyboard
|
||||
// _nav_kb, until navClosePopup().
|
||||
struct TextEntry {
|
||||
const char* title;
|
||||
lv_event_cb_t done;
|
||||
const char* text = nullptr; // what the field starts with
|
||||
const char* hint = nullptr; // placeholder
|
||||
size_t max_bytes = 0; // UTF-8 bytes the field takes; 0 = no limit
|
||||
bool password = false;
|
||||
const char* accepted = nullptr; // only these characters (lv_textarea_set_accepted_chars)
|
||||
bool symbols = false; // open on the digits / symbols page
|
||||
};
|
||||
|
||||
class UITask : public UITaskBase, public UiCoreHost {
|
||||
public:
|
||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial) {}
|
||||
@@ -385,6 +399,7 @@ private:
|
||||
void navFrameTarget();
|
||||
void navSetTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name);
|
||||
lv_obj_t* navPopupPanel(const char* title, bool full, bool bottom = false);
|
||||
lv_obj_t* navTextEntry(const TextEntry& t); // popup + one-line field + keyboard (NavMap.h)
|
||||
void navRenamePopup(int idx);
|
||||
void navAddWaypoint(int32_t lat, int32_t lon);
|
||||
void navDropAt(int x, int y);
|
||||
|
||||
Reference in New Issue
Block a user