mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-09-23 04:20:16 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b147f81322 | ||
|
|
880980dccd | ||
|
|
356084a3f1 |
+7
-24
@@ -1,13 +1,17 @@
|
|||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
// @ts-ignore -- no types for this plugin
|
// @ts-ignore -- no types for this plugin
|
||||||
import drizzle from "eslint-plugin-drizzle";
|
import drizzle from "eslint-plugin-drizzle";
|
||||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: import.meta.dirname,
|
||||||
|
});
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: [".next", "src/trpc/*.js"],
|
ignores: [".next"],
|
||||||
},
|
},
|
||||||
...nextVitals,
|
...compat.extends("next/core-web-vitals"),
|
||||||
{
|
{
|
||||||
files: ["**/*.ts", "**/*.tsx"],
|
files: ["**/*.ts", "**/*.tsx"],
|
||||||
plugins: {
|
plugins: {
|
||||||
@@ -34,27 +38,6 @@ export default tseslint.config(
|
|||||||
"error",
|
"error",
|
||||||
{ checksVoidReturn: { attributes: false } },
|
{ checksVoidReturn: { attributes: false } },
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
|
||||||
"@typescript-eslint/no-unsafe-call": "warn",
|
|
||||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
|
||||||
"@typescript-eslint/no-unsafe-return": "warn",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
|
||||||
"@typescript-eslint/non-nullable-type-assertion-style": "off",
|
|
||||||
"@typescript-eslint/no-base-to-string": "warn",
|
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
|
||||||
"@typescript-eslint/restrict-template-expressions": "warn",
|
|
||||||
"@typescript-eslint/prefer-promise-reject-errors": "warn",
|
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": "warn",
|
|
||||||
"@typescript-eslint/no-empty-function": "warn",
|
|
||||||
"react-hooks/refs": "warn",
|
|
||||||
"react-hooks/set-state-in-effect": "warn",
|
|
||||||
"react-hooks/immutability": "warn",
|
|
||||||
"react-hooks/static-components": "warn",
|
|
||||||
"react-hooks/purity": "warn",
|
|
||||||
"react-hooks/error-boundaries": "warn",
|
|
||||||
"react-hooks/preserve-manual-memoization": "warn",
|
|
||||||
"drizzle/enforce-delete-with-where": [
|
"drizzle/enforce-delete-with-where": [
|
||||||
"error",
|
"error",
|
||||||
{ drizzleObjectName: ["db", "ctx.db"] },
|
{ drizzleObjectName: ["db", "ctx.db"] },
|
||||||
|
|||||||
+66
-79
@@ -28,106 +28,107 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/drizzle-adapter": "^1.11.3",
|
"@auth/drizzle-adapter": "^1.11.1",
|
||||||
"@aws-sdk/client-s3": "^3.1132.0",
|
"@aws-sdk/client-s3": "^3.989.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.1132.0",
|
"@aws-sdk/s3-request-presigner": "^3.989.0",
|
||||||
"@better-auth/drizzle-adapter": "~1.6.33",
|
"@better-auth/drizzle-adapter": "^1.5.5",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.9.1",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.20",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.23",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.15",
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||||
"@radix-ui/react-avatar": "^1.2.6",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.11",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.20",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.23",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.24",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.15",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.23",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.16",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.18",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.3.7",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.15",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slider": "^1.4.7",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.3.3",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.3.7",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.21",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.16",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@shadcn/ui": "^0.0.4",
|
"@shadcn/ui": "^0.0.4",
|
||||||
"@t3-oss/env-nextjs": "^0.13.11",
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "^5.102.8",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tiptap/extension-table": "^3.31.3",
|
"@tiptap/extension-table": "^3.20.0",
|
||||||
"@tiptap/extension-table-cell": "^3.31.3",
|
"@tiptap/extension-table-cell": "^3.20.0",
|
||||||
"@tiptap/extension-table-header": "^3.31.3",
|
"@tiptap/extension-table-header": "^3.20.0",
|
||||||
"@tiptap/extension-table-row": "^3.31.3",
|
"@tiptap/extension-table-row": "^3.20.0",
|
||||||
"@tiptap/pm": "^3.31.3",
|
"@tiptap/pm": "^3.20.0",
|
||||||
"@tiptap/react": "^3.31.3",
|
"@tiptap/react": "^3.20.0",
|
||||||
"@tiptap/starter-kit": "^3.31.3",
|
"@tiptap/starter-kit": "^3.20.0",
|
||||||
"@trpc/client": "^11.18.0",
|
"@trpc/client": "^11.10.0",
|
||||||
"@trpc/react-query": "^11.18.0",
|
"@trpc/react-query": "^11.10.0",
|
||||||
"@trpc/server": "^11.18.0",
|
"@trpc/server": "^11.10.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-auth": "~1.6.33",
|
"better-auth": "^1.5.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.1.0",
|
||||||
"driver.js": "^1.8.0",
|
"driver.js": "^1.4.0",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"html2pdf.js": "^0.14.0",
|
"html2pdf.js": "^0.14.0",
|
||||||
"js-cookie": "^3.0.8",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.536.0",
|
"lucide-react": "^0.536.0",
|
||||||
"minio": "^8.0.7",
|
"minio": "^8.0.6",
|
||||||
"next": "16",
|
"next": "16.2.6",
|
||||||
"next-auth": "^5.0.0-beta.32",
|
"next-auth": "^5.0.0-beta.30",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.8",
|
||||||
"radix-ui": "^1.6.7",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-hook-form": "^7.88.0",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
"react-webcam": "^7.2.0",
|
"react-webcam": "^7.2.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^2.0.8",
|
"sonner": "^2.0.7",
|
||||||
"superjson": "^2.2.6",
|
"superjson": "^2.2.6",
|
||||||
"tailwind-merge": "^3.7.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tiptap-markdown": "^0.9.0",
|
"tiptap-markdown": "^0.9.0",
|
||||||
"uuid": "^13.0.2",
|
"uuid": "^13.0.0",
|
||||||
"ws": "^8.21.3",
|
"ws": "^8.19.0",
|
||||||
"zod": "^4.6.5",
|
"zod": "^4.3.6",
|
||||||
"zustand": "^4.5.7"
|
"zustand": "^4.5.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "^1.4.2",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^20.19.43",
|
"@types/node": "^20.19.33",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.30.6",
|
||||||
"eslint": "^9.39.5",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-next": "16.3.5",
|
"eslint-config-next": "16.2.1",
|
||||||
"eslint-plugin-drizzle": "^0.2.3",
|
"eslint-plugin-drizzle": "^0.2.3",
|
||||||
"postcss": "^8.5.28",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.1.18",
|
||||||
"ts-unused-exports": "^11.0.1",
|
"ts-unused-exports": "^11.0.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.70.0",
|
"typescript-eslint": "^8.55.0",
|
||||||
"vitest": "^4.1.11"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.39.3"
|
"initVersion": "7.39.3"
|
||||||
@@ -140,20 +141,6 @@
|
|||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3"
|
||||||
"@better-auth/core": "1.6.33",
|
|
||||||
"minio": {
|
|
||||||
"fast-xml-parser": "^5.7.0",
|
|
||||||
"stream-json": "^3.6.0"
|
|
||||||
},
|
|
||||||
"query-string": {
|
|
||||||
"decode-uri-component": "^0.5.0"
|
|
||||||
},
|
|
||||||
"@esbuild-kit/core-utils": {
|
|
||||||
"esbuild": "^0.25.12"
|
|
||||||
},
|
|
||||||
"vite": {
|
|
||||||
"esbuild": "^0.28.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ function validateSeedData() {
|
|||||||
const seanRole = userRoles.find(
|
const seanRole = userRoles.find(
|
||||||
(r) => r.userId === "01234567-89ab-cdef-0123-456789abcde0",
|
(r) => r.userId === "01234567-89ab-cdef-0123-456789abcde0",
|
||||||
);
|
);
|
||||||
if (seanRole?.role === "administrator") {
|
if (seanRole && seanRole.role === "administrator") {
|
||||||
console.log(` ✅ Sean has administrator role`);
|
console.log(` ✅ Sean has administrator role`);
|
||||||
} else {
|
} else {
|
||||||
console.error(` ❌ Sean missing administrator role`);
|
console.error(` ❌ Sean missing administrator role`);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface Subscriber {
|
|||||||
const PORT = parseInt(process.env.MOCK_ROBOT_PORT || "9090", 10);
|
const PORT = parseInt(process.env.MOCK_ROBOT_PORT || "9090", 10);
|
||||||
const PUBLISH_INTERVAL = parseInt(process.env.MOCK_PUBLISH_INTERVAL || "100", 10);
|
const PUBLISH_INTERVAL = parseInt(process.env.MOCK_PUBLISH_INTERVAL || "100", 10);
|
||||||
|
|
||||||
const subscribers = new Map<string, Subscriber>();
|
const subscribers: Map<string, Subscriber> = new Map();
|
||||||
let subscriberIdCounter = 0;
|
let subscriberIdCounter = 0;
|
||||||
|
|
||||||
const mockRobotState = {
|
const mockRobotState = {
|
||||||
|
|||||||
@@ -29,28 +29,12 @@ export default function RobotIntegrationTutorial() {
|
|||||||
<p>HRIStudio supports multiple robot platforms:</p>
|
<p>HRIStudio supports multiple robot platforms:</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr><th>Robot</th><th>Protocol</th><th>Capabilities</th></tr>
|
||||||
<th>Robot</th>
|
|
||||||
<th>Protocol</th>
|
|
||||||
<th>Capabilities</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr><td>NAO6</td><td>ROS2</td><td>Speech, movement, gestures, sensors</td></tr>
|
||||||
<td>NAO6</td>
|
<tr><td>TurtleBot3</td><td>ROS2</td><td>Navigation, sensors</td></tr>
|
||||||
<td>ROS2</td>
|
<tr><td>Mock Robot</td><td>WebSocket</td><td>All actions (simulation)</td></tr>
|
||||||
<td>Speech, movement, gestures, sensors</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>TurtleBot3</td>
|
|
||||||
<td>ROS2</td>
|
|
||||||
<td>Navigation, sensors</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Mock Robot</td>
|
|
||||||
<td>WebSocket</td>
|
|
||||||
<td>All actions (simulation)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -59,70 +43,44 @@ export default function RobotIntegrationTutorial() {
|
|||||||
<h3>Network Configuration</h3>
|
<h3>Network Configuration</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Connect NAO6 to your network</li>
|
<li>Connect NAO6 to your network</li>
|
||||||
<li>
|
<li>Note the robot's IP address:
|
||||||
Note the robot's IP address:
|
<pre><code># On the robot, say "What is my IP address?"
|
||||||
<pre>
|
# Or check robot's network settings</code></pre>
|
||||||
<code>
|
|
||||||
# On the robot, say "What is my IP address?" # Or check
|
|
||||||
robot's network settings
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>Verify network access:
|
||||||
Verify network access:
|
<pre><code>ping nao.local
|
||||||
<pre>
|
# Or ping the IP directly:
|
||||||
<code>
|
ping 192.168.1.100</code></pre>
|
||||||
ping nao.local # Or ping the IP directly: ping 192.168.1.100
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<h3>Wake Up Robot</h3>
|
<h3>Wake Up Robot</h3>
|
||||||
<p>Before connecting, wake up the robot:</p>
|
<p>Before connecting, wake up the robot:</p>
|
||||||
<pre>
|
<pre><code>ssh nao@192.168.1.100
|
||||||
<code>
|
# Enter password when prompted
|
||||||
ssh nao@192.168.1.100 # Enter password when prompted # Wake up the
|
|
||||||
robot python -c "from naoqi import ALProxy; proxy =
|
# Wake up the robot
|
||||||
ALProxy('ALMotion', '192.168.1.100', 9559);
|
python -c "from naoqi import ALProxy; proxy = ALProxy('ALMotion', '192.168.1.100', 9559); proxy.wakeUp()"</code></pre>
|
||||||
proxy.wakeUp()"
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h2>Step 2: Start Docker Services</h2>
|
<h2>Step 2: Start Docker Services</h2>
|
||||||
|
|
||||||
<pre>
|
<pre><code>cd ~/nao6-hristudio-integration
|
||||||
<code>
|
|
||||||
cd ~/nao6-hristudio-integration # Set robot IP export
|
# Set robot IP
|
||||||
NAO_IP=192.168.1.100 # Start services docker compose up -d
|
export NAO_IP=192.168.1.100
|
||||||
</code>
|
|
||||||
</pre>
|
# Start services
|
||||||
|
docker compose up -d</code></pre>
|
||||||
|
|
||||||
<h3>Services Overview</h3>
|
<h3>Services Overview</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr><th>Service</th><th>Port</th><th>Purpose</th></tr>
|
||||||
<th>Service</th>
|
|
||||||
<th>Port</th>
|
|
||||||
<th>Purpose</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr><td>nao_driver</td><td>-</td><td>ROS2 driver for NAO</td></tr>
|
||||||
<td>nao_driver</td>
|
<tr><td>ros_bridge</td><td>9090</td><td>WebSocket bridge</td></tr>
|
||||||
<td>-</td>
|
<tr><td>ros_api</td><td>-</td><td>Topic introspection</td></tr>
|
||||||
<td>ROS2 driver for NAO</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ros_bridge</td>
|
|
||||||
<td>9090</td>
|
|
||||||
<td>WebSocket bridge</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ros_api</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>Topic introspection</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -130,49 +88,32 @@ export default function RobotIntegrationTutorial() {
|
|||||||
|
|
||||||
<h3>Install Robot Plugin</h3>
|
<h3>Install Robot Plugin</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Go to <strong>Plugins</strong> in sidebar</li>
|
||||||
Go to <strong>Plugins</strong> in sidebar
|
|
||||||
</li>
|
|
||||||
<li>Select your study</li>
|
<li>Select your study</li>
|
||||||
<li>
|
<li>Click <strong>Browse Plugins</strong></li>
|
||||||
Click <strong>Browse Plugins</strong>
|
<li>Find <strong>NAO6 Robot (ROS2 Integration)</strong></li>
|
||||||
</li>
|
<li>Click <strong>Install</strong></li>
|
||||||
<li>
|
|
||||||
Find <strong>NAO6 Robot (ROS2 Integration)</strong>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Install</strong>
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<h3>Configure Plugin</h3>
|
<h3>Configure Plugin</h3>
|
||||||
<pre>
|
<pre><code>Robot Name: NAO6-Lab
|
||||||
<code>
|
Robot IP: 192.168.1.100
|
||||||
Robot Name: NAO6-Lab Robot IP: 192.168.1.100 WebSocket URL:
|
WebSocket URL: ws://localhost:9090</code></pre>
|
||||||
ws://localhost:9090
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h3>Environment Variables</h3>
|
<h3>Environment Variables</h3>
|
||||||
<p>
|
<p>Create <code>hristudio/.env.local</code>:</p>
|
||||||
Create <code>hristudio/.env.local</code>:
|
<pre><code># Robot connection
|
||||||
</p>
|
NAO_ROBOT_IP=192.168.1.100
|
||||||
<pre>
|
NAO_PASSWORD=robolab
|
||||||
<code>
|
NAO_USERNAME=nao
|
||||||
# Robot connection NAO_ROBOT_IP=192.168.1.100 NAO_PASSWORD=robolab
|
|
||||||
NAO_USERNAME=nao # WebSocket bridge
|
# WebSocket bridge
|
||||||
NEXT_PUBLIC_ROS_BRIDGE_URL=ws://localhost:9090
|
NEXT_PUBLIC_ROS_BRIDGE_URL=ws://localhost:9090</code></pre>
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
<h2>Step 4: Test Connection</h2>
|
<h2>Step 4: Test Connection</h2>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Navigate to: <code>http://localhost:3000/nao-test</code></li>
|
||||||
Navigate to: <code>http://localhost:3000/nao-test</code>
|
<li>Click <strong>Connect</strong></li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Connect</strong>
|
|
||||||
</li>
|
|
||||||
<li>Verify connection status shows "Connected"</li>
|
<li>Verify connection status shows "Connected"</li>
|
||||||
<li>Test basic actions (Say, Wave, Move)</li>
|
<li>Test basic actions (Say, Wave, Move)</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -182,84 +123,47 @@ export default function RobotIntegrationTutorial() {
|
|||||||
<h3>Speech Actions</h3>
|
<h3>Speech Actions</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr><th>Action</th><th>Parameters</th><th>Description</th></tr>
|
||||||
<th>Action</th>
|
|
||||||
<th>Parameters</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr><td>say_text</td><td>text</td><td>Speak text</td></tr>
|
||||||
<td>say_text</td>
|
<tr><td>say_with_emotion</td><td>text, emotion</td><td>Emotional speech</td></tr>
|
||||||
<td>text</td>
|
<tr><td>set_volume</td><td>level</td><td>Set speech volume</td></tr>
|
||||||
<td>Speak text</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>say_with_emotion</td>
|
|
||||||
<td>text, emotion</td>
|
|
||||||
<td>Emotional speech</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>set_volume</td>
|
|
||||||
<td>level</td>
|
|
||||||
<td>Set speech volume</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3>Movement Actions</h3>
|
<h3>Movement Actions</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr><th>Action</th><th>Parameters</th><th>Description</th></tr>
|
||||||
<th>Action</th>
|
|
||||||
<th>Parameters</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr><td>walk_forward</td><td>speed, duration</td><td>Walk forward</td></tr>
|
||||||
<td>walk_forward</td>
|
<tr><td>walk_backward</td><td>speed</td><td>Walk backward</td></tr>
|
||||||
<td>speed, duration</td>
|
<tr><td>turn_left</td><td>speed</td><td>Turn left</td></tr>
|
||||||
<td>Walk forward</td>
|
<tr><td>turn_right</td><td>speed</td><td>Turn right</td></tr>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>walk_backward</td>
|
|
||||||
<td>speed</td>
|
|
||||||
<td>Walk backward</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>turn_left</td>
|
|
||||||
<td>speed</td>
|
|
||||||
<td>Turn left</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>turn_right</td>
|
|
||||||
<td>speed</td>
|
|
||||||
<td>Turn right</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>Troubleshooting</h2>
|
<h2>Troubleshooting</h2>
|
||||||
|
|
||||||
<h3>Robot Not Found</h3>
|
<h3>Robot Not Found</h3>
|
||||||
<pre>
|
<pre><code>Error: Cannot connect to robot at 192.168.1.100
|
||||||
<code>
|
|
||||||
Error: Cannot connect to robot at 192.168.1.100 Solutions: 1. Verify
|
Solutions:
|
||||||
IP address: ping 192.168.1.100 2. Check robot is powered on 3. Verify
|
1. Verify IP address: ping 192.168.1.100
|
||||||
network connectivity 4. Try nao.local hostname
|
2. Check robot is powered on
|
||||||
</code>
|
3. Verify network connectivity
|
||||||
</pre>
|
4. Try nao.local hostname</code></pre>
|
||||||
|
|
||||||
<h3>WebSocket Connection Failed</h3>
|
<h3>WebSocket Connection Failed</h3>
|
||||||
<pre>
|
<pre><code>Error: WebSocket connection to ws://localhost:9090 failed
|
||||||
<code>
|
|
||||||
Error: WebSocket connection to ws://localhost:9090 failed Solutions:
|
Solutions:
|
||||||
1. Check Docker is running: docker ps 2. Verify ros_bridge container
|
1. Check Docker is running: docker ps
|
||||||
3. Check port 9090 is not blocked 4. Restart services: docker compose
|
2. Verify ros_bridge container
|
||||||
restart
|
3. Check port 9090 is not blocked
|
||||||
</code>
|
4. Restart services: docker compose restart</code></pre>
|
||||||
</pre>
|
|
||||||
|
|
||||||
<div className="mt-8 flex justify-between">
|
<div className="mt-8 flex justify-between">
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export default function NaoTestPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const publishMessage = (topic: string, type: string, msg: any) => {
|
const publishMessage = (topic: string, type: string, msg: any) => {
|
||||||
if (rosSocket?.readyState !== WebSocket.OPEN) {
|
if (!rosSocket || rosSocket.readyState !== WebSocket.OPEN) {
|
||||||
addLog("Error: Not connected to ROS bridge");
|
addLog("Error: Not connected to ROS bridge");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export default function FormViewPage({ params }: FormViewPageProps) {
|
|||||||
'<div style="margin-top: 4px;"><input type="radio" name="yn" /> Yes <input type="radio" name="yn" /> No</div>';
|
'<div style="margin-top: 4px;"><input type="radio" name="yn" /> Yes <input type="radio" name="yn" /> No</div>';
|
||||||
break;
|
break;
|
||||||
case "rating":
|
case "rating":
|
||||||
const scale = field.settings?.scale ?? 5;
|
const scale = (field.settings?.scale as number) || 5;
|
||||||
inputField = `<div style="margin-top: 4px;">${Array.from(
|
inputField = `<div style="margin-top: 4px;">${Array.from(
|
||||||
{ length: scale },
|
{ length: scale },
|
||||||
(_, i) => `<input type="radio" name="rating" /> ${i + 1} `,
|
(_, i) => `<input type="radio" name="rating" /> ${i + 1} `,
|
||||||
@@ -422,14 +422,12 @@ export default function FormViewPage({ params }: FormViewPageProps) {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{
|
{
|
||||||
FORM_FIELD_TYPES.find(
|
FORM_FIELD_TYPES.find((f) => f.value === field.type)
|
||||||
(f) => f.value === field.type,
|
?.icon
|
||||||
)?.icon
|
|
||||||
}{" "}
|
}{" "}
|
||||||
{
|
{
|
||||||
FORM_FIELD_TYPES.find(
|
FORM_FIELD_TYPES.find((f) => f.value === field.type)
|
||||||
(f) => f.value === field.type,
|
?.label
|
||||||
)?.label
|
|
||||||
}
|
}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Input
|
<Input
|
||||||
@@ -549,9 +547,8 @@ export default function FormViewPage({ params }: FormViewPageProps) {
|
|||||||
<p className="font-medium">{field.label}</p>
|
<p className="font-medium">{field.label}</p>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
{
|
{
|
||||||
FORM_FIELD_TYPES.find(
|
FORM_FIELD_TYPES.find((f) => f.value === field.type)
|
||||||
(f) => f.value === field.type,
|
?.label
|
||||||
)?.label
|
|
||||||
}
|
}
|
||||||
{field.required && " • Required"}
|
{field.required && " • Required"}
|
||||||
{field.type === "multiple_choice" &&
|
{field.type === "multiple_choice" &&
|
||||||
@@ -627,7 +624,7 @@ export default function FormViewPage({ params }: FormViewPageProps) {
|
|||||||
{field.type === "rating" && (
|
{field.type === "rating" && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{Array.from(
|
{Array.from(
|
||||||
{ length: field.settings?.scale ?? 5 },
|
{ length: (field.settings?.scale as number) || 5 },
|
||||||
(_, i) => (
|
(_, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
@@ -812,7 +809,7 @@ export default function FormViewPage({ params }: FormViewPageProps) {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{Array.from(
|
{Array.from(
|
||||||
{ length: field.settings?.scale ?? 5 },
|
{ length: (field.settings?.scale as number) || 5 },
|
||||||
(_, i) => (
|
(_, i) => (
|
||||||
<SelectItem key={i} value={String(i + 1)}>
|
<SelectItem key={i} value={String(i + 1)}>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
@@ -929,7 +926,7 @@ export default function FormViewPage({ params }: FormViewPageProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
className={`text-xs ${formStatusColors[response.status!]}`}
|
className={`text-xs ${formStatusColors[response.status as keyof typeof formStatusColors]}`}
|
||||||
>
|
>
|
||||||
{response.status}
|
{response.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default async function EditParticipantPage({
|
|||||||
|
|
||||||
const participant = await api.participants.get({ id: participantId });
|
const participant = await api.participants.get({ id: participantId });
|
||||||
|
|
||||||
if (participant?.studyId !== studyId) {
|
if (!participant || participant.studyId !== studyId) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "~/lib/auth";
|
import { auth } from "~/lib/auth";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { studyMembers } from "~/server/db/schema";
|
import { studyMembers } from "~/server/db/schema";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { wsManager } from "~/server/services/websocket-manager";
|
import { wsManager } from "~/server/services/websocket-manager";
|
||||||
import { auth } from "~/lib/auth";
|
import { auth } from "~/lib/auth";
|
||||||
@@ -48,7 +48,7 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const pair = new WebSocketPair();
|
const pair = new WebSocketPair();
|
||||||
const clientId = generateClientId();
|
const clientId = generateClientId();
|
||||||
const serverWebSocket = Object.values(pair)[0]!;
|
const serverWebSocket = Object.values(pair)[0] as WebSocket;
|
||||||
|
|
||||||
clientConnections.set(clientId, { socket: serverWebSocket, clientId });
|
clientConnections.set(clientId, { socket: serverWebSocket, clientId });
|
||||||
|
|
||||||
@@ -56,61 +56,59 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
serverWebSocket.accept();
|
serverWebSocket.accept();
|
||||||
|
|
||||||
serverWebSocket.addEventListener("message", (event) => {
|
serverWebSocket.addEventListener("message", async (event) => {
|
||||||
void (async () => {
|
try {
|
||||||
try {
|
const message = JSON.parse(event.data as string);
|
||||||
const message = JSON.parse(event.data as string);
|
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "heartbeat":
|
case "heartbeat":
|
||||||
wsManager.sendToClient(clientId, {
|
wsManager.sendToClient(clientId, {
|
||||||
type: "heartbeat_response",
|
type: "heartbeat_response",
|
||||||
data: { timestamp: Date.now() },
|
data: { timestamp: Date.now() },
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "request_trial_status": {
|
case "request_trial_status": {
|
||||||
const status = await wsManager.getTrialStatus(trialId);
|
const status = await wsManager.getTrialStatus(trialId);
|
||||||
wsManager.sendToClient(clientId, {
|
wsManager.sendToClient(clientId, {
|
||||||
type: "trial_status",
|
type: "trial_status",
|
||||||
data: {
|
data: {
|
||||||
trial: status?.trial ?? null,
|
trial: status?.trial ?? null,
|
||||||
current_step_index: status?.currentStepIndex ?? 0,
|
current_step_index: status?.currentStepIndex ?? 0,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case "request_trial_events": {
|
|
||||||
const events = await wsManager.getTrialEvents(
|
|
||||||
trialId,
|
|
||||||
message.data?.limit ?? 100,
|
|
||||||
);
|
|
||||||
wsManager.sendToClient(clientId, {
|
|
||||||
type: "trial_events_snapshot",
|
|
||||||
data: { events, timestamp: Date.now() },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ping":
|
|
||||||
wsManager.sendToClient(clientId, {
|
|
||||||
type: "pong",
|
|
||||||
data: { timestamp: Date.now() },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log(
|
|
||||||
`[WS] Unknown message type from client ${clientId}:`,
|
|
||||||
message.type,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(`[WS] Error processing message from ${clientId}:`, error);
|
case "request_trial_events": {
|
||||||
|
const events = await wsManager.getTrialEvents(
|
||||||
|
trialId,
|
||||||
|
message.data?.limit ?? 100,
|
||||||
|
);
|
||||||
|
wsManager.sendToClient(clientId, {
|
||||||
|
type: "trial_events_snapshot",
|
||||||
|
data: { events, timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ping":
|
||||||
|
wsManager.sendToClient(clientId, {
|
||||||
|
type: "pong",
|
||||||
|
data: { timestamp: Date.now() },
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(
|
||||||
|
`[WS] Unknown message type from client ${clientId}:`,
|
||||||
|
message.type,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})();
|
} catch (error) {
|
||||||
|
console.error(`[WS] Error processing message from ${clientId}:`, error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
serverWebSocket.addEventListener("close", () => {
|
serverWebSocket.addEventListener("close", () => {
|
||||||
|
|||||||
@@ -495,14 +495,12 @@ export function DesignerRoot({
|
|||||||
|
|
||||||
// console.log('[DesignerRoot] Steps changed, scheduling hash recomputation');
|
// console.log('[DesignerRoot] Steps changed, scheduling hash recomputation');
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(async () => {
|
||||||
void (async () => {
|
// console.log('[DesignerRoot] Executing debounced hash recomputation');
|
||||||
// console.log('[DesignerRoot] Executing debounced hash recomputation');
|
const result = await recomputeHash();
|
||||||
const result = await recomputeHash();
|
if (result) {
|
||||||
if (result) {
|
// console.log('[DesignerRoot] Hash recomputed:', result.designHash.slice(0, 16));
|
||||||
// console.log('[DesignerRoot] Hash recomputed:', result.designHash.slice(0, 16));
|
}
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, 300); // Debounce 300ms
|
}, 300); // Debounce 300ms
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
@@ -901,7 +899,7 @@ export function DesignerRoot({
|
|||||||
// Detect target based on over id
|
// Detect target based on over id
|
||||||
if (overId.startsWith("s-act-")) {
|
if (overId.startsWith("s-act-")) {
|
||||||
const data = over.data.current;
|
const data = over.data.current;
|
||||||
if (data?.stepId) {
|
if (data && data.stepId) {
|
||||||
stepId = data.stepId;
|
stepId = data.stepId;
|
||||||
parentId = data.parentId ?? null; // Use parentId from the action we are hovering over
|
parentId = data.parentId ?? null; // Use parentId from the action we are hovering over
|
||||||
// Use sortable index (insertion point provided by dnd-kit sortable strategy)
|
// Use sortable index (insertion point provided by dnd-kit sortable strategy)
|
||||||
@@ -910,7 +908,7 @@ export function DesignerRoot({
|
|||||||
} else if (overId.startsWith("container-")) {
|
} else if (overId.startsWith("container-")) {
|
||||||
// Dropping into a container (e.g. Loop)
|
// Dropping into a container (e.g. Loop)
|
||||||
const data = over.data.current;
|
const data = over.data.current;
|
||||||
if (data?.stepId) {
|
if (data && data.stepId) {
|
||||||
stepId = data.stepId;
|
stepId = data.stepId;
|
||||||
parentId = data.parentId ?? overId.slice("container-".length);
|
parentId = data.parentId ?? overId.slice("container-".length);
|
||||||
// If dropping into container, appending is a safe default if specific index logic is missing
|
// If dropping into container, appending is a safe default if specific index logic is missing
|
||||||
@@ -946,7 +944,12 @@ export function DesignerRoot({
|
|||||||
if (stepId) {
|
if (stepId) {
|
||||||
const current = store.insertionProjection;
|
const current = store.insertionProjection;
|
||||||
// Optimization: avoid redundant updates if projection matches
|
// Optimization: avoid redundant updates if projection matches
|
||||||
if (current?.stepId === stepId && current?.index === index) {
|
if (
|
||||||
|
current &&
|
||||||
|
current.stepId === stepId &&
|
||||||
|
current.parentId === parentId &&
|
||||||
|
current.index === index
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ export const createDesignerStore = (props: {
|
|||||||
reorderAction: (stepId: string, from: number, to: number) =>
|
reorderAction: (stepId: string, from: number, to: number) =>
|
||||||
get().moveAction(
|
get().moveAction(
|
||||||
stepId,
|
stepId,
|
||||||
get().steps.find((s) => s.id === stepId)?.actions[from]?.id as string,
|
get().steps.find((s) => s.id === stepId)?.actions[from]?.id!,
|
||||||
null,
|
null,
|
||||||
to,
|
to,
|
||||||
), // Legacy compat support (only works for root level reorder)
|
), // Legacy compat support (only works for root level reorder)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function FormFieldRenderer({
|
|||||||
className: error ? "border-destructive" : "",
|
className: error ? "border-destructive" : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const scale = field.settings?.scale ?? 5;
|
const scale = (field.settings?.scale as number) || 5;
|
||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case "text":
|
case "text":
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function ConsentUploadForm({
|
|||||||
const recordConsentMutation = api.participants.recordConsent.useMutation();
|
const recordConsentMutation = api.participants.recordConsent.useMutation();
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files?.[0]) {
|
if (e.target.files && e.target.files[0]) {
|
||||||
const selectedFile = e.target.files[0];
|
const selectedFile = e.target.files[0];
|
||||||
// Validate size (10MB)
|
// Validate size (10MB)
|
||||||
if (selectedFile.size > 10 * 1024 * 1024) {
|
if (selectedFile.size > 10 * 1024 * 1024) {
|
||||||
|
|||||||
@@ -246,10 +246,9 @@ export function DigitalSignatureModal({
|
|||||||
Agreement
|
Agreement
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||||
By clicking "Submit Signed Document", you confirm that
|
By clicking "Submit Signed Document", you confirm that you have
|
||||||
you have read and understood the information provided in the
|
read and understood the information provided in the document
|
||||||
document preview, and you voluntarily agree to participate in
|
preview, and you voluntarily agree to participate in this study.
|
||||||
this study.
|
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="mt-2 w-full"
|
className="mt-2 w-full"
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ export function EventsDataTable({ data, startTime }: EventsDataTableProps) {
|
|||||||
return null;
|
return null;
|
||||||
}, [events, currentEventIndex]);
|
}, [events, currentEventIndex]);
|
||||||
|
|
||||||
const rowRefs = React.useRef<Record<string, HTMLTableRowElement | null>>({});
|
const rowRefs = React.useRef<{ [key: string]: HTMLTableRowElement | null }>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (activeEventId && rowRefs.current[activeEventId]) {
|
if (activeEventId && rowRefs.current[activeEventId]) {
|
||||||
@@ -279,7 +281,7 @@ export function EventsDataTable({ data, startTime }: EventsDataTableProps) {
|
|||||||
<strong>{d?.command || d?.type || "Action"}</strong>
|
<strong>{d?.command || d?.type || "Action"}</strong>
|
||||||
{text && (
|
{text && (
|
||||||
<span className="text-muted-foreground ml-1">
|
<span className="text-muted-foreground ml-1">
|
||||||
"{text}"
|
"{text}"
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ export function ActionControls({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Step-Specific Controls */}
|
{/* Step-Specific Controls */}
|
||||||
{currentStep?.type === "wizard_action" && (
|
{currentStep && currentStep.type === "wizard_action" && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center space-x-2">
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function RobotSettingsModal({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize settings from current configuration
|
// Initialize settings from current configuration
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
useState(() => {
|
useState(() => {
|
||||||
if (currentSettings) {
|
if (currentSettings) {
|
||||||
setSettings(currentSettings as Record<string, unknown>);
|
setSettings(currentSettings as Record<string, unknown>);
|
||||||
@@ -103,7 +103,7 @@ export function RobotSettingsModal({
|
|||||||
const renderField = (
|
const renderField = (
|
||||||
key: string,
|
key: string,
|
||||||
schema: PropertySchema,
|
schema: PropertySchema,
|
||||||
parentPath = "",
|
parentPath: string = "",
|
||||||
) => {
|
) => {
|
||||||
const fullPath = parentPath ? `${parentPath}.${key}` : key;
|
const fullPath = parentPath ? `${parentPath}.${key}` : key;
|
||||||
const value = getNestedValue(settings, fullPath);
|
const value = getNestedValue(settings, fullPath);
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ export function WebcamPanel({
|
|||||||
|
|
||||||
const handleStartRecording = () => {
|
const handleStartRecording = () => {
|
||||||
if (!webcamRef.current?.stream) return;
|
if (!webcamRef.current?.stream) return;
|
||||||
if (mediaRecorderRef.current?.state === "recording") {
|
if (
|
||||||
|
mediaRecorderRef.current &&
|
||||||
|
mediaRecorderRef.current.state === "recording"
|
||||||
|
) {
|
||||||
console.log("Already recording, skipping start");
|
console.log("Already recording, skipping start");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ type StateListener = (state: GlobalWSState) => void;
|
|||||||
|
|
||||||
class GlobalWebSocketManager {
|
class GlobalWebSocketManager {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private subscriptions = new Map<string, Subscription>();
|
private subscriptions: Map<string, Subscription> = new Map();
|
||||||
private stateListeners = new Set<StateListener>();
|
private stateListeners: Set<StateListener> = new Set();
|
||||||
private sessionRef: { user: { id: string } } | null = null;
|
private sessionRef: { user: { id: string } } | null = null;
|
||||||
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
type WizardRosService,
|
WizardRosService,
|
||||||
type RobotStatus,
|
type RobotStatus,
|
||||||
type RobotActionExecution,
|
type RobotActionExecution,
|
||||||
getWizardRosService,
|
getWizardRosService,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const steps: ExperimentStep[] = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
const first = converted[0];
|
const first = converted[0];
|
||||||
if (first?.actions.length !== 2) {
|
if (!first || first.actions.length !== 2) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected first converted step to contain 2 actions, got ${first?.actions.length ?? "undefined"}`,
|
`Expected first converted step to contain 2 actions, got ${first?.actions.length ?? "undefined"}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export function convertDatabaseToAction(dbAction: any): ExperimentAction {
|
|||||||
|
|
||||||
// Robust Inference: If properties are missing but Type suggests a plugin (e.g., "nao6-ros2.say_text"),
|
// Robust Inference: If properties are missing but Type suggests a plugin (e.g., "nao6-ros2.say_text"),
|
||||||
// assume/infer the pluginId to ensure validation passes.
|
// assume/infer the pluginId to ensure validation passes.
|
||||||
if (dbAction.type?.includes(".") && !source.pluginId) {
|
if (dbAction.type && dbAction.type.includes(".") && !source.pluginId) {
|
||||||
const parts = dbAction.type.split(".");
|
const parts = dbAction.type.split(".");
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
source.kind = "plugin";
|
source.kind = "plugin";
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export async function downloadPdfFromHtml(
|
|||||||
htmlContent: string,
|
htmlContent: string,
|
||||||
options: PdfOptions = {},
|
options: PdfOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Dynamic import to prevent SSR issues with window/document
|
// @ts-ignore - Dynamic import to prevent SSR issues with window/document
|
||||||
const html2pdf = (await import("html2pdf.js")).default;
|
const html2pdf = (await import("html2pdf.js")).default;
|
||||||
|
|
||||||
const { printWrapper, element } = createPrintWrapper(htmlContent);
|
const { printWrapper, element } = createPrintWrapper(htmlContent);
|
||||||
@@ -61,7 +61,7 @@ export async function generatePdfBlobFromHtml(
|
|||||||
htmlContent: string,
|
htmlContent: string,
|
||||||
options: PdfOptions = {},
|
options: PdfOptions = {},
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
// Dynamic import to prevent SSR issues with window/document
|
// @ts-ignore - Dynamic import to prevent SSR issues with window/document
|
||||||
const html2pdf = (await import("html2pdf.js")).default;
|
const html2pdf = (await import("html2pdf.js")).default;
|
||||||
|
|
||||||
const { printWrapper, element } = createPrintWrapper(htmlContent);
|
const { printWrapper, element } = createPrintWrapper(htmlContent);
|
||||||
|
|||||||
@@ -90,15 +90,13 @@ export class WizardRosService extends EventEmitter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Active action tracking
|
// Active action tracking
|
||||||
private activeActions = new Map<string, RobotActionExecution>();
|
private activeActions: Map<string, RobotActionExecution> = new Map();
|
||||||
|
|
||||||
constructor(url = "ws://localhost:9090", simulationMode = false) {
|
constructor(url: string = "ws://localhost:9090", simulationMode: boolean = false) {
|
||||||
super();
|
super();
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.simulationMode =
|
this.simulationMode = simulationMode ||
|
||||||
simulationMode ||
|
(typeof window !== "undefined" && process.env.NEXT_PUBLIC_SIMULATION_MODE === "true");
|
||||||
(typeof window !== "undefined" &&
|
|
||||||
process.env.NEXT_PUBLIC_SIMULATION_MODE === "true");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,13 +242,10 @@ export class WizardRosService extends EventEmitter {
|
|||||||
connected: true,
|
connected: true,
|
||||||
battery: 85,
|
battery: 85,
|
||||||
position: { x: 0, y: 0, theta: 0 },
|
position: { x: 0, y: 0, theta: 0 },
|
||||||
joints: mockStates.names.reduce(
|
joints: mockStates.names.reduce((acc, name, i) => {
|
||||||
(acc, name, i) => {
|
acc[name] = mockStates.positions[i] ?? 0;
|
||||||
acc[name] = mockStates.positions[i] ?? 0;
|
return acc;
|
||||||
return acc;
|
}, {} as Record<string, number>),
|
||||||
},
|
|
||||||
{} as Record<string, number>,
|
|
||||||
),
|
|
||||||
sensors: {},
|
sensors: {},
|
||||||
lastUpdate: new Date(),
|
lastUpdate: new Date(),
|
||||||
};
|
};
|
||||||
@@ -303,32 +298,11 @@ export class WizardRosService extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private getMockJointStates(): { names: string[]; positions: number[] } {
|
private getMockJointStates(): { names: string[]; positions: number[] } {
|
||||||
const names = [
|
const names = [
|
||||||
"HeadYaw",
|
"HeadYaw", "HeadPitch",
|
||||||
"HeadPitch",
|
"LShoulderPitch", "LShoulderRoll", "LElbowYaw", "LElbowRoll", "LWristYaw", "LHand",
|
||||||
"LShoulderPitch",
|
"RShoulderPitch", "RShoulderRoll", "RElbowYaw", "RElbowRoll", "RWristYaw", "RHand",
|
||||||
"LShoulderRoll",
|
"LHipYawPitch", "LHipRoll", "LHipPitch", "LKneePitch", "LAnklePitch", "LAnkleRoll",
|
||||||
"LElbowYaw",
|
"RHipYawPitch", "RHipRoll", "RHipPitch", "RKneePitch", "RAnklePitch", "RAnkleRoll",
|
||||||
"LElbowRoll",
|
|
||||||
"LWristYaw",
|
|
||||||
"LHand",
|
|
||||||
"RShoulderPitch",
|
|
||||||
"RShoulderRoll",
|
|
||||||
"RElbowYaw",
|
|
||||||
"RElbowRoll",
|
|
||||||
"RWristYaw",
|
|
||||||
"RHand",
|
|
||||||
"LHipYawPitch",
|
|
||||||
"LHipRoll",
|
|
||||||
"LHipPitch",
|
|
||||||
"LKneePitch",
|
|
||||||
"LAnklePitch",
|
|
||||||
"LAnkleRoll",
|
|
||||||
"RHipYawPitch",
|
|
||||||
"RHipRoll",
|
|
||||||
"RHipPitch",
|
|
||||||
"RKneePitch",
|
|
||||||
"RAnklePitch",
|
|
||||||
"RAnkleRoll",
|
|
||||||
];
|
];
|
||||||
const positions = names.map(() => (Math.random() - 0.5) * 0.1);
|
const positions = names.map(() => (Math.random() - 0.5) * 0.1);
|
||||||
return { names, positions };
|
return { names, positions };
|
||||||
@@ -368,17 +342,11 @@ export class WizardRosService extends EventEmitter {
|
|||||||
execution.status = "executing";
|
execution.status = "executing";
|
||||||
this.activeActions.set(executionId, execution);
|
this.activeActions.set(executionId, execution);
|
||||||
|
|
||||||
console.log(
|
console.log(`[WizardROS] SIMULATION MODE - Executing ${actionId}:`, parameters);
|
||||||
`[WizardROS] SIMULATION MODE - Executing ${actionId}:`,
|
|
||||||
parameters,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the action config carries a gesture_sequence payload, run the sim animation handler
|
// If the action config carries a gesture_sequence payload, run the sim animation handler
|
||||||
if (actionConfig?.payloadMapping?.payload) {
|
if (actionConfig?.payloadMapping?.payload) {
|
||||||
const payload = actionConfig.payloadMapping.payload as {
|
const payload = actionConfig.payloadMapping.payload as { type?: string; movements?: AnimationMovement[] };
|
||||||
type?: string;
|
|
||||||
movements?: AnimationMovement[];
|
|
||||||
};
|
|
||||||
if (payload.type === "gesture_sequence" && payload.movements?.length) {
|
if (payload.type === "gesture_sequence" && payload.movements?.length) {
|
||||||
await this.executeSimulationAnimationSequence(payload.movements);
|
await this.executeSimulationAnimationSequence(payload.movements);
|
||||||
execution.status = "completed";
|
execution.status = "completed";
|
||||||
@@ -392,19 +360,11 @@ export class WizardRosService extends EventEmitter {
|
|||||||
// Simulate action execution based on action type
|
// Simulate action execution based on action type
|
||||||
let duration = 500;
|
let duration = 500;
|
||||||
|
|
||||||
if (
|
if (actionId === "say_text" || actionId === "say_with_emotion" || actionConfig?.topic === "/speech") {
|
||||||
actionId === "say_text" ||
|
|
||||||
actionId === "say_with_emotion" ||
|
|
||||||
actionConfig?.topic === "/speech"
|
|
||||||
) {
|
|
||||||
const text = String(parameters.text || parameters.data || "Hello");
|
const text = String(parameters.text || parameters.data || "Hello");
|
||||||
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
||||||
duration = 1500 + Math.max(1000, wordCount * 300);
|
duration = 1500 + Math.max(1000, wordCount * 300);
|
||||||
} else if (
|
} else if (actionId.includes("walk") || actionId === "stop_walking" || actionConfig?.topic === "/cmd_vel") {
|
||||||
actionId.includes("walk") ||
|
|
||||||
actionId === "stop_walking" ||
|
|
||||||
actionConfig?.topic === "/cmd_vel"
|
|
||||||
) {
|
|
||||||
duration = 500;
|
duration = 500;
|
||||||
const speed = Number(parameters.speed) || 0.1;
|
const speed = Number(parameters.speed) || 0.1;
|
||||||
if (actionId === "walk_forward") {
|
if (actionId === "walk_forward") {
|
||||||
@@ -418,11 +378,7 @@ export class WizardRosService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
} else if (actionConfig?.topic === "/joint_angles") {
|
} else if (actionConfig?.topic === "/joint_angles") {
|
||||||
duration = 1000;
|
duration = 1000;
|
||||||
} else if (
|
} else if (actionId === "wake_up" || actionId === "rest" || actionId === "set_posture") {
|
||||||
actionId === "wake_up" ||
|
|
||||||
actionId === "rest" ||
|
|
||||||
actionId === "set_posture"
|
|
||||||
) {
|
|
||||||
duration = 2000;
|
duration = 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +399,8 @@ export class WizardRosService extends EventEmitter {
|
|||||||
return execution;
|
return execution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if connected to ROS bridge
|
* Check if connected to ROS bridge
|
||||||
*/
|
*/
|
||||||
@@ -483,12 +441,7 @@ export class WizardRosService extends EventEmitter {
|
|||||||
|
|
||||||
// Simulation mode - simulate action execution
|
// Simulation mode - simulate action execution
|
||||||
if (this.simulationMode) {
|
if (this.simulationMode) {
|
||||||
return this.executeSimulationAction(
|
return this.executeSimulationAction(pluginName, actionId, parameters, actionConfig);
|
||||||
pluginName,
|
|
||||||
actionId,
|
|
||||||
parameters,
|
|
||||||
actionConfig,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionId = `${pluginName}_${actionId}_${Date.now()}`;
|
const executionId = `${pluginName}_${actionId}_${Date.now()}`;
|
||||||
@@ -541,13 +494,9 @@ export class WizardRosService extends EventEmitter {
|
|||||||
* Each frame is published to /joint_angles then held for delay_after ms
|
* Each frame is published to /joint_angles then held for delay_after ms
|
||||||
* (default 800 ms) before the next frame is sent.
|
* (default 800 ms) before the next frame is sent.
|
||||||
*/
|
*/
|
||||||
async executeAnimationSequence(
|
async executeAnimationSequence(movements: AnimationMovement[]): Promise<void> {
|
||||||
movements: AnimationMovement[],
|
|
||||||
): Promise<void> {
|
|
||||||
if (!movements.length) {
|
if (!movements.length) {
|
||||||
console.warn(
|
console.warn("[WizardROS] executeAnimationSequence called with empty movements");
|
||||||
"[WizardROS] executeAnimationSequence called with empty movements",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,10 +563,7 @@ export class WizardRosService extends EventEmitter {
|
|||||||
|
|
||||||
this.advertise("/speech", "std_msgs/String");
|
this.advertise("/speech", "std_msgs/String");
|
||||||
this.advertise("/cmd_vel", "geometry_msgs/Twist");
|
this.advertise("/cmd_vel", "geometry_msgs/Twist");
|
||||||
this.advertise(
|
this.advertise("/joint_angles", "naoqi_bridge_msgs/msg/JointAnglesWithSpeed");
|
||||||
"/joint_angles",
|
|
||||||
"naoqi_bridge_msgs/msg/JointAnglesWithSpeed",
|
|
||||||
);
|
|
||||||
this.advertise("/robot_pose", "geometry_msgs/Pose");
|
this.advertise("/robot_pose", "geometry_msgs/Pose");
|
||||||
this.advertise("/animation", "std_msgs/String");
|
this.advertise("/animation", "std_msgs/String");
|
||||||
}
|
}
|
||||||
@@ -779,10 +725,7 @@ export class WizardRosService extends EventEmitter {
|
|||||||
actionId?: string,
|
actionId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// SSH command actions
|
// SSH command actions
|
||||||
if (
|
if (config.payloadMapping.type === "ssh" && config.payloadMapping.sshCommand) {
|
||||||
config.payloadMapping.type === "ssh" &&
|
|
||||||
config.payloadMapping.sshCommand
|
|
||||||
) {
|
|
||||||
await this.executeSSHCommand(config.payloadMapping.sshCommand);
|
await this.executeSSHCommand(config.payloadMapping.sshCommand);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -804,15 +747,9 @@ export class WizardRosService extends EventEmitter {
|
|||||||
config.payloadMapping.type === "static") &&
|
config.payloadMapping.type === "static") &&
|
||||||
config.payloadMapping.payload
|
config.payloadMapping.payload
|
||||||
) {
|
) {
|
||||||
msg = this.buildTemplatePayload(
|
msg = this.buildTemplatePayload(config.payloadMapping.payload, parameters);
|
||||||
config.payloadMapping.payload,
|
|
||||||
parameters,
|
|
||||||
);
|
|
||||||
} else if (config.payloadMapping.transformFn) {
|
} else if (config.payloadMapping.transformFn) {
|
||||||
msg = this.applyTransformFunction(
|
msg = this.applyTransformFunction(config.payloadMapping.transformFn, parameters);
|
||||||
config.payloadMapping.transformFn,
|
|
||||||
parameters,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
msg = parameters;
|
msg = parameters;
|
||||||
}
|
}
|
||||||
@@ -824,18 +761,13 @@ export class WizardRosService extends EventEmitter {
|
|||||||
console.warn("[WizardROS] gesture_sequence payload has no movements");
|
console.warn("[WizardROS] gesture_sequence payload has no movements");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(`[WizardROS] Delegating to animation handler (${movements.length} frames)`);
|
||||||
`[WizardROS] Delegating to animation handler (${movements.length} frames)`,
|
|
||||||
);
|
|
||||||
await this.executeAnimationSequence(movements);
|
await this.executeAnimationSequence(movements);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route /animation topic through SSH instead of ROS to avoid crashes
|
// Route /animation topic through SSH instead of ROS to avoid crashes
|
||||||
if (
|
if (config.topic === "/animation" && actionId?.startsWith("play_animation_")) {
|
||||||
config.topic === "/animation" &&
|
|
||||||
actionId?.startsWith("play_animation_")
|
|
||||||
) {
|
|
||||||
await this.executeAnimationSSH(actionId);
|
await this.executeAnimationSSH(actionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1037,10 +969,7 @@ export class WizardRosService extends EventEmitter {
|
|||||||
|
|
||||||
// Simulation mode - return mock responses
|
// Simulation mode - return mock responses
|
||||||
if (this.simulationMode) {
|
if (this.simulationMode) {
|
||||||
console.log(
|
console.log(`[WizardROS] SIMULATION MODE - Service call: ${service}`, args);
|
||||||
`[WizardROS] SIMULATION MODE - Service call: ${service}`,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockResponses: Record<string, ServiceResponse> = {
|
const mockResponses: Record<string, ServiceResponse> = {
|
||||||
"/naoqi_driver/get_robot_info": {
|
"/naoqi_driver/get_robot_info": {
|
||||||
@@ -1055,32 +984,13 @@ export class WizardRosService extends EventEmitter {
|
|||||||
result: true,
|
result: true,
|
||||||
values: {
|
values: {
|
||||||
joint_names: [
|
joint_names: [
|
||||||
"HeadYaw",
|
"HeadYaw", "HeadPitch", "LShoulderPitch", "LShoulderRoll",
|
||||||
"HeadPitch",
|
"LElbowYaw", "LElbowRoll", "LWristYaw", "LHand",
|
||||||
"LShoulderPitch",
|
"RShoulderPitch", "RShoulderRoll", "RElbowYaw", "RElbowRoll",
|
||||||
"LShoulderRoll",
|
"RWristYaw", "RHand", "LHipYawPitch", "LHipRoll",
|
||||||
"LElbowYaw",
|
"LHipPitch", "LKneePitch", "LAnklePitch", "LAnkleRoll",
|
||||||
"LElbowRoll",
|
"RHipYawPitch", "RHipRoll", "RHipPitch", "RKneePitch",
|
||||||
"LWristYaw",
|
"RAnklePitch", "RAnkleRoll",
|
||||||
"LHand",
|
|
||||||
"RShoulderPitch",
|
|
||||||
"RShoulderRoll",
|
|
||||||
"RElbowYaw",
|
|
||||||
"RElbowRoll",
|
|
||||||
"RWristYaw",
|
|
||||||
"RHand",
|
|
||||||
"LHipYawPitch",
|
|
||||||
"LHipRoll",
|
|
||||||
"LHipPitch",
|
|
||||||
"LKneePitch",
|
|
||||||
"LAnklePitch",
|
|
||||||
"LAnkleRoll",
|
|
||||||
"RHipYawPitch",
|
|
||||||
"RHipRoll",
|
|
||||||
"RHipPitch",
|
|
||||||
"RKneePitch",
|
|
||||||
"RAnklePitch",
|
|
||||||
"RAnkleRoll",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1131,13 +1041,13 @@ export class WizardRosService extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private async executeAnimationSSH(actionId: string): Promise<void> {
|
private async executeAnimationSSH(actionId: string): Promise<void> {
|
||||||
const animationMap: Record<string, string> = {
|
const animationMap: Record<string, string> = {
|
||||||
play_animation_bow: "animations/Stand/Gestures/BowShort_1",
|
"play_animation_bow": "animations/Stand/Gestures/BowShort_1",
|
||||||
play_animation_hey: "animations/Stand/Gestures/Hey_1",
|
"play_animation_hey": "animations/Stand/Gestures/Hey_1",
|
||||||
play_animation_show_floor: "animations/Stand/Gestures/ShowFloor_1",
|
"play_animation_show_floor": "animations/Stand/Gestures/ShowFloor_1",
|
||||||
play_animation_enthusiastic: "animations/Stand/Gestures/Enthusiastic_4",
|
"play_animation_enthusiastic": "animations/Stand/Gestures/Enthusiastic_4",
|
||||||
play_animation_yes: "animations/Stand/Gestures/Yes_1",
|
"play_animation_yes": "animations/Stand/Gestures/Yes_1",
|
||||||
play_animation_no: "animations/Stand/Gestures/No_3",
|
"play_animation_no": "animations/Stand/Gestures/No_3",
|
||||||
play_animation_idontknow: "animations/Stand/Gestures/IDontKnow_1",
|
"play_animation_idontknow": "animations/Stand/Gestures/IDontKnow_1",
|
||||||
};
|
};
|
||||||
|
|
||||||
const animation = animationMap[actionId];
|
const animation = animationMap[actionId];
|
||||||
@@ -1148,9 +1058,7 @@ export class WizardRosService extends EventEmitter {
|
|||||||
console.log(`[WizardROS] Executing animation via API: ${animation}`);
|
console.log(`[WizardROS] Executing animation via API: ${animation}`);
|
||||||
|
|
||||||
// Use executeSSH to run animation via qicli (bypasses studyId requirement)
|
// Use executeSSH to run animation via qicli (bypasses studyId requirement)
|
||||||
await this.executeSSHCommand(
|
await this.executeSSHCommand(`qicli call ALAnimationPlayer.run '${animation}'`);
|
||||||
`qicli call ALAnimationPlayer.run '${animation}'`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[WizardROS] Animation completed: ${animation}`);
|
console.log(`[WizardROS] Animation completed: ${animation}`);
|
||||||
}
|
}
|
||||||
@@ -1455,20 +1363,18 @@ export class WizardRosService extends EventEmitter {
|
|||||||
`[WizardROS] Scheduling reconnect attempt ${this.connectionAttempts}/${this.maxReconnectAttempts}`,
|
`[WizardROS] Scheduling reconnect attempt ${this.connectionAttempts}/${this.maxReconnectAttempts}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.reconnectTimer = setTimeout(() => {
|
this.reconnectTimer = setTimeout(async () => {
|
||||||
void (async () => {
|
this.reconnectTimer = null;
|
||||||
this.reconnectTimer = null;
|
try {
|
||||||
try {
|
await this.connect();
|
||||||
await this.connect();
|
} catch (error) {
|
||||||
} catch (error) {
|
console.warn("[WizardROS] Reconnect failed:", error);
|
||||||
console.warn("[WizardROS] Reconnect failed:", error);
|
if (this.connectionAttempts < this.maxReconnectAttempts) {
|
||||||
if (this.connectionAttempts < this.maxReconnectAttempts) {
|
this.scheduleReconnect();
|
||||||
this.scheduleReconnect();
|
} else {
|
||||||
} else {
|
this.emit("max_reconnects_reached");
|
||||||
this.emit("max_reconnects_reached");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
}, this.reconnectInterval);
|
}, this.reconnectInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,9 +1396,7 @@ let isCreatingInstance = false;
|
|||||||
/**
|
/**
|
||||||
* Get or create the global wizard ROS service (true singleton)
|
* Get or create the global wizard ROS service (true singleton)
|
||||||
*/
|
*/
|
||||||
export function getWizardRosService(
|
export function getWizardRosService(simulationMode?: boolean): WizardRosService {
|
||||||
simulationMode?: boolean,
|
|
||||||
): WizardRosService {
|
|
||||||
// Prevent multiple instances during creation
|
// Prevent multiple instances during creation
|
||||||
if (isCreatingInstance && !wizardRosService) {
|
if (isCreatingInstance && !wizardRosService) {
|
||||||
throw new Error("WizardRosService is being initialized, please wait");
|
throw new Error("WizardRosService is being initialized, please wait");
|
||||||
@@ -1501,10 +1405,9 @@ export function getWizardRosService(
|
|||||||
if (!wizardRosService) {
|
if (!wizardRosService) {
|
||||||
isCreatingInstance = true;
|
isCreatingInstance = true;
|
||||||
try {
|
try {
|
||||||
const url =
|
const url = typeof window !== "undefined"
|
||||||
typeof window !== "undefined"
|
? (process.env.NEXT_PUBLIC_ROS_BRIDGE_URL || "ws://localhost:9090")
|
||||||
? process.env.NEXT_PUBLIC_ROS_BRIDGE_URL || "ws://localhost:9090"
|
: "ws://localhost:9090";
|
||||||
: "ws://localhost:9090";
|
|
||||||
wizardRosService = new WizardRosService(url, simulationMode);
|
wizardRosService = new WizardRosService(url, simulationMode);
|
||||||
} finally {
|
} finally {
|
||||||
isCreatingInstance = false;
|
isCreatingInstance = false;
|
||||||
@@ -1516,9 +1419,7 @@ export function getWizardRosService(
|
|||||||
/**
|
/**
|
||||||
* Initialize wizard ROS service with connection
|
* Initialize wizard ROS service with connection
|
||||||
*/
|
*/
|
||||||
export async function initWizardRosService(
|
export async function initWizardRosService(simulationMode?: boolean): Promise<WizardRosService> {
|
||||||
simulationMode?: boolean,
|
|
||||||
): Promise<WizardRosService> {
|
|
||||||
const service = getWizardRosService(simulationMode);
|
const service = getWizardRosService(simulationMode);
|
||||||
|
|
||||||
if (simulationMode !== undefined) {
|
if (simulationMode !== undefined) {
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ export const studiesRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (membership?.role !== "owner") {
|
if (!membership || membership.role !== "owner") {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Only study owners can remove members",
|
message: "Only study owners can remove members",
|
||||||
@@ -521,7 +521,7 @@ export const studiesRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (memberToRemove?.studyId !== studyId) {
|
if (!memberToRemove || memberToRemove.studyId !== studyId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "Member not found",
|
message: "Member not found",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
@@ -193,9 +193,8 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
: actionType;
|
: actionType;
|
||||||
|
|
||||||
const isAnimationAction = baseActionId?.startsWith("play_animation_");
|
const isAnimationAction = baseActionId?.startsWith("play_animation_");
|
||||||
const sshCommand =
|
const sshCommand = implementation.payloadMapping?.sshCommand
|
||||||
implementation.payloadMapping?.sshCommand ||
|
|| implementation.ros2?.payloadMapping?.sshCommand;
|
||||||
implementation.ros2?.payloadMapping?.sshCommand;
|
|
||||||
|
|
||||||
// SSH actions don't require ROS connection
|
// SSH actions don't require ROS connection
|
||||||
if (isAnimationAction || sshCommand) {
|
if (isAnimationAction || sshCommand) {
|
||||||
@@ -205,11 +204,7 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[RobotComm] Executing SSH action: ${action.actionId}`);
|
console.log(`[RobotComm] Executing SSH action: ${action.actionId}`);
|
||||||
const result = await this.executeRobotActionInternal(
|
const result = await this.executeRobotActionInternal(action, actionId, startTime);
|
||||||
action,
|
|
||||||
actionId,
|
|
||||||
startTime,
|
|
||||||
);
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -287,9 +282,8 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for SSH command type
|
// Check for SSH command type
|
||||||
const sshCommand =
|
const sshCommand = implementation.payloadMapping?.sshCommand
|
||||||
implementation.payloadMapping?.sshCommand ||
|
|| implementation.ros2?.payloadMapping?.sshCommand;
|
||||||
implementation.ros2?.payloadMapping?.sshCommand;
|
|
||||||
|
|
||||||
if (sshCommand) {
|
if (sshCommand) {
|
||||||
await this.executeSSHCommand(sshCommand);
|
await this.executeSSHCommand(sshCommand);
|
||||||
@@ -302,9 +296,8 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
|
|
||||||
// Apply transform if specified
|
// Apply transform if specified
|
||||||
let message: Record<string, unknown>;
|
let message: Record<string, unknown>;
|
||||||
const transformFn =
|
const transformFn = implementation.payloadMapping?.transformFn
|
||||||
implementation.payloadMapping?.transformFn ||
|
|| implementation.ros2?.payloadMapping?.transformFn;
|
||||||
implementation.ros2?.payloadMapping?.transformFn;
|
|
||||||
|
|
||||||
if (transformFn) {
|
if (transformFn) {
|
||||||
message = this.applyTransform(transformFn, parameters);
|
message = this.applyTransform(transformFn, parameters);
|
||||||
@@ -359,13 +352,13 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
|
|
||||||
private async executeAnimationViaSSH(actionType: string): Promise<void> {
|
private async executeAnimationViaSSH(actionType: string): Promise<void> {
|
||||||
const animationMap: Record<string, string> = {
|
const animationMap: Record<string, string> = {
|
||||||
play_animation_bow: "animations/Stand/Gestures/BowShort_1",
|
"play_animation_bow": "animations/Stand/Gestures/BowShort_1",
|
||||||
play_animation_hey: "animations/Stand/Gestures/Hey_1",
|
"play_animation_hey": "animations/Stand/Gestures/Hey_1",
|
||||||
play_animation_show_floor: "animations/Stand/Gestures/ShowFloor_1",
|
"play_animation_show_floor": "animations/Stand/Gestures/ShowFloor_1",
|
||||||
play_animation_enthusiastic: "animations/Stand/Gestures/Enthusiastic_4",
|
"play_animation_enthusiastic": "animations/Stand/Gestures/Enthusiastic_4",
|
||||||
play_animation_yes: "animations/Stand/Gestures/Yes_1",
|
"play_animation_yes": "animations/Stand/Gestures/Yes_1",
|
||||||
play_animation_no: "animations/Stand/Gestures/No_3",
|
"play_animation_no": "animations/Stand/Gestures/No_3",
|
||||||
play_animation_idontknow: "animations/Stand/Gestures/IDontKnow_1",
|
"play_animation_idontknow": "animations/Stand/Gestures/IDontKnow_1",
|
||||||
};
|
};
|
||||||
|
|
||||||
const animation = animationMap[actionType];
|
const animation = animationMap[actionType];
|
||||||
@@ -389,9 +382,7 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
console.log(`[RobotComm] Animation result: ${stdout}`);
|
console.log(`[RobotComm] Animation result: ${stdout}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformToEmotionalSpeech(parameters: Record<string, unknown>): {
|
private transformToEmotionalSpeech(parameters: Record<string, unknown>): { data: string } {
|
||||||
data: string;
|
|
||||||
} {
|
|
||||||
const text = String(parameters.text || "Hello");
|
const text = String(parameters.text || "Hello");
|
||||||
const emotion = String(parameters.emotion || "neutral");
|
const emotion = String(parameters.emotion || "neutral");
|
||||||
|
|
||||||
@@ -406,10 +397,7 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyTransform(
|
private applyTransform(transformFn: string, parameters: Record<string, unknown>): Record<string, unknown> {
|
||||||
transformFn: string,
|
|
||||||
parameters: Record<string, unknown>,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
switch (transformFn) {
|
switch (transformFn) {
|
||||||
case "transformToEmotionalSpeech":
|
case "transformToEmotionalSpeech":
|
||||||
case "transformToEmotionSpeech":
|
case "transformToEmotionSpeech":
|
||||||
@@ -452,8 +440,8 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
substituted.includes(":")
|
substituted.includes(":")
|
||||||
) {
|
) {
|
||||||
// Simple conditional: {{condition ? valueTrue : valueFalse}}
|
// Simple conditional: {{condition ? valueTrue : valueFalse}}
|
||||||
const match = /\{\{(.+?)\s*\?\s*(.+?)\s*:\s*(.+?)\}\}/.exec(
|
const match = substituted.match(
|
||||||
substituted,
|
/\{\{(.+?)\s*\?\s*(.+?)\s*:\s*(.+?)\}\}/,
|
||||||
);
|
);
|
||||||
if (match && match.length >= 4) {
|
if (match && match.length >= 4) {
|
||||||
const condition = match[1];
|
const condition = match[1];
|
||||||
@@ -599,23 +587,21 @@ export class RobotCommunicationService extends EventEmitter {
|
|||||||
`[RobotComm] Scheduling reconnect attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} in ${this.config.reconnectInterval}ms`,
|
`[RobotComm] Scheduling reconnect attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} in ${this.config.reconnectInterval}ms`,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.reconnectTimer = setTimeout(() => {
|
this.reconnectTimer = setTimeout(async () => {
|
||||||
void (async () => {
|
this.reconnectTimer = null;
|
||||||
this.reconnectTimer = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.connect();
|
await this.connect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[RobotComm] Reconnect failed:", error);
|
console.error("[RobotComm] Reconnect failed:", error);
|
||||||
|
|
||||||
if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
|
if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
} else {
|
} else {
|
||||||
console.error("[RobotComm] Max reconnect attempts reached");
|
console.error("[RobotComm] Max reconnect attempts reached");
|
||||||
this.emit("max_reconnects_reached");
|
this.emit("max_reconnects_reached");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
}, this.config.reconnectInterval);
|
}, this.config.reconnectInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ type OutgoingMessage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class WebSocketManager {
|
class WebSocketManager {
|
||||||
private clients = new Map<string, ClientConnection>();
|
private clients: Map<string, ClientConnection> = new Map();
|
||||||
private heartbeatIntervals =
|
private heartbeatIntervals: Map<string, ReturnType<typeof setInterval>> =
|
||||||
new Map<string, ReturnType<typeof setInterval>>();
|
new Map();
|
||||||
|
|
||||||
private getTrialRoomClients(trialId: string): ClientConnection[] {
|
private getTrialRoomClients(trialId: string): ClientConnection[] {
|
||||||
const clients: ClientConnection[] = [];
|
const clients: ClientConnection[] = [];
|
||||||
@@ -224,7 +224,7 @@ class WebSocketManager {
|
|||||||
|
|
||||||
async getTrialEvents(
|
async getTrialEvents(
|
||||||
trialId: string,
|
trialId: string,
|
||||||
limit = 100,
|
limit: number = 100,
|
||||||
): Promise<unknown[]> {
|
): Promise<unknown[]> {
|
||||||
const events = await db
|
const events = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -248,7 +248,7 @@ class WebSocketManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTrialEventsSync(trialId: string, limit = 100): unknown[] {
|
getTrialEventsSync(trialId: string, limit: number = 100): unknown[] {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user