Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4df391a283 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
# beta_42 (TEST) - fixes for the beta_41 REMOTE mode: it works on the Heltec V4 now, and keyboard-less boards can leave it.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Fixed ---
|
||||
REMOTE mode now works on the Heltec V4 TFT (and other 2 MB-PSRAM boards) instead of showing a black screen in the browser. Its off-screen renderer needed more free memory than those boards had, so nothing was streamed; the buffers are sized to fit now, and the render is a bit smaller and faster as a result. VNC was never affected and already worked.
|
||||
Keyboard-less touch boards (Heltec V4, RAK WisMesh Tap) can leave REMOTE mode again: touch and hold the device screen for about 3 seconds. Those boards have no SPACE key, which used to be the only way out on the device.
|
||||
|
||||
# --- New ---
|
||||
The web page has an "Exit remote" button (with a confirmation) that leaves REMOTE mode from any browser, on any board.
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
void setScreenSize(int w, int h) { _scr_w = (uint16_t)w; _scr_h = (uint16_t)h; }
|
||||
uint16_t screenW() const { return _scr_w; }
|
||||
uint16_t screenH() const { return _scr_h; }
|
||||
size_t ringBytes() const { return _cap; } // 0 = ring alloc failed (feature can't stream)
|
||||
|
||||
// True only when we should capture/stream: opted in, ring ready, >=1 client.
|
||||
// The LVGL flush hook checks this first, so the feature costs one bool load
|
||||
@@ -72,6 +73,11 @@ public:
|
||||
void requestOrient(uint8_t v) { _orient_req = v; } // 1 = landscape, 2 = portrait, 0 = none
|
||||
uint8_t takeOrient() { uint8_t v = _orient_req; _orient_req = 0; return v; }
|
||||
|
||||
// ---- exit-remote request (browser -> device): the Exit button leaves remote mode.
|
||||
// Board-agnostic way out for keyboard-less boards (V4/RAK). ----
|
||||
void requestExit() { _exit_req = true; }
|
||||
bool takeExit() { bool v = _exit_req; _exit_req = false; return v; }
|
||||
|
||||
private:
|
||||
uint8_t* _ring = nullptr;
|
||||
size_t _cap = 0;
|
||||
@@ -91,6 +97,7 @@ private:
|
||||
volatile bool _kb_focused = false, _kb_dirty = false; // editable-field focus signal
|
||||
volatile bool _remote = false; // remote mode -> browser shows the Rotate button
|
||||
volatile uint8_t _orient_req = 0; // browser-requested orientation (1=landscape, 2=portrait, 0=none)
|
||||
volatile bool _exit_req = false; // browser asked to leave remote mode
|
||||
};
|
||||
|
||||
extern WebMirror g_web_mirror;
|
||||
@@ -42,6 +42,8 @@ static const char WS_HTTP_INFO_PAGE[] =
|
||||
"#kb:active{background:#19d6c2;color:#000;border-color:#19d6c2}\n"
|
||||
"#rot{margin-top:6px;padding:7px 15px;font-size:12px;background:#141416;color:#c8ccce;border:1px solid #2a2a2e;border-radius:8px;display:none}\n"
|
||||
"#rot:active{background:#19d6c2;color:#000;border-color:#19d6c2}\n"
|
||||
"#xit{margin-top:6px;padding:7px 15px;font-size:12px;background:#1e1416;color:#e0a6a6;border:1px solid #4a2a2e;border-radius:8px;display:none}\n"
|
||||
"#xit:active{background:#ff6b6b;color:#000;border-color:#ff6b6b}\n"
|
||||
"#k{position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;border:0;padding:0;font-size:16px}\n"
|
||||
"#hdr{position:fixed;top:14px;left:18px;display:none;align-items:center;gap:11px;z-index:5}\n"
|
||||
"#hdr .wm{font-family:'JetBrains Mono','Courier New',monospace;font-weight:700;font-size:22px;letter-spacing:2px;color:#e8e8ea}\n"
|
||||
@@ -57,6 +59,7 @@ static const char WS_HTTP_INFO_PAGE[] =
|
||||
"<div id=s>connecting...</div>\n"
|
||||
"<button id=kb>⌨ Keyboard</button>\n"
|
||||
"<button id=rot>↻ Rotate</button>\n"
|
||||
"<button id=xit>✕ Exit remote</button>\n"
|
||||
"<textarea id=k autocomplete=off autocorrect=off autocapitalize=off spellcheck=false></textarea>\n"
|
||||
"</div>\n"
|
||||
"<script>\n"
|
||||
@@ -64,7 +67,7 @@ static const char WS_HTTP_INFO_PAGE[] =
|
||||
"var OT=document.createElement('canvas'),OX=OT.getContext('2d');\n"
|
||||
"var DW=320,DH=240,down=false,last=0,ws;\n"
|
||||
"var isTouch=('ontouchstart' in window)||navigator.maxTouchPoints>0,kbT=null;\n"
|
||||
"var ROT=document.getElementById('rot');\n"
|
||||
"var ROT=document.getElementById('rot'),XIT=document.getElementById('xit');\n"
|
||||
"function st(t,k){S.textContent=t;S.className=k||''}\n"
|
||||
"function conn(){\n"
|
||||
" ws=new WebSocket((location.protocol=='https:'?'wss://':'ws://')+location.host+'/mirror');\n"
|
||||
@@ -74,7 +77,7 @@ static const char WS_HTTP_INFO_PAGE[] =
|
||||
" ws.onerror=function(){st('connection error','err')};\n"
|
||||
" ws.onmessage=function(e){\n"
|
||||
" var a=new Uint8Array(e.data);\n"
|
||||
" if(a[0]==2){DW=a[1]|(a[2]<<8);DH=a[3]|(a[4]<<8);if(C.width!=DW)C.width=DW;if(C.height!=DH)C.height=DH;ROT.style.display=(a.length>5&&(a[5]&1))?'':'none';fit();return}\n"
|
||||
" if(a[0]==2){DW=a[1]|(a[2]<<8);DH=a[3]|(a[4]<<8);if(C.width!=DW)C.width=DW;if(C.height!=DH)C.height=DH;var rem=(a.length>5&&(a[5]&1))?'':'none';ROT.style.display=rem;XIT.style.display=rem;fit();return}\n"
|
||||
" if(a[0]==3){clearTimeout(kbT);if(a[1])K.focus();else K.blur();return}\n"
|
||||
" if(a[0]==1){\n"
|
||||
" var fl=a[1],x=a[2]|(a[3]<<8),y=a[4]|(a[5]<<8),w=a[6]|(a[7]<<8),h=a[8]|(a[9]<<8);\n"
|
||||
@@ -103,6 +106,7 @@ static const char WS_HTTP_INFO_PAGE[] =
|
||||
"var K=document.getElementById('k');\n"
|
||||
"document.getElementById('kb').addEventListener('click',function(){K.value=' ';K.focus();try{K.setSelectionRange(1,1)}catch(x){}});\n"
|
||||
"ROT.addEventListener('click',function(){if(!ws||ws.readyState!=1)return;st('rotating - reconnecting','');ws.send(new Uint8Array([4,DW<DH?1:0]))});\n"
|
||||
"XIT.addEventListener('click',function(){if(!ws||ws.readyState!=1)return;if(!confirm('Leave remote mode? The device reboots to its normal screen.'))return;st('leaving remote - rebooting','');ws.send(new Uint8Array([5]))});\n"
|
||||
"function skey(cp){if(!ws||ws.readyState!=1)return;ws.send(new Uint8Array([2,cp&255,(cp>>8)&255]))}\n"
|
||||
"K.addEventListener('beforeinput',function(e){var t=e.inputType;\n"
|
||||
" if(t=='insertText'&&e.data){for(var i=0;i<e.data.length;i++)skey(e.data.codePointAt(i));e.preventDefault()}\n"
|
||||
@@ -695,6 +699,8 @@ void WebSocketCompanionServer::drainMirrorInput(int idx, WebMirror& m) {
|
||||
m.pushKey((uint16_t)(c->comp_rx_buf[1] | (c->comp_rx_buf[2] << 8)));
|
||||
} else if (ty == 0x04 && c->comp_rx_len >= 2) { // orientation: [0x04, want_landscape]
|
||||
m.requestOrient(c->comp_rx_buf[1] ? 1 : 2); // 1=landscape, 2=portrait; UI thread reboots into it
|
||||
} else if (ty == 0x05) { // exit remote mode
|
||||
m.requestExit();
|
||||
}
|
||||
} else if (c->ws_opcode == 0x08) { // client close
|
||||
disconnectClient(idx);
|
||||
|
||||
+58
-10
@@ -2162,7 +2162,15 @@ static void drawRemotePlaceholder(bool exit_armed = false) {
|
||||
display.drawTextCentered(W / 2, y, "Connecting to Wi-Fi...");
|
||||
}
|
||||
display.setColor(exit_armed ? DisplayDriver::GREEN : DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(W / 2, H - 16, exit_armed ? "Press SPACE again to exit" : "Press SPACE twice to exit");
|
||||
#if CAP_KEYBOARD
|
||||
display.drawTextCentered(W / 2, H - 30, exit_armed ? "Press SPACE again to exit" : "Press SPACE twice to exit");
|
||||
#elif CAP_TOUCH
|
||||
display.drawTextCentered(W / 2, H - 30, exit_armed ? "Keep holding to exit..." : "Touch and hold to exit");
|
||||
#else
|
||||
display.drawTextCentered(W / 2, H - 30, "Open the web page to exit");
|
||||
#endif
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(W / 2, H - 14, "or tap Exit in the browser");
|
||||
display.endFrame();
|
||||
s_remote_ph_ip = up ? (uint32_t)WiFi.localIP() : 0;
|
||||
}
|
||||
@@ -2180,6 +2188,29 @@ static void remotePhysicalKey(int key) {
|
||||
drawRemotePlaceholder(true);
|
||||
}
|
||||
}
|
||||
|
||||
#if CAP_TOUCH
|
||||
// Keyboard-less touch boards (Heltec V4, RAK Tap) have no SPACE key to exit remote mode,
|
||||
// so a 3 s touch-and-hold on the physical panel leaves it. The touch poll runs even in
|
||||
// remote mode (the LVGL indev is skipped but the driver still polls), so the live-touch
|
||||
// state is available here. Called each loop from the remote block.
|
||||
static uint32_t s_remote_touch_start = 0;
|
||||
static void remoteTouchTick() {
|
||||
uint16_t tx, ty;
|
||||
const bool pressed = heltecV4CapTouchGetLive(&tx, &ty);
|
||||
const uint32_t nowm = millis();
|
||||
if (pressed) {
|
||||
if (!s_remote_touch_start) { s_remote_touch_start = nowm; drawRemotePlaceholder(true); } // "Keep holding..."
|
||||
else if (nowm - s_remote_touch_start >= 3000) { // held long enough -> leave
|
||||
touchPrefsSetRemoteMode(false);
|
||||
if (g_lv.task) g_lv.task->rebootDevice();
|
||||
}
|
||||
} else if (s_remote_touch_start) {
|
||||
s_remote_touch_start = 0;
|
||||
drawRemotePlaceholder(false); // lifted before 3 s -> revert the hint
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(HAS_TANMATSU)
|
||||
@@ -38277,8 +38308,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
// scales with pixel count, so fewer pixels = proportionally faster — ~44% less than
|
||||
// 640x384. The browser bilinear-upscales the canvas (near-free on the GPU) at the cost of
|
||||
// some softness. Orientation toggles live via the Rotate button ([0x04] + s_remote_landscape).
|
||||
g_lv.disp_drv.hor_res = s_remote_mode ? (s_remote_landscape ? 480 : 288) : (ui_landscape ? 320 : 240);
|
||||
g_lv.disp_drv.ver_res = s_remote_mode ? (s_remote_landscape ? 288 : 480) : (ui_landscape ? 240 : 320);
|
||||
// 400x240 (was 480x288): keeps the whole mirror footprint (~514 KB) BELOW the VNC
|
||||
// footprint that already allocates on the 2 MB-PSRAM Heltec V4 (~616 KB), so remote is
|
||||
// guaranteed to fit anywhere VNC does instead of black-screening on a failed alloc. Also
|
||||
// faster (fewer pixels). Upscaled in the browser.
|
||||
g_lv.disp_drv.hor_res = s_remote_mode ? (s_remote_landscape ? 400 : 240) : (ui_landscape ? 320 : 240);
|
||||
g_lv.disp_drv.ver_res = s_remote_mode ? (s_remote_landscape ? 240 : 400) : (ui_landscape ? 240 : 320);
|
||||
#endif
|
||||
g_lv.disp_drv.flush_cb = lvglFlush;
|
||||
g_lv.disp_drv.draw_buf = &g_lv.draw_buf;
|
||||
@@ -38361,15 +38396,22 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
g_lv.indev_drv.disp = lv_disp_get_default();
|
||||
if (!lv_indev_drv_register(&g_lv.indev_drv)) pushDiagLine("LVGL indev failed");
|
||||
}
|
||||
// Web UI mirror: stream this display + accept a phone browser's taps as a
|
||||
// second pointer indev (opt-in via the VNC/REMOTE apps; see WebMirror / the WS
|
||||
// server). Remote mode always streams + wants a bigger ring for the 480x800 frame.
|
||||
g_web_mirror.begin(s_remote_mode ? (size_t)(1024 * 1024) : (size_t)0);
|
||||
g_web_mirror.setScreenSize(lv_disp_get_hor_res(NULL), lv_disp_get_ver_res(NULL));
|
||||
g_web_mirror.setEnabled(s_remote_mode ? true : touchPrefsGetWebMirror());
|
||||
g_web_mirror.setRemote(s_remote_mode); // browser shows the Rotate button only in remote mode
|
||||
// Web UI mirror: stream this display + accept a phone browser's taps as a second
|
||||
// pointer indev (opt-in via the VNC/REMOTE apps; see WebMirror / the WS server).
|
||||
// Size the remote ring to just above ONE frame (w*h*2 + 48 KB) instead of a fixed
|
||||
// 1 MB: the old 1 MB ring failed to allocate on the 2 MB-PSRAM Heltec V4 (VNC's
|
||||
// smaller default ring fit), leaving active()==false and a black browser. The ring
|
||||
// only ever holds one dirty region at a time, so frame-sized is also functionally
|
||||
// enough. begin() shrink-retries if even this is tight, so remote streams as long
|
||||
// as one frame fits in PSRAM.
|
||||
s_web_fb_w = lv_disp_get_hor_res(NULL); // shadow-buffer dimensions for the mirror
|
||||
s_web_fb_h = lv_disp_get_ver_res(NULL);
|
||||
const size_t rmt_frame = (size_t)s_web_fb_w * s_web_fb_h * 2;
|
||||
g_web_mirror.begin(s_remote_mode ? (rmt_frame + 48u * 1024u) : (size_t)0);
|
||||
g_web_mirror.setScreenSize(s_web_fb_w, s_web_fb_h);
|
||||
g_web_mirror.setEnabled(s_remote_mode ? true : touchPrefsGetWebMirror());
|
||||
g_web_mirror.setRemote(s_remote_mode); // browser shows the Rotate button only in remote mode
|
||||
if (s_remote_mode) webMirrorEnsureBufs(); // pre-allocate the mirror shadow/band buffers up front
|
||||
lv_indev_drv_init(&s_web_indev_drv);
|
||||
s_web_indev_drv.type = LV_INDEV_TYPE_POINTER;
|
||||
s_web_indev_drv.read_cb = webPointerRead;
|
||||
@@ -39647,6 +39689,12 @@ void UITask::loop() {
|
||||
}
|
||||
}
|
||||
if (s_remote_exit_armed && (now - s_remote_exit_armed) >= 3000) { s_remote_exit_armed = 0; drawRemotePlaceholder(false); }
|
||||
// Exit button on the web page -> leave remote mode (board-agnostic; the main way out
|
||||
// for keyboard-less boards like the Heltec V4).
|
||||
if (g_web_mirror.takeExit()) { touchPrefsSetRemoteMode(false); if (g_lv.task) g_lv.task->rebootDevice(); }
|
||||
#if CAP_TOUCH
|
||||
remoteTouchTick(); // 3 s touch-and-hold on the panel also leaves remote (no SPACE key)
|
||||
#endif
|
||||
uint32_t rip = (WiFi.status() == WL_CONNECTED) ? (uint32_t)WiFi.localIP() : 0;
|
||||
if (rip != s_remote_ph_ip) drawRemotePlaceholder();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user