feat: add weekly curriculum cover image upload and display
This commit is contained in:
@@ -49,7 +49,7 @@
|
||||
#cameraPreviewWrap {
|
||||
position:absolute;
|
||||
left:16px;
|
||||
top:48px;
|
||||
top:16px;
|
||||
z-index:10;
|
||||
display:none;
|
||||
width:min(260px, calc(100vw - 32px));
|
||||
@@ -69,7 +69,7 @@
|
||||
#cameraPreviewWrap.is-collapsed #cameraPreview { display:none; }
|
||||
#cameraPreviewToggle {
|
||||
position:absolute;
|
||||
right:0;
|
||||
left:0;
|
||||
top:100%;
|
||||
display:grid;
|
||||
width:30px;
|
||||
@@ -110,6 +110,8 @@
|
||||
bottom:16px;
|
||||
z-index:10;
|
||||
padding:7px 9px;
|
||||
max-width:calc(100% - 32px);
|
||||
box-sizing:border-box;
|
||||
border:1px solid rgba(255,255,255,.24);
|
||||
background:rgba(17,24,39,.34);
|
||||
color:rgba(255,255,255,.82);
|
||||
@@ -369,8 +371,8 @@
|
||||
.nav2-navigation #rosTopicStatusWrap { bottom:16px; }
|
||||
.real-runtime #topLeftControls { display:none; }
|
||||
.real-runtime #keyGuide { display:none; }
|
||||
.real-yahboom-teleop:not(.editor-mode) #keyGuide { display:block; }
|
||||
.real-yahboom-teleop #rosTopicStatusWrap { bottom:72px; }
|
||||
.real-teleop:not(.editor-mode) #keyGuide { display:block; }
|
||||
.real-teleop #rosTopicStatusWrap { bottom:50px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -444,7 +446,8 @@ const ROBOT_PLATFORM = (params.get('platform') || '').trim().toLowerCase();
|
||||
// Keep the prior real-platform default when an older LMS URL has no platform.
|
||||
const USE_MKMINI_MODEL = ROBOT_PLATFORM === 'mkmini' || (!ROBOT_PLATFORM && REAL_RUNTIME);
|
||||
const USE_YAHBOOM_MODEL = ROBOT_PLATFORM === 'yahboom';
|
||||
const REAL_YAHBOOM_TELEOP = REAL_RUNTIME && USE_YAHBOOM_MODEL;
|
||||
const REAL_MKMINI_TELEOP = REAL_RUNTIME && USE_MKMINI_MODEL;
|
||||
const REAL_TELEOP = REAL_RUNTIME && (USE_YAHBOOM_MODEL || USE_MKMINI_MODEL);
|
||||
const SHOW_POINT_CLOUD = params.get('pointCloud') === '1';
|
||||
const SHOW_SENSOR_POINT_CLOUD = REAL_RUNTIME && SHOW_POINT_CLOUD;
|
||||
const SENSOR_POINT_CLOUD_MAX_AGE_MS = 3_000;
|
||||
@@ -495,8 +498,11 @@ const rosTopicStatusList = document.getElementById('rosTopicStatusList');
|
||||
if (EDITOR_MODE) document.body.classList.add('editor-mode');
|
||||
if (!EDITOR_MODE) document.body.classList.add(`nav2-${NAV2_VIEW_MODE}`);
|
||||
if (REAL_RUNTIME) document.body.classList.add('real-runtime');
|
||||
if (REAL_YAHBOOM_TELEOP && !EDITOR_MODE) {
|
||||
document.body.classList.add('real-yahboom-teleop');
|
||||
if (REAL_TELEOP && !EDITOR_MODE) {
|
||||
document.body.classList.add('real-teleop');
|
||||
if (REAL_MKMINI_TELEOP) {
|
||||
document.querySelector('#keyGuide div').textContent = 'i 전진 · , 후진 · j/l 주행 중 조향 · k 정지';
|
||||
}
|
||||
}
|
||||
if (EDITOR_MODE) responseWaitOverlay?.classList.add('is-hidden');
|
||||
|
||||
@@ -895,6 +901,7 @@ let savedMapPointCloudLoaded = false;
|
||||
let sensorPointCloudRequestInFlight = false;
|
||||
let sensorPointCloudRefreshTimer = null;
|
||||
let cameraFrameRequestInFlight = false;
|
||||
let cameraFrameRetryAt = 0;
|
||||
let cameraFrameRefreshTimer = null;
|
||||
let cameraFrameObjectUrl = null;
|
||||
let pathVisible = false;
|
||||
@@ -2051,28 +2058,47 @@ function setCameraPreviewAvailable(available) {
|
||||
if (cameraPreview) cameraPreview.removeAttribute('src');
|
||||
}
|
||||
|
||||
function hasLiveCameraState() {
|
||||
return latestSlamState?.topic_status?.camera_image?.available === true
|
||||
&& Date.now() - latestSlamStateReceivedAt < 3000;
|
||||
}
|
||||
|
||||
async function refreshCameraFrame() {
|
||||
if (!ENABLE_CAMERA_PREVIEW || cameraFrameRequestInFlight || document.hidden) return;
|
||||
// Reuse websocket topic status instead of probing an absent camera with HTTP.
|
||||
if (!hasLiveCameraState()) {
|
||||
setCameraPreviewAvailable(false);
|
||||
return;
|
||||
}
|
||||
if (Date.now() < cameraFrameRetryAt) return;
|
||||
cameraFrameRequestInFlight = true;
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/camera-frame`, { cache: 'no-store', signal: AbortSignal.timeout(2000) });
|
||||
if (!response.ok) {
|
||||
cameraFrameRetryAt = Date.now() + 5000;
|
||||
setCameraPreviewAvailable(false);
|
||||
return;
|
||||
}
|
||||
const nextUrl = URL.createObjectURL(await response.blob());
|
||||
const frame = await response.blob();
|
||||
if (!hasLiveCameraState() || document.hidden) {
|
||||
setCameraPreviewAvailable(false);
|
||||
return;
|
||||
}
|
||||
cameraFrameRetryAt = 0;
|
||||
const nextUrl = URL.createObjectURL(frame);
|
||||
const previousUrl = cameraFrameObjectUrl;
|
||||
cameraFrameObjectUrl = nextUrl;
|
||||
if (cameraPreview) cameraPreview.src = nextUrl;
|
||||
if (previousUrl) URL.revokeObjectURL(previousUrl);
|
||||
} catch (_) {
|
||||
cameraFrameRetryAt = Date.now() + 5000;
|
||||
setCameraPreviewAvailable(false);
|
||||
} finally {
|
||||
cameraFrameRequestInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
cameraPreview?.addEventListener('load', () => setCameraPreviewAvailable(true));
|
||||
cameraPreview?.addEventListener('load', () => setCameraPreviewAvailable(hasLiveCameraState()));
|
||||
cameraPreview?.addEventListener('error', () => setCameraPreviewAvailable(false));
|
||||
|
||||
function drawMiniMap(state) {
|
||||
@@ -3217,7 +3243,7 @@ function connectControlSocket() {
|
||||
if (!CONTROL_WS) return;
|
||||
controlSocket = new WebSocket(CONTROL_WS);
|
||||
controlSocket.addEventListener('close', () => {
|
||||
if (REAL_YAHBOOM_TELEOP) {
|
||||
if (REAL_TELEOP) {
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
}
|
||||
@@ -3228,8 +3254,16 @@ function connectControlSocket() {
|
||||
});
|
||||
}
|
||||
|
||||
let maxLinear = REAL_YAHBOOM_TELEOP ? 0.1 : 0.22;
|
||||
let maxAngular = REAL_YAHBOOM_TELEOP ? 0.5 : 2.84;
|
||||
let maxLinear = REAL_TELEOP ? 0.1 : 0.22;
|
||||
let maxAngular = REAL_TELEOP ? 0.5 : 2.84;
|
||||
const mkMiniHeldKeys = new Set();
|
||||
const MKMINI_REAL_ANGULAR_SPEED = 0.08;
|
||||
function teleopKey(event) {
|
||||
const key = event.key.toLowerCase();
|
||||
return REAL_MKMINI_TELEOP
|
||||
? ({ arrowup: 'i', arrowdown: ',', arrowleft: 'j', arrowright: 'l', ' ': 'k' }[key] ?? key)
|
||||
: key;
|
||||
}
|
||||
let activeMoveKey = null;
|
||||
let teleopInterval = null;
|
||||
let singleMoveStopTimer = null;
|
||||
@@ -3253,7 +3287,7 @@ const speedBindings = {
|
||||
};
|
||||
|
||||
function canUseTeleop(event) {
|
||||
if (REAL_RUNTIME && (!REAL_YAHBOOM_TELEOP || !realTeleopReady())) return false;
|
||||
if (EDITOR_MODE || (REAL_RUNTIME && (!REAL_TELEOP || !realTeleopReady()))) return false;
|
||||
if (!document.hasFocus()) return false;
|
||||
if (document.hidden || event.ctrlKey || event.altKey || event.metaKey || event.target?.isContentEditable) return false;
|
||||
const tag = event.target?.tagName?.toLowerCase();
|
||||
@@ -3292,7 +3326,7 @@ function flushQueuedCmdVel() {
|
||||
|
||||
function sendCmdVel(linear, angular) {
|
||||
const payload = { type:'cmd_vel', linear, angular };
|
||||
if (REAL_YAHBOOM_TELEOP) {
|
||||
if (REAL_TELEOP) {
|
||||
// Never queue physical motion for replay after a connection recovers.
|
||||
if (!controlSocket || controlSocket.readyState !== WebSocket.OPEN) return;
|
||||
if ((linear || angular) && !realTeleopReady()) return;
|
||||
@@ -3346,6 +3380,7 @@ function setSimulatorMkMiniSteering(angle) {
|
||||
}
|
||||
|
||||
function stopRobot() {
|
||||
mkMiniHeldKeys.clear();
|
||||
if (teleopInterval) {
|
||||
window.clearInterval(teleopInterval);
|
||||
teleopInterval = null;
|
||||
@@ -3359,13 +3394,25 @@ function stopRobot() {
|
||||
|
||||
function sendActiveMoveCommand() {
|
||||
if (!activeMoveKey) return;
|
||||
if (REAL_YAHBOOM_TELEOP && (!realTeleopReady() || document.hidden || !document.hasFocus())) {
|
||||
if (REAL_TELEOP && (!realTeleopReady() || document.hidden || !document.hasFocus())) {
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
return;
|
||||
}
|
||||
const [linearSign, angularSign] = moveBindings[activeMoveKey];
|
||||
if (linearSign === 0 && angularSign === 0) return;
|
||||
if (REAL_MKMINI_TELEOP) {
|
||||
let drive = 0;
|
||||
let steering = 0;
|
||||
for (const key of mkMiniHeldKeys) {
|
||||
drive += moveBindings[key][0];
|
||||
steering += moveBindings[key][1];
|
||||
}
|
||||
// The TOD adapter uses abs(linear) for steering: keep wheel direction
|
||||
// consistent in reverse, and never turn a steering-only key into motion.
|
||||
sendCmdVel(Math.sign(drive) * maxLinear, drive ? Math.sign(steering) * MKMINI_REAL_ANGULAR_SPEED : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (USE_MKMINI_MODEL && !REAL_RUNTIME) {
|
||||
if (activeMoveKey === 'j' || activeMoveKey === 'l') {
|
||||
@@ -3389,6 +3436,7 @@ function sendActiveMoveCommand() {
|
||||
|
||||
function startActiveMoveCommand() {
|
||||
sendActiveMoveCommand();
|
||||
if (!activeMoveKey) return;
|
||||
if (teleopInterval) return;
|
||||
teleopInterval = window.setInterval(sendActiveMoveCommand, TELEOP_COMMAND_INTERVAL_MS);
|
||||
}
|
||||
@@ -3406,7 +3454,7 @@ function sendSingleMoveCommand(key) {
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (!canUseTeleop(event)) return;
|
||||
const key = event.key.toLowerCase();
|
||||
const key = teleopKey(event);
|
||||
|
||||
if (event.repeat) {
|
||||
if (moveBindings[key]) event.preventDefault();
|
||||
@@ -3415,7 +3463,7 @@ window.addEventListener('keydown', (event) => {
|
||||
|
||||
if (speedBindings[key]) {
|
||||
event.preventDefault();
|
||||
if (REAL_YAHBOOM_TELEOP) return;
|
||||
if (REAL_TELEOP) return;
|
||||
const [linearScale, angularScale] = speedBindings[key];
|
||||
maxLinear = Math.max(0.01, maxLinear * linearScale);
|
||||
maxAngular = Math.max(0.1, maxAngular * angularScale);
|
||||
@@ -3431,7 +3479,8 @@ window.addEventListener('keydown', (event) => {
|
||||
stopRobot();
|
||||
return;
|
||||
}
|
||||
if (REAL_YAHBOOM_TELEOP) {
|
||||
if (REAL_TELEOP) {
|
||||
if (REAL_MKMINI_TELEOP) mkMiniHeldKeys.add(key);
|
||||
activeMoveKey = key;
|
||||
startActiveMoveCommand();
|
||||
return;
|
||||
@@ -3451,7 +3500,7 @@ window.addEventListener('keydown', (event) => {
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (event) => {
|
||||
if (REAL_YAHBOOM_TELEOP && event.key.toLowerCase() === activeMoveKey) {
|
||||
if (REAL_TELEOP && (teleopKey(event) === activeMoveKey || (REAL_MKMINI_TELEOP && mkMiniHeldKeys.has(teleopKey(event))))) {
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
return;
|
||||
@@ -3468,7 +3517,7 @@ window.addEventListener('blur', () => {
|
||||
});
|
||||
|
||||
function releaseRealTeleop() {
|
||||
if (!REAL_YAHBOOM_TELEOP || !activeMoveKey) return;
|
||||
if (!REAL_TELEOP || !activeMoveKey) return;
|
||||
activeMoveKey = null;
|
||||
stopRobot();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user