update docker

This commit is contained in:
2026-06-13 10:17:26 +07:00
parent 4f8d8148f7
commit 8c111f2406
11 changed files with 431 additions and 19 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
build/
.git/
.vscode/
*.md

39
Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
FROM ubuntu:20.04 AS build
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
cmake \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY CMakeLists.txt ./
COPY src ./src
RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
&& cmake --build build -j"$(nproc)"
FROM ubuntu:20.04 AS runtime
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
htop \
procps \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /src/build/lidar_manager_web /app/lidar_manager_web
COPY www ./www
RUN mkdir -p data/models
EXPOSE 8080
ENTRYPOINT ["/app/lidar_manager_web"]
CMD ["8080", "/app/www", "/app/data/state.json"]

View File

@@ -30,3 +30,48 @@ Hoặc chỉ định:
Mở trình duyệt: `http://localhost:8080/`
## Docker (giới hạn 2 CPU, 4 GB RAM)
Mô phỏng cấu hình controller tối thiểu SICK (Dual-Core, 4 GB) trên máy dev:
```bash
cd /home/robotics/RD/Test3
sudo docker compose up --build -d
# hoặc: sudo ./scripts/docker-up.sh
```
Kiểm tra giới hạn:
```bash
sudo ./scripts/docker-stats.sh
```
Dừng:
```bash
sudo docker compose down
```
Dữ liệu layout vẫn lưu tại `data/` trên host (volume mount).
Kiểm tra tài nguyên trong container:
```bash
# Vào shell container
sudo docker exec -it lidar-manager-limited bash
# hoặc: sudo ./scripts/docker-shell.sh
# Trong container, thử:
htop # CPU/RAM (q để thoát)
free -h # RAM
nproc # số CPU nhìn thấy
ps aux # process
cat /proc/meminfo | head
```
```bash
# htop / stats từ ngoài (không cần vào shell)
sudo ./scripts/docker-htop.sh
sudo ./scripts/docker-stats.sh
```

14
docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
services:
lidar-manager:
build: .
image: lidar-manager-web:test3
container_name: lidar-manager-limited
ports:
- "8080:8080"
volumes:
- ./data:/app/data
restart: unless-stopped
# Giới hạn gần spec SICK controller: Dual-Core, 4 GB RAM
cpus: 2
mem_limit: 4g
memswap_limit: 4g

21
scripts/docker-htop.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
# shellcheck source=docker-lib.sh
source "$ROOT/scripts/docker-lib.sh"
docker_cmd
if ! "${DOCKER[@]}" ps --format '{{.Names}}' | grep -qx 'lidar-manager-limited'; then
echo "Container chưa chạy. Khởi động: sudo docker compose up -d"
exit 1
fi
echo "Giới hạn container:"
print_container_limits lidar-manager-limited
echo
echo "Mở htop trong container (q để thoát)..."
exec "${DOCKER[@]}" exec -it lidar-manager-limited htop

21
scripts/docker-lib.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
docker_cmd() {
if docker info >/dev/null 2>&1; then
DOCKER=(docker)
elif sudo -n docker info >/dev/null 2>&1; then
DOCKER=(sudo docker)
else
DOCKER=(sudo docker)
fi
}
print_container_limits() {
local name="${1:-lidar-manager-limited}"
local nano mem cpus ram_mb
nano="$("${DOCKER[@]}" inspect -f '{{.HostConfig.NanoCpus}}' "$name")"
mem="$("${DOCKER[@]}" inspect -f '{{.HostConfig.Memory}}' "$name")"
cpus="$(awk "BEGIN { if ($nano > 0) printf \"%.2f\", $nano / 1000000000; else print \"unlimited\" }")"
ram_mb=$((mem / 1048576))
echo " CPUs quota = ${cpus} core(s), RAM max = ${ram_mb} MB"
}

44
scripts/docker-stats.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=docker-lib.sh
source "$ROOT/scripts/docker-lib.sh"
docker_cmd
NAME="${1:-lidar-manager-limited}"
if ! "${DOCKER[@]}" ps --format '{{.Names}}' | grep -qx "$NAME"; then
echo "Container '$NAME' không chạy."
exit 1
fi
echo "=== docker stats (live) ==="
"${DOCKER[@]}" stats --no-stream "$NAME"
echo
echo "=== limits ==="
print_container_limits "$NAME"
CID="$("${DOCKER[@]}" inspect -f '{{.Id}}' "$NAME")"
CG="/sys/fs/cgroup"
echo
echo "=== cgroup (host) ==="
printf 'memory.usage = '
cat "$CG/memory/docker/$CID/memory.usage_in_bytes" 2>/dev/null \
|| cat "$CG/memory/system.slice/docker-$CID.scope/memory.usage_in_bytes" 2>/dev/null \
|| echo "n/a"
printf ' bytes\n'
printf 'cpu.cfs_quota / period = '
cat "$CG/cpu,cpuacct/docker/$CID/cpu.cfs_quota_us" 2>/dev/null \
|| echo -n "n/a"
printf ' / '
cat "$CG/cpu,cpuacct/docker/$CID/cpu.cfs_period_us" 2>/dev/null \
|| echo "n/a"
echo
echo "=== processes in container ==="
"${DOCKER[@]}" top "$NAME"

20
scripts/docker-up.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
# shellcheck source=docker-lib.sh
source "$ROOT/scripts/docker-lib.sh"
docker_cmd
if ! docker info >/dev/null 2>&1 && ! sudo -n docker info >/dev/null 2>&1; then
echo "Cần quyền Docker. Chạy một trong hai cách:"
echo " sudo usermod -aG docker \$USER # rồi đăng nhập lại"
echo " sudo $0"
fi
"${DOCKER[@]}" compose up --build -d
echo "Đã chạy: http://localhost:8080/"
echo -n "Giới hạn: "
print_container_limits lidar-manager-limited

View File

@@ -3,6 +3,15 @@ const el = (id) => document.getElementById(id);
const statusEl = el("status");
const listEl = el("lidarList");
const lidarFormHintEl = el("lidarFormHint");
const pageTitleEl = document.querySelector(".pageTitle");
const navItemEls = Array.from(document.querySelectorAll(".navItem[data-page]"));
const pageOverviewEl = el("pageOverview");
const pageConfigEl = el("pageConfig");
const overviewBackendEl = el("overviewBackend");
const overviewActiveLayoutEl = el("overviewActiveLayout");
const overviewActiveModelEl = el("overviewActiveModel");
const overviewActiveSensorsEl = el("overviewActiveSensors");
const configSplitterEl = el("configSplitter");
const canvasWrap = el("canvasWrap");
const robotModelEl = el("robotModel");
@@ -107,6 +116,111 @@ const state = {
pendingFootprintClick: null, // { sx, sy } when Shift+click may add a vertex
};
function setActivePage(page) {
const p = page === "overview" ? "overview" : "config";
navItemEls.forEach((a) => {
const on = (a.dataset.page || "") === p;
a.classList.toggle("active", on);
if (on) a.setAttribute("aria-current", "page");
else a.removeAttribute("aria-current");
});
if (pageTitleEl) pageTitleEl.textContent = p === "overview" ? "Tổng quan" : "Cấu Hình";
if (pageOverviewEl) pageOverviewEl.hidden = p !== "overview";
if (pageConfigEl) pageConfigEl.hidden = p !== "config";
try {
localStorage.setItem("activePage", p);
} catch {
/* ignore */
}
}
function initNavigation() {
navItemEls.forEach((a) => {
a.addEventListener("click", (evt) => {
evt.preventDefault();
setActivePage(a.dataset.page || "config");
});
});
// Restore last page, default to config (màn hình chính).
let initial = "config";
try {
const saved = localStorage.getItem("activePage");
if (saved === "overview" || saved === "config") initial = saved;
} catch {
/* ignore */
}
setActivePage(initial);
}
function setLeftPaneWidth(px) {
const v = Math.round(clamp(Number(px), 320, 720));
document.documentElement.style.setProperty("--leftPaneW", `${v}px`);
try {
localStorage.setItem("leftPaneW", String(v));
} catch {
/* ignore */
}
}
function initSplitPane() {
if (!configSplitterEl) return;
try {
const saved = Number(localStorage.getItem("leftPaneW"));
if (Number.isFinite(saved) && saved > 0) setLeftPaneWidth(saved);
else setLeftPaneWidth(460);
} catch {
setLeftPaneWidth(460);
}
let dragging = false;
let startX = 0;
let startW = 0;
const onMove = (evt) => {
if (!dragging) return;
const x = evt.clientX ?? (evt.touches && evt.touches[0] ? evt.touches[0].clientX : startX);
setLeftPaneWidth(startW + (x - startX));
};
const onUp = () => {
if (!dragging) return;
dragging = false;
configSplitterEl.classList.remove("dragging");
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
window.removeEventListener("touchmove", onMove);
window.removeEventListener("touchend", onUp);
};
configSplitterEl.addEventListener("mousedown", (evt) => {
evt.preventDefault();
dragging = true;
configSplitterEl.classList.add("dragging");
startX = evt.clientX;
startW = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--leftPaneW")) || 460;
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
});
configSplitterEl.addEventListener("touchstart", (evt) => {
if (!evt.touches || !evt.touches[0]) return;
dragging = true;
configSplitterEl.classList.add("dragging");
startX = evt.touches[0].clientX;
startW = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--leftPaneW")) || 460;
window.addEventListener("touchmove", onMove, { passive: true });
window.addEventListener("touchend", onUp);
}, { passive: false });
// Keyboard resize (focus splitter, use arrows)
configSplitterEl.addEventListener("keydown", (evt) => {
if (evt.key !== "ArrowLeft" && evt.key !== "ArrowRight") return;
evt.preventDefault();
const cur = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--leftPaneW")) || 460;
setLeftPaneWidth(cur + (evt.key === "ArrowLeft" ? -20 : 20));
});
}
const DIFF_DEFAULTS = {
frame_id: "base_footprint",
wheel_separation_m: 1.0,
@@ -2906,6 +3020,7 @@ async function loadAll() {
loadAllInFlight = (async () => {
const st = await api("/api/state");
if (overviewBackendEl) overviewBackendEl.textContent = "OK";
state.activeLayoutId = st.active_layout_id || null;
state.activeLayoutName = st.active_layout_name || "";
state.layoutCatalog = st.layouts || [];
@@ -2980,6 +3095,16 @@ async function loadAll() {
setSelectedRelText();
renderList();
renderImuList();
if (overviewActiveLayoutEl) {
const name = state.activeLayoutName || state.activeLayoutId || "—";
overviewActiveLayoutEl.textContent = name;
}
if (overviewActiveModelEl) {
overviewActiveModelEl.textContent = state.layout?.robot?.model || "diff";
}
if (overviewActiveSensorsEl) {
overviewActiveSensorsEl.textContent = `${state.lidars.length} LiDAR • ${state.imus.length} IMU`;
}
if (!state.viewInitialized) {
fitViewToWorld();
state.viewInitialized = true;
@@ -3203,6 +3328,8 @@ function initRobotModelPanelCollapse() {
}
initLayoutManagerEvents();
initNavigation();
initSplitPane();
initLidarForm();
initMotorWheelsEvents();
initBicycleMotorWheelsEvents();
@@ -3253,6 +3380,7 @@ saveLayoutBtn.addEventListener("click", async () => {
setStatus("Sẵn sàng");
} catch (e) {
const msg = String(e.message || e);
if (overviewBackendEl) overviewBackendEl.textContent = `Lỗi: ${msg}`;
if (msg.includes("stack") || msg.includes("Maximum call")) {
setStatus(`Lỗi JavaScript: ${msg}`);
} else {

View File

@@ -50,6 +50,51 @@
</header>
<main class="content">
<div class="page" id="pageOverview" data-page-content="overview" hidden>
<section class="card">
<div class="cardHeader">
<div>
<div class="cardTitle">Phần mềm</div>
<div class="cardSub">Thông tin phiên bản và trạng thái backend.</div>
</div>
</div>
<div class="cardBody">
<div class="row rowWide">
<label>Backend</label>
<div id="overviewBackend" class="mutedNote"></div>
</div>
<div class="row rowWide">
<label>Dữ liệu</label>
<div class="mutedNote">`data/state.json` (catalog) + `data/models/{id}.json` (layout)</div>
</div>
</div>
</section>
<section class="card">
<div class="cardHeader">
<div>
<div class="cardTitle">Cấu hình đang active</div>
<div class="cardSub">Tóm tắt layout đang mở.</div>
</div>
</div>
<div class="cardBody">
<div class="row rowWide">
<label>Layout</label>
<div id="overviewActiveLayout" class="mutedNote"></div>
</div>
<div class="row rowWide">
<label>Model robot</label>
<div id="overviewActiveModel" class="mutedNote"></div>
</div>
<div class="row rowWide">
<label>LiDAR / IMU</label>
<div id="overviewActiveSensors" class="mutedNote"></div>
</div>
</div>
</section>
</div>
<div class="page" id="pageConfig" data-page-content="config">
<div class="contentLeft">
<section class="card" id="layoutManagerCard">
<div class="cardHeader">
@@ -462,6 +507,11 @@
</section>
</div>
</div>
<div id="configSplitter" class="splitter" role="separator" aria-orientation="vertical" tabindex="0"></div>
<div class="contentRight">
<section class="card">
<div class="cardHeader">
<div>
@@ -482,6 +532,7 @@
</div>
</div>
</section>
</div>
</main>
</div>
</div>

View File

@@ -137,15 +137,38 @@ body {
.content {
padding: 18px;
display: grid;
grid-template-columns: min(460px, 100%) 1fr;
grid-template-columns: var(--leftPaneW, 460px) 10px 1fr;
gap: 16px;
align-items: start;
height: 100%;
min-height: 0;
}
.contentLeft {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
max-height: 100%;
overflow: auto;
overscroll-behavior: contain;
}
.contentRight {
min-width: 0;
max-height: 100%;
}
.splitter {
align-self: stretch;
width: 10px;
margin: 0 -16px; /* overlap gap a bit for easier grab */
cursor: col-resize;
border-radius: 10px;
background: rgba(15, 23, 42, 0.04);
border: 1px solid rgba(15, 23, 42, 0.08);
}
.splitter:hover,
.splitter.dragging {
background: rgba(37, 99, 235, 0.12);
border-color: rgba(37, 99, 235, 0.25);
}
.modelForm { display: grid; gap: 10px; }
.modelParams { display: grid; gap: 10px; }
@@ -519,6 +542,8 @@ canvas {
.shell { grid-template-columns: 1fr; }
.sidebar { position: relative; height: auto; }
.body { grid-template-rows: auto 1fr; }
.content { grid-template-columns: 1fr; }
.content { grid-template-columns: 1fr; height: auto; }
.splitter { display: none; }
.contentLeft { max-height: none; overflow: visible; }
}