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