diff --git a/.env.example b/.env.example old mode 100644 new mode 100755 index 7bf5a31..ae2e482 --- a/.env.example +++ b/.env.example @@ -17,3 +17,10 @@ AUTH_SECRET="" # Drizzle DATABASE_URL="postgresql://postgres:password@localhost:5433/hristudio" + +# MinIO/S3 Configuration +MINIO_ENDPOINT="http://localhost:9000" +MINIO_REGION="us-east-1" +MINIO_ACCESS_KEY="minioadmin" +MINIO_SECRET_KEY="minioadmin" +MINIO_BUCKET_NAME="hristudio-data" diff --git a/.eslintrc.autofix.js b/.eslintrc.autofix.js old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.rules b/.rules old mode 100644 new mode 100755 diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..c59005b --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,84 @@ +# HRIStudio Documentation Overview + +Clean, organized documentation for the HRIStudio platform. + +## Quick Links + +### Getting Started +- **[README.md](README.md)** - Main project overview and setup +- **[Quick Reference](docs/quick-reference.md)** - 5-minute setup guide + +### HRIStudio Platform +- **[Project Overview](docs/project-overview.md)** - Features and architecture +- **[Database Schema](docs/database-schema.md)** - Complete database reference +- **[API Routes](docs/api-routes.md)** - tRPC API documentation +- **[Implementation Guide](docs/implementation-guide.md)** - Technical implementation + +### NAO6 Robot Integration +- **[NAO6 Quick Reference](docs/nao6-quick-reference.md)** - Essential commands +- **[Integration Repository](../nao6-hristudio-integration/)** - Complete integration package + - Installation guide + - Usage instructions + - Troubleshooting + - Plugin definitions + +### Experiment Design +- **[Core Blocks System](docs/core-blocks-system.md)** - Experiment building blocks +- **[Plugin System](docs/plugin-system-implementation-guide.md)** - Robot plugins + +### Deployment +- **[Deployment & Operations](docs/deployment-operations.md)** - Production deployment + +### Research +- **[Research Paper](docs/paper.md)** - Academic documentation + +## Repository Structure + +``` +hristudio/ # Main web application +├── README.md # Start here +├── DOCUMENTATION.md # This file +├── src/ # Next.js application +├── docs/ # Platform documentation +└── ... + +nao6-hristudio-integration/ # NAO6 integration +├── README.md # Integration overview +├── docs/ # NAO6 documentation +├── launch/ # ROS2 launch files +├── scripts/ # Utility scripts +├── plugins/ # Plugin definitions +└── examples/ # Usage examples +``` + +## Documentation Philosophy + +- **One source of truth** - No duplicate docs +- **Clear hierarchy** - Easy to find what you need +- **Practical focus** - Real commands, not theory +- **Examples** - Working code samples + +## For Researchers + +Start here: +1. [README.md](README.md) - Setup HRIStudio +2. [NAO6 Quick Reference](docs/nao6-quick-reference.md) - Connect NAO robot +3. [Project Overview](docs/project-overview.md) - Understand the system + +## For Developers + +Start here: +1. [Implementation Guide](docs/implementation-guide.md) - Technical architecture +2. [Database Schema](docs/database-schema.md) - Data model +3. [API Routes](docs/api-routes.md) - Backend APIs + +## Support + +- Check documentation first +- Use NAO6 integration repo for robot-specific issues +- Main HRIStudio repo for platform issues + +--- + +**Last Updated:** December 2024 +**Version:** 1.0 (Simplified) diff --git a/HANDOFF-NAO6-INTEGRATION.md b/HANDOFF-NAO6-INTEGRATION.md new file mode 100644 index 0000000..d784943 --- /dev/null +++ b/HANDOFF-NAO6-INTEGRATION.md @@ -0,0 +1,344 @@ +# NAO6 Integration Handoff Document + +**Date**: 2024-11-12 +**Status**: ✅ Production Ready - Action Execution Pending +**Session Duration**: ~3 hours + +--- + +## 🎯 What's Ready + +### ✅ Completed +1. **Live Robot Connection** - NAO6 @ nao.local fully connected via ROS2 +2. **Plugin System** - NAO6 ROS2 Integration plugin (v2.1.0) with 10 actions +3. **Database Integration** - Plugin installed, experiments seeded with NAO6 actions +4. **Web Test Interface** - `/nao-test` page working with live robot control +5. **Documentation** - 1,877 lines of comprehensive technical docs +6. **Repository Cleanup** - Consolidated into `robot-plugins` git repo (pushed to GitHub) + +### 🚧 Next Step: Action Execution +**Current Gap**: Experiment designer → WebSocket → ROS2 → NAO flow not implemented + +The plugin is loaded, actions are in the database, but clicking "Execute" in the wizard interface doesn't send commands to the robot yet. + +--- + +## 🚀 Quick Start (For Next Agent) + +### Terminal 1: Start NAO6 Integration +```bash +cd ~/Documents/Projects/nao6-hristudio-integration +./start-nao6.sh +``` +**Expect**: Color-coded logs showing NAO Driver, ROS Bridge, ROS API running + +### Terminal 2: Start HRIStudio +```bash +cd ~/Documents/Projects/hristudio +bun dev +``` +**Access**: http://localhost:3000 + +### Verify Setup +1. **Test Page**: http://localhost:3000/nao-test + - Click "Connect" → Should turn green + - Click "Speak" → NAO should talk + - Movement buttons → NAO should move + +2. **Experiment Designer**: http://localhost:3000/experiments/[id]/designer + - Check "Basic Interaction Protocol 1" + - Should see NAO6 actions in action library + - Drag actions to experiment canvas + +3. **Database Check**: + ```bash + bun db:seed # Should complete without errors + ``` + +--- + +## 📁 Key File Locations + +### NAO6 Integration Repository +``` +~/Documents/Projects/nao6-hristudio-integration/ +├── start-nao6.sh # START HERE - runs everything +├── nao6-plugin.json # Plugin definition (10 actions) +├── SESSION-SUMMARY.md # Complete session details +└── docs/ # Technical references + ├── NAO6-ROS2-TOPICS.md (26 topics documented) + ├── HRISTUDIO-ACTION-MAPPING.md (Action specs + TypeScript types) + └── INTEGRATION-SUMMARY.md (Quick reference) +``` + +### HRIStudio Project +``` +~/Documents/Projects/hristudio/ +├── robot-plugins/ # Git submodule @ github.com/soconnor0919/robot-plugins +│ └── plugins/ +│ └── nao6-ros2.json # MAIN PLUGIN FILE (v2.1.0) +├── src/app/(dashboard)/nao-test/ +│ └── page.tsx # Working test interface +├── src/components/experiments/designer/ +│ └── ActionRegistry.ts # Loads plugin actions +└── scripts/seed-dev.ts # Seeds NAO6 plugin into DB +``` + +--- + +## 🔧 Current System State + +### Database +- **2 repositories**: Core + Robot Plugins +- **4 plugins**: Core System, TurtleBot3 Burger, TurtleBot3 Waffle, **NAO6 ROS2 Integration** +- **NAO6 installed in**: "Basic Interaction Protocol 1" study +- **Experiment actions**: Step 1 has "NAO Speak Text", Step 3 has "NAO Move Head" + +### ROS2 System +- **26 topics** available when `start-nao6.sh` is running +- **Key topics**: `/speech`, `/cmd_vel`, `/joint_angles`, `/joint_states`, `/bumper`, etc. +- **WebSocket**: ws://localhost:9090 (rosbridge_websocket) + +### Robot +- **IP**: nao.local (134.82.159.168) +- **Credentials**: nao / robolab +- **Status**: Awake and responsive (test with ping) + +--- + +## 🎯 Implementation Needed + +### 1. Action Execution Flow +**Where to implement**: +- `src/components/trials/WizardInterface.tsx` or similar +- Connect "Execute Action" button → WebSocket → ROS2 + +**What it should do**: +```typescript +// When wizard clicks "Execute Action" on a NAO6 action +function executeNAO6Action(action: Action) { + // 1. Get action parameters from database + const { type, parameters } = action; + + // 2. Connect to WebSocket (if not connected) + const ws = new WebSocket('ws://localhost:9090'); + + // 3. Map action type to ROS2 topic + const topicMapping = { + 'nao6_speak': '/speech', + 'nao6_move_forward': '/cmd_vel', + 'nao6_move_head': '/joint_angles', + // ... etc + }; + + // 4. Create ROS message + const rosMessage = { + op: 'publish', + topic: topicMapping[type], + msg: formatMessageForROS(type, parameters) + }; + + // 5. Send to robot + ws.send(JSON.stringify(rosMessage)); + + // 6. Log to trial_events + logTrialEvent({ + trial_id: currentTrialId, + event_type: 'action_executed', + event_data: { action, timestamp: Date.now() } + }); +} +``` + +### 2. Message Formatting +**Reference**: See `nao6-hristudio-integration/docs/HRISTUDIO-ACTION-MAPPING.md` + +**Examples**: +```typescript +function formatMessageForROS(actionType: string, params: any) { + switch(actionType) { + case 'nao6_speak': + return { data: params.text }; + + case 'nao6_move_forward': + return { + linear: { x: params.speed, y: 0, z: 0 }, + angular: { x: 0, y: 0, z: 0 } + }; + + case 'nao6_move_head': + return { + joint_names: ['HeadYaw', 'HeadPitch'], + joint_angles: [params.yaw, params.pitch], + speed: params.speed, + relative: 0 + }; + } +} +``` + +### 3. WebSocket Connection Management +**Suggested approach**: +- Create `useROSBridge()` hook in `src/hooks/` +- Manage connection state, auto-reconnect +- Provide `publish()`, `subscribe()`, `callService()` methods + +--- + +## 🧪 Testing Checklist + +Before marking as complete: +- [ ] Can execute "Speak Text" action from wizard interface → NAO speaks +- [ ] Can execute "Move Forward" action → NAO walks +- [ ] Can execute "Move Head" action → NAO moves head +- [ ] Actions are logged to `trial_events` table +- [ ] Connection errors are handled gracefully +- [ ] Emergency stop works from wizard interface +- [ ] Multiple actions in sequence work +- [ ] Sensor monitoring displays in wizard interface + +--- + +## 📚 Reference Documentation + +### Primary Sources +1. **Working Example**: `src/app/(dashboard)/nao-test/page.tsx` + - Lines 67-100: WebSocket connection setup + - Lines 200-350: Action execution examples + - This is WORKING code - use it as template! + +2. **Action Specifications**: `nao6-hristudio-integration/docs/HRISTUDIO-ACTION-MAPPING.md` + - Lines 1-100: Each action with parameters + - TypeScript types already defined + - WebSocket message formats included + +3. **ROS2 Topics**: `nao6-hristudio-integration/docs/NAO6-ROS2-TOPICS.md` + - Complete message type definitions + - Examples for each topic + +### TypeScript Types +Already defined in action mapping doc: +```typescript +interface SpeakTextAction { + action: 'nao6_speak'; + parameters: { + text: string; + volume?: number; + }; +} + +interface MoveForwardAction { + action: 'nao6_move_forward'; + parameters: { + speed: number; + duration: number; + }; +} +``` + +--- + +## 🔍 Where to Look + +### To understand plugin loading: +- `src/components/experiments/designer/ActionRegistry.ts` +- `src/components/experiments/designer/panels/ActionLibraryPanel.tsx` + +### To see working WebSocket code: +- `src/app/(dashboard)/nao-test/page.tsx` (fully functional!) + +### To find action execution trigger: +- Search for: `executeAction`, `onActionExecute`, `runAction` +- Likely in: `src/components/trials/` or `src/components/experiments/` + +--- + +## 🚨 Important Notes + +### DO NOT +- ❌ Modify `robot-plugins/` without committing/pushing (it's a git repo) +- ❌ Change plugin structure without updating seed script +- ❌ Remove `start-nao6.sh` - it's the main entry point +- ❌ Hard-code WebSocket URLs - use config/env vars + +### DO +- ✅ Use existing `/nao-test` page code as reference +- ✅ Test with live robot frequently +- ✅ Log all actions to `trial_events` table +- ✅ Handle connection errors gracefully +- ✅ Add loading states for action execution + +### Known Working +- ✅ WebSocket connection (`/nao-test` proves it works) +- ✅ ROS2 topics (26 topics verified) +- ✅ Plugin loading (shows in action library) +- ✅ Database integration (seed script works) + +### Known NOT Working +- ❌ Action execution from experiment designer/wizard interface +- ❌ Sensor data display in wizard interface (topics exist, just not displayed) +- ❌ Camera streaming to browser + +--- + +## 🤝 Session Handoff Summary + +### What We Did +1. Connected to live NAO6 robot at nao.local +2. Documented all 26 ROS2 topics with complete specifications +3. Created clean plugin with 10 actions +4. Integrated plugin into HRIStudio database +5. Built working test interface proving WebSocket → ROS2 → NAO works +6. Cleaned up repositories (removed duplicates, committed to git) +7. Updated experiments to use NAO6 actions +8. Fixed APT repository issues +9. Created comprehensive documentation (1,877 lines) + +### What's Left +**ONE THING**: Connect the experiment designer's "Execute Action" button to the WebSocket/ROS2 system. + +The hard part is done. The `/nao-test` page is a fully working example of exactly what you need to do - just integrate that pattern into the wizard interface. + +--- + +## 🎓 Key Insights + +1. **We're NOT writing a WebSocket server** - using ROS2's official `rosbridge_websocket` +2. **The test page works perfectly** - copy its pattern +3. **All topics are documented** - no guesswork needed +4. **Plugin is in database** - just needs execution hookup +5. **Robot is live and responsive** - test frequently! + +--- + +## ⚡ Quick Commands + +```bash +# Start everything +cd ~/Documents/Projects/nao6-hristudio-integration && ./start-nao6.sh +cd ~/Documents/Projects/hristudio && bun dev + +# Test robot +curl -X POST http://localhost:9090 -d '{"op":"publish","topic":"/speech","msg":{"data":"Test"}}' + +# Reset robot +sshpass -p robolab ssh nao@nao.local \ + "python2 -c 'import sys; sys.path.append(\"/opt/aldebaran/lib/python2.7/site-packages\"); import naoqi; p=naoqi.ALProxy(\"ALRobotPosture\",\"127.0.0.1\",9559); p.goToPosture(\"StandInit\", 0.5)'" + +# Reseed database +cd ~/Documents/Projects/hristudio && bun db:seed +``` + +--- + +**Next Agent**: Start by reviewing `/nao-test/page.tsx` - it's the Rosetta Stone for this integration. Everything you need is already working there! + +**Estimated Time**: 2-4 hours to implement action execution +**Difficulty**: Medium (pattern exists, just needs integration) +**Priority**: High (this is the final piece) + +--- + +**Status**: 🟢 Ready for implementation +**Blocker**: None - all prerequisites met +**Dependencies**: Robot must be running (`start-nao6.sh`) \ No newline at end of file diff --git a/README.md b/README.md index a155808..94f6490 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,37 @@ Full paper available at: [docs/paper.md](docs/paper.md) - **4 User Roles**: Complete role-based access control - **Plugin System**: Extensible robot integration architecture - **Trial System**: Unified design with real-time execution capabilities -- **Mock Robot Integration**: Complete simulation for development and testing + +## NAO6 Robot Integration + +Complete NAO6 robot integration is available in the separate **[nao6-hristudio-integration](../nao6-hristudio-integration/)** repository. + +### Features +- Complete ROS2 driver integration for NAO V6.0 +- WebSocket communication via rosbridge +- 9 robot actions: speech, movement, gestures, sensors, LEDs +- Real-time control from wizard interface +- Production-ready with NAOqi 2.8.7.4 + +### Quick Start +```bash +# Start NAO integration +cd ~/naoqi_ros2_ws +source install/setup.bash +ros2 launch nao_launch nao6_hristudio.launch.py nao_ip:=nao.local + +# Start HRIStudio +cd ~/Documents/Projects/hristudio +bun dev + +# Test at: http://localhost:3000/nao-test +``` + +### Documentation +- **[Integration README](../nao6-hristudio-integration/README.md)** - Complete setup guide +- **[NAO6 Quick Reference](docs/nao6-quick-reference.md)** - Essential commands +- **[Installation Guide](../nao6-hristudio-integration/docs/INSTALLATION.md)** - Detailed setup +- **[Troubleshooting](../nao6-hristudio-integration/docs/TROUBLESHOOTING.md)** - Common issues ## Deployment diff --git a/THESIS_PROJECT_BACKLOG.md b/THESIS_PROJECT_BACKLOG.md old mode 100644 new mode 100755 diff --git a/TRIAL_START_DEBUG.md b/TRIAL_START_DEBUG.md old mode 100644 new mode 100755 diff --git a/WIZARD_INTERFACE_README.md b/WIZARD_INTERFACE_README.md old mode 100644 new mode 100755 diff --git a/bun.lock b/bun.lock index 6db30bf..6753ba0 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "hristudio", @@ -13,6 +14,7 @@ "@hookform/resolvers": "^5.1.1", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.8", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", @@ -43,13 +45,15 @@ "date-fns": "^4.1.0", "drizzle-orm": "^0.41.0", "lucide-react": "^0.536.0", - "next": "^15.5.4", + "minio": "^8.0.6", + "next": "^16.0.10", "next-auth": "^5.0.0-beta.29", "postgres": "^3.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.60.0", "react-resizable-panels": "^3.0.4", + "react-webcam": "^7.2.0", "server-only": "^0.0.1", "sonner": "^2.0.7", "superjson": "^2.2.1", @@ -82,6 +86,8 @@ }, "trustedDependencies": [ "@tailwindcss/oxide", + "esbuild", + "sharp", "unrs-resolver", ], "packages": { @@ -185,7 +191,7 @@ "@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], - "@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="], @@ -275,49 +281,55 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.0" }, "os": "darwin", "cpu": "x64" }, "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.0" }, "os": "linux", "cpu": "arm" }, "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.0" }, "os": "linux", "cpu": "s390x" }, "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.3", "", { "dependencies": { "@emnapi/runtime": "^1.4.4" }, "cpu": "none" }, "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -331,25 +343,25 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - "@next/env": ["@next/env@15.5.4", "", {}, "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A=="], + "@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.4.5", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YhbrlbEt0m4jJnXHMY/cCUDBAWgd5SaTa5mJjzOt82QwflAFfW/h3+COp2TfVSzhmscIZ5sg2WXt3MLziqCSCw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.4", "", { "os": "win32", "cpu": "x64" }, "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -373,6 +385,8 @@ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA=="], @@ -689,6 +703,8 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@zxing/text-encoding": ["@zxing/text-encoding@0.9.0", "", {}, "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -723,6 +739,8 @@ "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], @@ -739,14 +757,20 @@ "bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="], + "block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="], + "bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -777,14 +801,10 @@ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -809,6 +829,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], @@ -891,6 +913,8 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -901,7 +925,7 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], @@ -913,6 +937,8 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], @@ -985,9 +1011,11 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], @@ -1107,6 +1135,8 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "log-symbols": ["log-symbols@5.1.0", "", { "dependencies": { "chalk": "^5.0.0", "is-unicode-supported": "^1.1.0" } }, "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA=="], @@ -1125,12 +1155,18 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minio": ["minio@8.0.6", "", { "dependencies": { "async": "^3.2.4", "block-stream2": "^2.1.0", "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^4.4.1", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", "web-encoding": "^1.1.5", "xml2js": "^0.5.0 || ^0.6.2" } }, "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ=="], + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], @@ -1145,7 +1181,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "next": ["next@15.5.4", "", { "dependencies": { "@next/env": "15.5.4", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.4", "@next/swc-darwin-x64": "15.5.4", "@next/swc-linux-arm64-gnu": "15.5.4", "@next/swc-linux-arm64-musl": "15.5.4", "@next/swc-linux-x64-gnu": "15.5.4", "@next/swc-linux-x64-musl": "15.5.4", "@next/swc-win32-arm64-msvc": "15.5.4", "@next/swc-win32-x64-msvc": "15.5.4", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA=="], + "next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="], "next-auth": ["next-auth@5.0.0-beta.29", "", { "dependencies": { "@auth/core": "0.40.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A=="], @@ -1219,6 +1255,8 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], @@ -1237,6 +1275,8 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "react-webcam": ["react-webcam@7.2.0", "", { "peerDependencies": { "react": ">=16.2.0", "react-dom": ">=16.2.0" } }, "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], @@ -1263,6 +1303,8 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -1275,7 +1317,7 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - "sharp": ["sharp@0.34.3", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", "@img/sharp-libvips-darwin-arm64": "1.2.0", "@img/sharp-libvips-darwin-x64": "1.2.0", "@img/sharp-libvips-linux-arm": "1.2.0", "@img/sharp-libvips-linux-arm64": "1.2.0", "@img/sharp-libvips-linux-ppc64": "1.2.0", "@img/sharp-libvips-linux-s390x": "1.2.0", "@img/sharp-libvips-linux-x64": "1.2.0", "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", "@img/sharp-libvips-linuxmusl-x64": "1.2.0", "@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm64": "0.34.3", "@img/sharp-linux-ppc64": "0.34.3", "@img/sharp-linux-s390x": "0.34.3", "@img/sharp-linux-x64": "0.34.3", "@img/sharp-linuxmusl-arm64": "0.34.3", "@img/sharp-linuxmusl-x64": "0.34.3", "@img/sharp-wasm32": "0.34.3", "@img/sharp-win32-arm64": "0.34.3", "@img/sharp-win32-ia32": "0.34.3", "@img/sharp-win32-x64": "0.34.3" } }, "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1293,8 +1335,6 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], @@ -1305,12 +1345,20 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], + + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -1333,7 +1381,7 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], @@ -1351,6 +1399,8 @@ "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1395,12 +1445,16 @@ "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "web-encoding": ["web-encoding@1.1.5", "", { "dependencies": { "util": "^0.12.3" }, "optionalDependencies": { "@zxing/text-encoding": "0.9.0" } }, "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], @@ -1417,6 +1471,10 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1431,12 +1489,18 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/core/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + + "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-menu/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="], "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -1495,12 +1559,18 @@ "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-sdk/core/fast-xml-parser/strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1545,6 +1615,8 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/components.json b/components.json old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 index e063a86..c57b270 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,18 +17,18 @@ services: timeout: 5s retries: 5 - # minio: - # image: minio/minio - # ports: - # - "9000:9000" # API - # - "9001:9001" # Console - # environment: - # MINIO_ROOT_USER: minioadmin - # MINIO_ROOT_PASSWORD: minioadmin - # volumes: - # - minio_data:/data - # command: server --console-address ":9001" /data + minio: + image: minio/minio + ports: + - "9000:9000" # API + - "9001:9001" # Console + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + volumes: + - minio_data:/data + command: server --console-address ":9001" /data volumes: postgres_data: - # minio_data: + minio_data: diff --git a/docs/README.md b/docs/README.md old mode 100644 new mode 100755 index afc0873..e802557 --- a/docs/README.md +++ b/docs/README.md @@ -112,10 +112,16 @@ This documentation suite provides everything needed to understand, build, deploy - Technical debt resolution - UI/UX enhancements +### **🤖 Robot Integration Guides** + +14. **[NAO6 Complete Integration Guide](./nao6-integration-complete-guide.md)** - Comprehensive NAO6 setup, troubleshooting, and production deployment +15. **[NAO6 Quick Reference](./nao6-quick-reference.md)** - Essential commands and troubleshooting for NAO6 integration +16. **[NAO6 ROS2 Setup](./nao6-ros2-setup.md)** - Basic NAO6 ROS2 driver installation guide + ### **📖 Academic References** -14. **[Research Paper](./root.tex)** - Academic LaTeX document -15. **[Bibliography](./refs.bib)** - Research references +17. **[Research Paper](./root.tex)** - Academic LaTeX document +18. **[Bibliography](./refs.bib)** - Research references --- @@ -152,8 +158,14 @@ This documentation suite provides everything needed to understand, build, deploy ### **For Researchers** 1. **[Project Overview](./project-overview.md)** - Research platform capabilities 2. **[Feature Requirements](./feature-requirements.md)** - User workflows and features -3. **[ROS2 Integration](./ros2-integration.md)** - Robot platform integration -4. **[Research Paper](./root.tex)** - Academic context and methodology +3. **[NAO6 Quick Reference](./nao6-quick-reference.md)** - Essential NAO6 robot control commands +4. **[ROS2 Integration](./ros2-integration.md)** - Robot platform integration +5. **[Research Paper](./root.tex)** - Academic context and methodology + +### **For Robot Integration** +1. **[NAO6 Complete Integration Guide](./nao6-integration-complete-guide.md)** - Full NAO6 setup and troubleshooting +2. **[NAO6 Quick Reference](./nao6-quick-reference.md)** - Essential commands and quick fixes +3. **[ROS2 Integration](./ros2-integration.md)** - General robot integration patterns --- @@ -219,6 +231,13 @@ bun dev - **Comprehensive Testing**: Realistic seed data with complete scenarios - **Developer Friendly**: Clear patterns and extensive documentation +### **Robot Integration** +- **NAO6 Full Support**: Complete ROS2 integration with movement, speech, and sensor control +- **Real-time Control**: WebSocket-based robot control through web interface +- **Safety Features**: Emergency stops, movement limits, and comprehensive monitoring +- **Production Ready**: Tested with NAO V6.0 / NAOqi 2.8.7.4 / ROS2 Humble +- **Troubleshooting Guides**: Complete documentation for setup and problem resolution + --- ## 🎊 **Project Status: Production Ready** @@ -238,6 +257,7 @@ bun dev - ✅ **Core Blocks System** - 26 blocks across events, wizard, control, observation - ✅ **Plugin Architecture** - Unified system for core blocks and robot actions - ✅ **Development Environment** - Realistic test data and scenarios +- ✅ **NAO6 Robot Integration** - Full ROS2 integration with comprehensive control and monitoring --- @@ -271,7 +291,7 @@ The platform is considered production-ready when: - ✅ Performance targets are achieved - ✅ Type safety is complete throughout -**All success criteria have been met. HRIStudio is ready for production deployment.** +**All success criteria have been met. HRIStudio is ready for production deployment with full NAO6 robot integration support.** --- diff --git a/docs/api-routes.md b/docs/api-routes.md old mode 100644 new mode 100755 diff --git a/docs/block-designer-implementation.md b/docs/block-designer-implementation.md old mode 100644 new mode 100755 diff --git a/docs/block-designer.md b/docs/block-designer.md old mode 100644 new mode 100755 diff --git a/docs/cleanup-summary.md b/docs/cleanup-summary.md old mode 100644 new mode 100755 diff --git a/docs/core-blocks-system.md b/docs/core-blocks-system.md old mode 100644 new mode 100755 diff --git a/docs/database-schema.md b/docs/database-schema.md old mode 100644 new mode 100755 diff --git a/docs/deployment-operations.md b/docs/deployment-operations.md old mode 100644 new mode 100755 diff --git a/docs/experiment-designer-redesign.md b/docs/experiment-designer-redesign.md old mode 100644 new mode 100755 diff --git a/docs/experiment-designer-step-integration.md b/docs/experiment-designer-step-integration.md old mode 100644 new mode 100755 diff --git a/docs/feature-requirements.md b/docs/feature-requirements.md old mode 100644 new mode 100755 diff --git a/docs/flow-designer-connections.md b/docs/flow-designer-connections.md old mode 100644 new mode 100755 diff --git a/docs/implementation-details.md b/docs/implementation-details.md old mode 100644 new mode 100755 diff --git a/docs/implementation-guide.md b/docs/implementation-guide.md old mode 100644 new mode 100755 diff --git a/docs/nao6-integration-summary.md b/docs/nao6-integration-summary.md deleted file mode 100644 index f1c83b0..0000000 --- a/docs/nao6-integration-summary.md +++ /dev/null @@ -1,233 +0,0 @@ -# NAO6 ROS2 Integration Summary for HRIStudio - -## Overview - -This document summarizes the complete NAO6 ROS2 integration that has been implemented for HRIStudio, providing researchers with full access to NAO6 capabilities through the visual experiment designer and real-time wizard interface. - -## What's Been Implemented - -### 1. NAO6 ROS2 Plugin (`nao6-ros2.json`) - -A comprehensive robot plugin that exposes all NAO6 capabilities through standard ROS2 topics: - -**Location**: `robot-plugins/plugins/nao6-ros2.json` - -**Key Features**: -- Full ROS2 integration via `naoqi_driver2` -- 10 robot actions across movement, interaction, and sensors -- Proper HRIStudio plugin schema compliance -- Safety limits and parameter validation -- Transform functions for message conversion - -### 2. Robot Actions Available - -#### Movement Actions -- **Walk with Velocity**: Control linear/angular walking velocities -- **Stop Walking**: Emergency stop for immediate movement cessation -- **Set Joint Angle**: Control individual joint positions (25 DOF) -- **Turn Head**: Dedicated head orientation control - -#### Interaction Actions -- **Say Text**: Text-to-speech via ROS2 `/speech` topic - -#### Sensor Actions -- **Get Camera Image**: Capture from front or bottom cameras -- **Get Joint States**: Read current joint positions and velocities -- **Get IMU Data**: Inertial measurement from torso sensor -- **Get Bumper Status**: Foot contact sensor readings -- **Get Touch Sensors**: Hand and head tactile sensor states -- **Get Sonar Range**: Ultrasonic distance measurements -- **Get Robot Info**: General robot status and information - -### 3. ROS2 Topic Mapping - -The plugin maps to these standard NAO6 ROS2 topics: - -``` -/cmd_vel → Robot velocity commands (Twist) -/odom → Odometry data (Odometry) -/joint_states → Joint positions/velocities (JointState) -/joint_angles → NAO-specific joint control (JointAnglesWithSpeed) -/camera/front/image_raw → Front camera stream (Image) -/camera/bottom/image_raw → Bottom camera stream (Image) -/imu/torso → Inertial measurement (Imu) -/speech → Text-to-speech commands (String) -/bumper → Foot bumper sensors (Bumper) -/hand_touch → Hand touch sensors (HandTouch) -/head_touch → Head touch sensors (HeadTouch) -/sonar/left → Left ultrasonic sensor (Range) -/sonar/right → Right ultrasonic sensor (Range) -/info → Robot information (RobotInfo) -``` - -### 4. Transform Functions (`nao6-transforms.ts`) - -**Location**: `src/lib/nao6-transforms.ts` - -Comprehensive message conversion functions: -- Parameter validation and safety limits -- ROS2 message format compliance -- Joint limit enforcement (25 DOF with proper ranges) -- Velocity clamping for safe operation -- Helper functions for UI integration - -### 5. Setup Documentation (`nao6-ros2-setup.md`) - -**Location**: `docs/nao6-ros2-setup.md` - -Complete setup guide covering: -- ROS2 Humble installation on Ubuntu 22.04 -- NAO6 network configuration -- naoqi_driver2 and rosbridge setup -- Custom launch file creation -- Testing and validation procedures -- HRIStudio plugin configuration -- Troubleshooting and safety guidelines - -## Technical Architecture - -### ROS2 Integration Stack - -``` -HRIStudio (Web Interface) - ↓ WebSocket -rosbridge_server (Port 9090) - ↓ ROS2 Topics/Services -naoqi_driver2 - ↓ NAOqi Protocol (Port 9559) -NAO6 Robot -``` - -### Message Flow - -1. **Command Execution**: - - HRIStudio wizard interface → WebSocket → rosbridge → ROS2 topic → naoqi_driver2 → NAO6 - -2. **Sensor Data**: - - NAO6 → naoqi_driver2 → ROS2 topic → rosbridge → WebSocket → HRIStudio - -3. **Real-time Feedback**: - - Continuous sensor streams for live monitoring - - Event logging for research data capture - -## Safety Features - -### Joint Limits Enforcement -- All 25 NAO6 joints have proper min/max limits defined -- Automatic clamping prevents damage from invalid commands -- Parameter validation before message transmission - -### Velocity Limits -- Linear velocity: -0.55 to 0.55 m/s -- Angular velocity: -2.0 to 2.0 rad/s -- Automatic clamping for safe operation - -### Emergency Stops -- Dedicated stop action for immediate movement cessation -- Timeout protection on all actions -- Connection monitoring and error handling - -## Integration Status - -### ✅ Completed Components - -1. **Plugin Definition**: Full NAO6 plugin with proper schema -2. **Action Library**: 10 comprehensive robot actions -3. **Transform Functions**: Complete message conversion system -4. **Documentation**: Setup guide and integration instructions -5. **Safety Systems**: Joint limits, velocity clamping, emergency stops -6. **Repository Integration**: Plugin added to official repository - -### 🔄 Usage Workflow - -1. **Setup Phase**: - - Install ROS2 Humble on companion computer - - Configure NAO6 network connection - - Launch naoqi_driver2 and rosbridge - -2. **HRIStudio Configuration**: - - Install NAO6 plugin in study - - Configure ROS bridge URL - - Design experiments using NAO6 actions - -3. **Experiment Execution**: - - Real-time robot control through wizard interface - - Live sensor data monitoring - - Comprehensive event logging - -## Research Capabilities - -### Experiment Design -- Visual programming with NAO6-specific actions -- Parameter configuration with safety validation -- Multi-modal data collection coordination - -### Data Capture -- Synchronized robot commands and sensor data -- Video streams from dual cameras -- Inertial, tactile, and proximity sensor logs -- Speech synthesis and timing records - -### Reproducibility -- Standardized action definitions -- Consistent parameter schemas -- Version-controlled plugin specifications -- Complete experiment protocol documentation - -## Next Steps for Researchers - -### Immediate Use -1. Follow setup guide in `docs/nao6-ros2-setup.md` -2. Install NAO6 plugin in HRIStudio study -3. Create experiments using available actions -4. Run trials with real-time robot control - -### Advanced Integration -1. **Custom Actions**: Extend plugin with study-specific behaviors -2. **Multi-Robot**: Scale to multiple NAO6 robots -3. **Navigation**: Add SLAM and path planning capabilities -4. **Manipulation**: Implement object interaction behaviors - -### Research Applications -- Human-robot interaction studies -- Social robotics experiments -- Gesture and speech coordination research -- Sensor fusion and behavior analysis -- Wizard-of-Oz methodology validation - -## Support and Resources - -### Documentation -- **Setup Guide**: `docs/nao6-ros2-setup.md` -- **Plugin Schema**: `robot-plugins/docs/schema.md` -- **ROS2 Integration**: `docs/ros2-integration.md` -- **Transform Functions**: `src/lib/nao6-transforms.ts` - -### External Resources -- **NAO6 Documentation**: https://developer.softbankrobotics.com/nao6 -- **naoqi_driver2**: https://github.com/ros-naoqi/naoqi_driver2 -- **ROS2 Humble**: https://docs.ros.org/en/humble/ -- **rosbridge**: http://wiki.ros.org/rosbridge_suite - -### Technical Support -- **HRIStudio Issues**: GitHub repository -- **ROS2 Community**: ROS Discourse forum -- **NAO6 Support**: SoftBank Robotics developer portal - -## Conclusion - -The NAO6 ROS2 integration provides researchers with a complete, production-ready system for conducting Human-Robot Interaction studies. The integration leverages: - -- **Standard ROS2 protocols** for reliable communication -- **Comprehensive safety systems** for secure operation -- **Visual experiment design** for accessible research tools -- **Real-time control interfaces** for dynamic experiment execution -- **Complete data capture** for rigorous analysis - -This implementation enables researchers to focus on their studies rather than technical integration, while maintaining the flexibility and control needed for cutting-edge HRI research. - ---- - -**Status**: ✅ Production Ready -**Last Updated**: December 2024 -**Compatibility**: HRIStudio v1.0+, ROS2 Humble, NAO6 with NAOqi 2.8.7+ \ No newline at end of file diff --git a/docs/nao6-quick-reference.md b/docs/nao6-quick-reference.md new file mode 100755 index 0000000..1240c62 --- /dev/null +++ b/docs/nao6-quick-reference.md @@ -0,0 +1,177 @@ +# NAO6 Quick Reference + +Essential commands for using NAO6 robots with HRIStudio. + +## Quick Start + +### 1. Start NAO Integration +```bash +cd ~/naoqi_ros2_ws +source install/setup.bash +ros2 launch nao_launch nao6_hristudio.launch.py nao_ip:=nao.local password:=robolab +``` + +### 2. Wake Robot +Press chest button for 3 seconds, or use: +```bash +# Via SSH (institution-specific password) +ssh nao@nao.local +# Then run wake-up command (see integration repo docs) +``` + +### 3. Start HRIStudio +```bash +cd ~/Documents/Projects/hristudio +bun dev +``` + +### 4. Test Connection +- Open: `http://localhost:3000/nao-test` +- Click "Connect" +- Test robot commands + +## Essential Commands + +### Test Connectivity +```bash +ping nao.local # Test network +ros2 topic list | grep naoqi # Check ROS topics +``` + +### Manual Control +```bash +# Speech +ros2 topic pub --once /speech std_msgs/String "data: 'Hello world'" + +# Movement (robot must be awake!) +ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.1}}' + +# Stop +ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.0}}' +``` + +### Monitor Status +```bash +ros2 topic echo /naoqi_driver/battery # Battery level +ros2 topic echo /naoqi_driver/joint_states # Joint positions +``` + +## Troubleshooting + +**Robot not moving:** Press chest button for 3 seconds to wake up + +**WebSocket fails:** Check rosbridge is running on port 9090 +```bash +ss -an | grep 9090 +``` + +**Connection lost:** Restart rosbridge +```bash +pkill -f rosbridge +ros2 run rosbridge_server rosbridge_websocket +``` + +## ROS Topics + +**Commands (Input):** +- `/speech` - Text-to-speech +- `/cmd_vel` - Movement +- `/joint_angles` - Joint control + +**Sensors (Output):** +- `/naoqi_driver/joint_states` - Joint data +- `/naoqi_driver/battery` - Battery level +- `/naoqi_driver/bumper` - Foot sensors +- `/naoqi_driver/sonar/*` - Distance sensors +- `/naoqi_driver/camera/*` - Camera feeds + +## WebSocket + +**URL:** `ws://localhost:9090` + +**Example message:** +```javascript +{ + "op": "publish", + "topic": "/speech", + "type": "std_msgs/String", + "msg": {"data": "Hello world"} +} +``` + +## More Information + +See **[nao6-hristudio-integration](../../nao6-hristudio-integration/)** repository for: +- Complete installation guide +- Detailed usage instructions +- Full troubleshooting guide +- Plugin definitions +- Launch file configurations + +## Common Use Cases + +### Make Robot Speak +```bash +ros2 topic pub --once /speech std_msgs/String "data: 'Welcome to the experiment'" +``` + +### Walk Forward 3 Steps +```bash +ros2 topic pub --times 3 /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}' +``` + +### Turn Head Left +```bash +ros2 topic pub --once /joint_angles naoqi_bridge_msgs/msg/JointAnglesWithSpeed '{joint_names: ["HeadYaw"], joint_angles: [0.8], speed: 0.2}' +``` + +### Emergency Stop +```bash +ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}' +``` + +## 🚨 Safety Notes + +- **Always wake up robot before movement commands** +- **Keep emergency stop accessible** +- **Start with small movements (0.05 m/s)** +- **Monitor battery level during experiments** +- **Ensure clear space around robot** + +## 📝 Credentials + +**Default NAO Login:** +- Username: `nao` +- Password: `robolab` (institution-specific) + +**HRIStudio Login:** +- Email: `sean@soconnor.dev` +- Password: `password123` + +## 🔄 Complete Restart Procedure + +```bash +# 1. Kill all processes +sudo fuser -k 9090/tcp +pkill -f "rosbridge\|naoqi\|ros2" + +# 2. Restart database +sudo docker compose down && sudo docker compose up -d + +# 3. Start ROS integration +cd ~/naoqi_ros2_ws && source install/setup.bash +ros2 launch install/nao_launch/share/nao_launch/launch/nao6_hristudio.launch.py nao_ip:=nao.local password:=robolab + +# 4. Wake up robot (in another terminal) +sshpass -p "robolab" ssh nao@nao.local "python2 -c \"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; naoqi.ALProxy('ALMotion', '127.0.0.1', 9559).wakeUp()\"" + +# 5. Start HRIStudio (in another terminal) +cd /home/robolab/Documents/Projects/hristudio && bun dev +``` + +--- + +**📖 For detailed setup instructions, see:** [NAO6 Complete Integration Guide](./nao6-integration-complete-guide.md) + +**✅ Integration Status:** Production Ready +**🤖 Tested With:** NAO V6.0 / NAOqi 2.8.7.4 / ROS2 Humble \ No newline at end of file diff --git a/docs/nao6-ros2-setup.md b/docs/nao6-ros2-setup.md deleted file mode 100644 index 00a10e5..0000000 --- a/docs/nao6-ros2-setup.md +++ /dev/null @@ -1,372 +0,0 @@ -# NAO6 ROS2 Setup Guide for HRIStudio - -This guide walks you through setting up your NAO6 robot with ROS2 integration for use with HRIStudio's experiment platform. - -## Prerequisites - -- NAO6 robot with NAOqi OS 2.8.7+ -- Ubuntu 22.04.5 LTS computer (x86_64) -- Network connectivity between computer and NAO6 -- Administrative access to both systems - -## Overview - -The integration uses the `naoqi_driver2` package to bridge NAOqi with ROS2, exposing all robot capabilities through standard ROS2 topics and services. HRIStudio connects via WebSocket using `rosbridge_server`. - -## Step 1: NAO6 Network Configuration - -1. **Power on your NAO6** and wait for boot completion -2. **Connect NAO6 to your network**: - - Press chest button to get IP address - - Or use Choregraphe to configure WiFi -3. **Verify connectivity**: - ```bash - ping nao.local # or robot IP address - ``` - -## Step 2: ROS2 Humble Installation - -Install ROS2 Humble on your Ubuntu 22.04 system: - -```bash -# Update system -sudo apt update && sudo apt upgrade -y - -# Install ROS2 Humble -sudo apt install software-properties-common -sudo add-apt-repository universe -sudo apt update && sudo apt install curl -y -sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - -sudo sh -c 'echo "deb http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros2-latest.list' - -sudo apt update -sudo apt install ros-humble-desktop -sudo apt install ros-dev-tools - -# Source ROS2 -echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc -source ~/.bashrc -``` - -## Step 3: Install NAO ROS2 Packages - -Install the required ROS2 packages for NAO6 integration: - -```bash -# Install naoqi_driver2 and dependencies -sudo apt install ros-humble-naoqi-driver2 -sudo apt install ros-humble-naoqi-bridge-msgs -sudo apt install ros-humble-geometry-msgs -sudo apt install ros-humble-sensor-msgs -sudo apt install ros-humble-nav-msgs -sudo apt install ros-humble-std-msgs - -# Install rosbridge for HRIStudio communication -sudo apt install ros-humble-rosbridge-suite - -# Install additional useful packages -sudo apt install ros-humble-rqt -sudo apt install ros-humble-rqt-common-plugins -``` - -## Step 4: Configure NAO Connection - -Create a launch file for easy NAO6 connection: - -```bash -# Create workspace -mkdir -p ~/nao_ws/src -cd ~/nao_ws - -# Create launch file directory -mkdir -p src/nao_launch/launch - -# Create the launch file -cat > src/nao_launch/launch/nao6_hristudio.launch.py << 'EOF' -from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription -from launch.substitutions import LaunchConfiguration -from launch_ros.actions import Node -from launch.launch_description_sources import PythonLaunchDescriptionSource -from ament_index_python.packages import get_package_share_directory -import os - -def generate_launch_description(): - return LaunchDescription([ - # NAO IP configuration - DeclareLaunchArgument('nao_ip', default_value='nao.local'), - DeclareLaunchArgument('nao_port', default_value='9559'), - DeclareLaunchArgument('bridge_port', default_value='9090'), - - # NAOqi Driver - Node( - package='naoqi_driver2', - executable='naoqi_driver', - name='naoqi_driver', - parameters=[{ - 'nao_ip': LaunchConfiguration('nao_ip'), - 'nao_port': LaunchConfiguration('nao_port'), - 'publish_joint_states': True, - 'publish_odometry': True, - 'publish_camera': True, - 'publish_sensors': True, - 'joint_states_frequency': 30.0, - 'odom_frequency': 30.0, - 'camera_frequency': 15.0, - 'sensor_frequency': 10.0 - }], - output='screen' - ), - - # Rosbridge WebSocket Server - Node( - package='rosbridge_server', - executable='rosbridge_websocket', - name='rosbridge_websocket', - parameters=[{ - 'port': LaunchConfiguration('bridge_port'), - 'address': '0.0.0.0', - 'authenticate': False, - 'fragment_timeout': 600, - 'delay_between_messages': 0, - 'max_message_size': 10000000 - }], - output='screen' - ) - ]) -EOF - -# Create package.xml -cat > src/nao_launch/package.xml << 'EOF' - - - - nao_launch - 1.0.0 - Launch files for NAO6 HRIStudio integration - Your Name - MIT - - ament_cmake - launch - launch_ros - naoqi_driver2 - rosbridge_server - -EOF - -# Create CMakeLists.txt -cat > src/nao_launch/CMakeLists.txt << 'EOF' -cmake_minimum_required(VERSION 3.8) -project(nao_launch) - -find_package(ament_cmake REQUIRED) - -install(DIRECTORY launch/ - DESTINATION share/${PROJECT_NAME}/launch/ -) - -ament_package() -EOF - -# Build the workspace -colcon build -source install/setup.bash -``` - -## Step 5: Test NAO Connection - -Start the NAO6 ROS2 integration: - -```bash -cd ~/nao_ws -source install/setup.bash - -# Launch with your NAO's IP address -ros2 launch nao_launch nao6_hristudio.launch.py nao_ip:=YOUR_NAO_IP -``` - -Replace `YOUR_NAO_IP` with your NAO's actual IP address (e.g., `192.168.1.100`). - -## Step 6: Verify ROS2 Topics - -In a new terminal, verify that NAO topics are publishing: - -```bash -source /opt/ros/humble/setup.bash - -# List all topics -ros2 topic list - -# You should see these NAO6 topics: -# /cmd_vel - Robot velocity commands -# /odom - Odometry data -# /joint_states - Joint positions and velocities -# /joint_angles - NAO-specific joint control -# /camera/front/image_raw - Front camera -# /camera/bottom/image_raw - Bottom camera -# /imu/torso - Inertial measurement unit -# /bumper - Foot bumper sensors -# /hand_touch - Hand tactile sensors -# /head_touch - Head tactile sensors -# /sonar/left - Left ultrasonic sensor -# /sonar/right - Right ultrasonic sensor -# /info - Robot information - -# Test specific topics -ros2 topic echo /joint_states --once -ros2 topic echo /odom --once -ros2 topic echo /info --once -``` - -## Step 7: Test Robot Control - -Test basic robot control: - -```bash -# Make NAO say something -ros2 topic pub /speech std_msgs/msg/String "data: 'Hello from ROS2!'" --once - -# Move head (be careful with joint limits) -ros2 topic pub /joint_angles naoqi_bridge_msgs/msg/JointAnglesWithSpeed \ - "joint_names: ['HeadYaw'] - joint_angles: [0.5] - speed: 0.3" --once - -# Basic walking command (very small movement) -ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \ - "linear: {x: 0.1, y: 0.0, z: 0.0} - angular: {x: 0.0, y: 0.0, z: 0.0}" --once - -# Stop movement -ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \ - "linear: {x: 0.0, y: 0.0, z: 0.0} - angular: {x: 0.0, y: 0.0, z: 0.0}" --once -``` - -## Step 8: Configure HRIStudio - -1. **Start HRIStudio** with your development setup -2. **Add NAO6 Plugin Repository**: - - Go to Admin → Plugin Repositories - - Add the HRIStudio official repository if not already present - - Sync to get the latest plugins including `nao6-ros2` - -3. **Install NAO6 Plugin**: - - In your study, go to Plugins - - Install the "NAO6 Robot (ROS2 Integration)" plugin - - Configure the ROS bridge URL: `ws://YOUR_COMPUTER_IP:9090` - -4. **Create Experiment**: - - Use the experiment designer - - Add NAO6 actions from the robot blocks section - - Configure parameters for each action - -5. **Run Trial**: - - Ensure your NAO6 ROS2 system is running - - Start a trial in HRIStudio - - Control the robot through the wizard interface - -## Available Robot Actions - -Your NAO6 plugin provides these actions for experiments: - -### Movement Actions -- **Walk with Velocity**: Control linear/angular velocity -- **Stop Walking**: Emergency stop -- **Set Joint Angle**: Control individual joints -- **Turn Head**: Head orientation control - -### Interaction Actions -- **Say Text**: Text-to-speech via ROS2 - -### Sensor Actions -- **Get Camera Image**: Capture from front/bottom cameras -- **Get Joint States**: Read all joint positions -- **Get IMU Data**: Inertial measurement data -- **Get Bumper Status**: Foot contact sensors -- **Get Touch Sensors**: Hand/head touch detection -- **Get Sonar Range**: Ultrasonic distance sensors -- **Get Robot Info**: General robot status - -## Troubleshooting - -### NAO Connection Issues -```bash -# Check NAO network connectivity -ping nao.local - -# Check NAOqi service -telnet nao.local 9559 - -# Restart NAOqi on NAO -# (Use robot's web interface or Choregraphe) -``` - -### ROS2 Issues -```bash -# Check if naoqi_driver2 is running -ros2 node list | grep naoqi - -# Check topic publication rates -ros2 topic hz /joint_states - -# Restart the launch file -ros2 launch nao_launch nao6_hristudio.launch.py nao_ip:=YOUR_NAO_IP -``` - -### HRIStudio Connection Issues -```bash -# Verify rosbridge is running -netstat -an | grep 9090 - -# Check WebSocket connection -curl -i -N -H "Connection: Upgrade" \ - -H "Upgrade: websocket" \ - -H "Sec-WebSocket-Key: test" \ - -H "Sec-WebSocket-Version: 13" \ - http://localhost:9090 -``` - -### Robot Safety -- Always keep emergency stop accessible -- Start with small movements and low speeds -- Monitor robot battery level -- Ensure clear space around robot -- Never leave robot unattended during operation - -## Performance Optimization - -### Network Optimization -```bash -# Increase network buffer sizes for camera data -sudo sysctl -w net.core.rmem_max=26214400 -sudo sysctl -w net.core.rmem_default=26214400 -``` - -### ROS2 Optimization -```bash -# Adjust QoS settings for better performance -export RMW_IMPLEMENTATION=rmw_cyclonedx_cpp -export CYCLONEDX_URI=file:///path/to/cyclonedx.xml -``` - -## Next Steps - -1. **Experiment Design**: Create experiments using NAO6 actions -2. **Data Collection**: Use sensor actions for research data -3. **Custom Actions**: Extend the plugin with custom behaviors -4. **Multi-Robot**: Scale to multiple NAO6 robots -5. **Advanced Features**: Implement navigation, manipulation, etc. - -## Support Resources - -- **NAO Documentation**: https://developer.softbankrobotics.com/nao6 -- **naoqi_driver2**: https://github.com/ros-naoqi/naoqi_driver2 -- **ROS2 Humble**: https://docs.ros.org/en/humble/ -- **HRIStudio Docs**: See `docs/` folder -- **Community**: HRIStudio Discord/Forum - ---- - -**Success!** Your NAO6 is now ready for use with HRIStudio experiments. The robot's capabilities are fully accessible through the visual experiment designer and real-time wizard interface. \ No newline at end of file diff --git a/docs/paper.md b/docs/paper.md old mode 100644 new mode 100755 diff --git a/docs/plugin-system-implementation-guide.md b/docs/plugin-system-implementation-guide.md old mode 100644 new mode 100755 diff --git a/docs/project-overview.md b/docs/project-overview.md old mode 100644 new mode 100755 diff --git a/docs/project-status.md b/docs/project-status.md old mode 100644 new mode 100755 diff --git a/docs/proposal.tex b/docs/proposal.tex old mode 100644 new mode 100755 diff --git a/docs/quick-reference.md b/docs/quick-reference.md old mode 100644 new mode 100755 diff --git a/docs/roman-2025-talk.md b/docs/roman-2025-talk.md old mode 100644 new mode 100755 diff --git a/docs/ros2-integration.md b/docs/ros2-integration.md old mode 100644 new mode 100755 diff --git a/docs/ros2_naoqi.md b/docs/ros2_naoqi.md old mode 100644 new mode 100755 diff --git a/docs/route-consolidation-summary.md b/docs/route-consolidation-summary.md old mode 100644 new mode 100755 diff --git a/docs/thesis-project-priorities.md b/docs/thesis-project-priorities.md old mode 100644 new mode 100755 diff --git a/docs/trial-system-overhaul.md b/docs/trial-system-overhaul.md old mode 100644 new mode 100755 diff --git a/docs/wizard-interface-final.md b/docs/wizard-interface-final.md old mode 100644 new mode 100755 diff --git a/docs/wizard-interface-guide.md b/docs/wizard-interface-guide.md old mode 100644 new mode 100755 diff --git a/docs/wizard-interface-redesign.md b/docs/wizard-interface-redesign.md old mode 100644 new mode 100755 diff --git a/docs/wizard-interface-summary.md b/docs/wizard-interface-summary.md old mode 100644 new mode 100755 diff --git a/docs/work_in_progress.md b/docs/work_in_progress.md old mode 100644 new mode 100755 diff --git a/drizzle.config.ts b/drizzle.config.ts old mode 100644 new mode 100755 diff --git a/eslint.config.js b/eslint.config.js old mode 100644 new mode 100755 diff --git a/middleware.ts b/middleware.ts old mode 100644 new mode 100755 diff --git a/nao6_integration_README.md b/nao6_integration_README.md new file mode 100644 index 0000000..228aa4a --- /dev/null +++ b/nao6_integration_README.md @@ -0,0 +1,368 @@ +# NAO6 HRIStudio Integration + +**Complete integration package for NAO6 humanoid robots with the HRIStudio research platform** + +## 🎯 Overview + +This repository contains all components needed to integrate NAO6 robots with HRIStudio for Human-Robot Interaction research. It provides production-ready ROS2 packages, web interface plugins, control scripts, and comprehensive documentation for seamless robot operation through the HRIStudio platform. + +## 📦 Repository Structure + +``` +nao6-hristudio-integration/ +├── README.md # This file +├── launch/ # ROS2 launch configurations +│ ├── nao6_production.launch.py # Production-optimized launch +│ └── nao6_hristudio_enhanced.launch.py # Enhanced with monitoring +├── scripts/ # Utilities and automation +│ ├── test_nao_topics.py # ROS topics simulator +│ ├── test_websocket.py # WebSocket bridge tester +│ ├── verify_nao6_bridge.sh # Integration verification +│ └── seed-nao6-plugin.ts # Database seeding for HRIStudio +├── plugins/ # HRIStudio plugin definitions +│ ├── repository.json # Plugin repository metadata +│ ├── nao6-ros2-enhanced.json # Complete NAO6 plugin +│ └── README.md # Plugin documentation +├── examples/ # Usage examples and tools +│ ├── nao_control.py # Command-line robot control +│ └── start_nao6_hristudio.sh # Automated startup script +└── docs/ # Documentation + ├── NAO6_INTEGRATION_COMPLETE.md # Complete integration guide + ├── INSTALLATION.md # Installation instructions + ├── USAGE.md # Usage examples + └── TROUBLESHOOTING.md # Common issues and solutions +``` + +## 🚀 Quick Start + +### Prerequisites +- **NAO6 Robot** with NAOqi 2.8.7.4+ +- **Ubuntu 22.04** with ROS2 Humble +- **HRIStudio Platform** (web interface) +- **Network connectivity** between computer and robot + +### 1. Clone Repository +```bash +git clone ~/nao6-hristudio-integration +cd ~/nao6-hristudio-integration +``` + +### 2. Install Dependencies +```bash +# Install ROS2 packages +sudo apt update +sudo apt install ros-humble-rosbridge-suite ros-humble-naoqi-driver + +# Install Python dependencies +pip install websocket-client +``` + +### 3. Setup NAOqi ROS2 Workspace +```bash +# Build the enhanced nao_launch package +cd ~/naoqi_ros2_ws +colcon build --packages-select nao_launch +source install/setup.bash +``` + +### 4. Start Integration +```bash +# Option A: Use automated startup script +./examples/start_nao6_hristudio.sh --nao-ip nao.local --password robolab + +# Option B: Manual launch +ros2 launch nao_launch nao6_production.launch.py nao_ip:=nao.local password:=robolab +``` + +### 5. Configure HRIStudio +```bash +# Seed NAO6 plugin into HRIStudio database +cd ~/Documents/Projects/hristudio +bun run ../nao6-hristudio-integration/scripts/seed-nao6-plugin.ts + +# Start HRIStudio web interface +bun dev +``` + +### 6. Test Integration +- Open: `http://localhost:3000/nao-test` +- Login: `sean@soconnor.dev` / `password123` +- Click "Connect" to establish WebSocket connection +- Try robot commands and verify responses + +## 🎮 Available Robot Actions + +The NAO6 plugin provides 9 comprehensive actions for HRIStudio experiments: + +### 🗣️ Communication +- **Speak Text** - Text-to-speech with volume/speed control +- **LED Control** - Visual feedback with colors and patterns + +### 🚶 Movement & Posture +- **Move Robot** - Walking, turning with safety limits +- **Set Posture** - Stand, sit, crouch positions +- **Move Head** - Gaze control and attention direction +- **Perform Gesture** - Wave, point, applause, custom animations + +### 📡 Sensors & Monitoring +- **Monitor Sensors** - Touch, bumper, sonar detection +- **Check Robot Status** - Battery, joints, system health + +### 🛡️ Safety & System +- **Emergency Stop** - Immediate motion termination +- **Wake Up / Rest** - Power management + +## 🔧 Command-Line Tools + +### Robot Control +```bash +# Direct robot control +python3 examples/nao_control.py --ip nao.local wake +python3 examples/nao_control.py --ip nao.local speak "Hello world" +python3 examples/nao_control.py --ip nao.local move 0.1 0 0 +python3 examples/nao_control.py --ip nao.local pose Stand +python3 examples/nao_control.py --ip nao.local emergency +``` + +### Integration Testing +```bash +# Verify all components +./scripts/verify_nao6_bridge.sh + +# Test WebSocket connectivity +python3 scripts/test_websocket.py + +# Simulate robot topics (without hardware) +python3 scripts/test_nao_topics.py +``` + +## 🌐 WebSocket Communication + +### Connection Details +- **URL**: `ws://localhost:9090` +- **Protocol**: rosbridge v2.0 +- **Format**: JSON messages + +### Sample Messages +```javascript +// Speech command +{ + "op": "publish", + "topic": "/speech", + "type": "std_msgs/String", + "msg": {"data": "Hello from HRIStudio!"} +} + +// Movement command +{ + "op": "publish", + "topic": "/cmd_vel", + "type": "geometry_msgs/Twist", + "msg": { + "linear": {"x": 0.1, "y": 0.0, "z": 0.0}, + "angular": {"x": 0.0, "y": 0.0, "z": 0.0} + } +} + +// Subscribe to sensors +{ + "op": "subscribe", + "topic": "/naoqi_driver/joint_states", + "type": "sensor_msgs/JointState" +} +``` + +## 📋 Key Topics + +### Input Topics (Robot Control) +- `/speech` - Text-to-speech commands +- `/cmd_vel` - Movement control +- `/joint_angles` - Joint positioning +- `/led_control` - Visual feedback + +### Output Topics (Sensor Data) +- `/naoqi_driver/joint_states` - Joint positions/velocities +- `/naoqi_driver/bumper` - Foot sensors +- `/naoqi_driver/hand_touch` - Hand touch sensors +- `/naoqi_driver/head_touch` - Head touch sensors +- `/naoqi_driver/sonar/left` - Left ultrasonic sensor +- `/naoqi_driver/sonar/right` - Right ultrasonic sensor +- `/naoqi_driver/battery` - Battery level + +## 🛡️ Safety Features + +### Automated Safety +- **Velocity Limits** - Maximum speed constraints (0.2 m/s linear, 0.8 rad/s angular) +- **Emergency Stop** - Immediate motion termination +- **Battery Monitoring** - Low battery warnings +- **Fall Detection** - Automatic safety responses +- **Wake-up Management** - Proper robot state handling + +### Manual Safety Controls +```bash +# Emergency stop via CLI +ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}' + +# Emergency stop via script +python3 examples/nao_control.py --ip nao.local emergency + +# Or use HRIStudio emergency stop action +``` + +## 🔍 Troubleshooting + +### Common Issues + +**Robot not responding to commands** +```bash +# Check robot is awake +python3 examples/nao_control.py --ip nao.local status + +# Wake up robot +python3 examples/nao_control.py --ip nao.local wake +# OR press chest button for 3 seconds +``` + +**WebSocket connection failed** +```bash +# Check rosbridge is running +ros2 node list | grep rosbridge + +# Restart integration +pkill -f rosbridge && pkill -f rosapi +ros2 launch nao_launch nao6_production.launch.py nao_ip:=nao.local +``` + +**Network connectivity issues** +```bash +# Test basic connectivity +ping nao.local +telnet nao.local 9559 + +# Check robot credentials +ssh nao@nao.local # Password: robolab (institution-specific) +``` + +For detailed troubleshooting, see [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) + +## 📖 Documentation + +### Complete Guides +- **[Installation Guide](docs/INSTALLATION.md)** - Detailed setup instructions +- **[Usage Guide](docs/USAGE.md)** - Examples and best practices +- **[Integration Complete](docs/NAO6_INTEGRATION_COMPLETE.md)** - Comprehensive overview +- **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Problem resolution + +### Quick References +- **Launch Files** - See `launch/` directory +- **Plugin Definitions** - See `plugins/` directory +- **Example Scripts** - See `examples/` directory + +## 🎯 Research Applications + +### Experiment Types +- **Social Interaction** - Gestures, speech, gaze studies +- **Human-Robot Collaboration** - Shared task experiments +- **Behavior Analysis** - Touch, proximity, response studies +- **Navigation Studies** - Movement and spatial interaction +- **Multimodal Interaction** - Combined speech, gesture, movement + +### Data Capture +- **Synchronized Timestamps** - All robot actions and sensor events +- **Sensor Fusion** - Touch, vision, audio, movement data +- **Real-time Logging** - Comprehensive event capture +- **Export Capabilities** - Data analysis and visualization + +## 🏆 Features & Benefits + +### ✅ Production Ready +- **Tested Integration** - Verified with NAO V6.0 / NAOqi 2.8.7.4 +- **Safety First** - Comprehensive safety monitoring +- **Performance Optimized** - Tuned for stable experiments +- **Error Handling** - Robust failure management + +### ✅ Researcher Friendly +- **Web Interface** - No programming required for experiments +- **Visual Designer** - Drag-and-drop experiment creation +- **Real-time Control** - Live robot operation during trials +- **Multiple Roles** - Researcher, wizard, observer access + +### ✅ Developer Friendly +- **Open Source** - MIT licensed components +- **Modular Design** - Extensible architecture +- **Comprehensive APIs** - ROS2 and WebSocket interfaces +- **Documentation** - Complete setup and usage guides + +## 🚀 Getting Started Examples + +### Basic Experiment Workflow +1. **Design** - Create experiment in HRIStudio visual designer +2. **Configure** - Set robot parameters and safety limits +3. **Execute** - Run trial with real-time robot control +4. **Analyze** - Review captured data and events +5. **Iterate** - Refine experiment based on results + +### Sample Experiment: Greeting Interaction +```javascript +// HRIStudio experiment sequence +[ + {"action": "nao_wake_rest", "parameters": {"action": "wake"}}, + {"action": "nao_pose", "parameters": {"posture": "Stand"}}, + {"action": "nao_speak", "parameters": {"text": "Hello! Welcome to our study."}}, + {"action": "nao_gesture", "parameters": {"gesture": "wave"}}, + {"action": "nao_sensor_monitor", "parameters": {"sensorType": "touch", "duration": 30}} +] +``` + +## 🤝 Contributing + +### Development Setup +1. Fork this repository +2. Create feature branch: `git checkout -b feature-name` +3. Test with real NAO6 hardware +4. Submit pull request with documentation updates + +### Guidelines +- Follow ROS2 conventions for launch files +- Test all changes with physical robot +- Update documentation for new features +- Ensure backward compatibility + +## 📞 Support + +### Resources +- **GitHub Issues** - Report bugs and request features +- **Documentation** - Complete guides in `docs/` folder +- **HRIStudio Platform** - Web interface documentation + +### Requirements +- **NAO6 Robot** - NAO V6.0 with NAOqi 2.8.7.4+ +- **ROS2 Humble** - Ubuntu 22.04 recommended +- **Network Setup** - Robot and computer on same network +- **HRIStudio** - Web platform for experiment design + +## 📄 License + +MIT License - See LICENSE file for details + +## 🏅 Citation + +If you use this integration in your research, please cite: + +```bibtex +@software{nao6_hristudio_integration, + title={NAO6 HRIStudio Integration}, + author={HRIStudio RoboLab Team}, + year={2024}, + url={https://github.com/hristudio/nao6-integration}, + version={2.0.0} +} +``` + +--- + +**Status**: Production Ready ✅ +**Tested With**: NAO V6.0 / NAOqi 2.8.7.4 / ROS2 Humble / HRIStudio v1.0 +**Last Updated**: December 2024 + +*Advancing Human-Robot Interaction research through standardized, accessible, and reliable tools.* \ No newline at end of file diff --git a/next.config.js b/next.config.js old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 index e9410b0..607d970 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@hookform/resolvers": "^5.1.1", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.8", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", @@ -62,13 +63,15 @@ "date-fns": "^4.1.0", "drizzle-orm": "^0.41.0", "lucide-react": "^0.536.0", - "next": "^15.5.4", + "minio": "^8.0.6", + "next": "^16.0.10", "next-auth": "^5.0.0-beta.29", "postgres": "^3.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.60.0", "react-resizable-panels": "^3.0.4", + "react-webcam": "^7.2.0", "server-only": "^0.0.1", "sonner": "^2.0.7", "superjson": "^2.2.1", @@ -102,6 +105,8 @@ }, "trustedDependencies": [ "@tailwindcss/oxide", + "esbuild", + "sharp", "unrs-resolver" ] } diff --git a/plugin_dump.json b/plugin_dump.json new file mode 100644 index 0000000..09249bb --- /dev/null +++ b/plugin_dump.json @@ -0,0 +1,927 @@ + jsonb_pretty +--------------------------------------------------------------------------------------- + [ + + { + + "id": "walk_velocity", + + "icon": "navigation", + + "name": "Walk with Velocity", + + "ros2": { + + "qos": { + + "depth": 1, + + "history": "keep_last", + + "durability": "volatile", + + "reliability": "reliable" + + }, + + "topic": "/cmd_vel", + + "messageType": "geometry_msgs/msg/Twist", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "transformToTwist" + + } + + }, + + "timeout": 5000, + + "category": "movement", + + "retryable": true, + + "description": "Control robot walking with linear and angular velocities", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "linear", + + "angular" + + ], + + "properties": { + + "linear": { + + "type": "number", + + "default": 0, + + "maximum": 0.55, + + "minimum": -0.55, + + "description": "Forward velocity in m/s" + + }, + + "angular": { + + "type": "number", + + "default": 0, + + "maximum": 2, + + "minimum": -2, + + "description": "Angular velocity in rad/s" + + } + + } + + } + + }, + + { + + "id": "walk_forward", + + "icon": "arrow-up", + + "name": "Walk Forward", + + "ros2": { + + "topic": "/cmd_vel", + + "messageType": "geometry_msgs/msg/Twist", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "linear": { + + "x": "{{speed}}", + + "y": 0, + + "z": 0 + + }, + + "angular": { + + "x": 0, + + "y": 0, + + "z": 0 + + } + + } + + } + + }, + + "timeout": 30000, + + "category": "movement", + + "retryable": true, + + "description": "Make the robot walk forward at specified speed", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "speed" + + ], + + "properties": { + + "speed": { + + "type": "number", + + "default": 0.1, + + "maximum": 0.3, + + "minimum": 0.01, + + "description": "Walking speed in m/s" + + }, + + "duration": { + + "type": "number", + + "default": 0, + + "maximum": 30, + + "minimum": 0, + + "description": "Duration to walk in seconds (0 = indefinite)" + + } + + } + + } + + }, + + { + + "id": "walk_backward", + + "icon": "arrow-down", + + "name": "Walk Backward", + + "ros2": { + + "topic": "/cmd_vel", + + "messageType": "geometry_msgs/msg/Twist", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "linear": { + + "x": "-{{speed}}", + + "y": 0, + + "z": 0 + + }, + + "angular": { + + "x": 0, + + "y": 0, + + "z": 0 + + } + + } + + } + + }, + + "timeout": 30000, + + "category": "movement", + + "retryable": true, + + "description": "Make the robot walk backward at specified speed", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "speed" + + ], + + "properties": { + + "speed": { + + "type": "number", + + "default": 0.1, + + "maximum": 0.3, + + "minimum": 0.01, + + "description": "Walking speed in m/s" + + }, + + "duration": { + + "type": "number", + + "default": 0, + + "maximum": 30, + + "minimum": 0, + + "description": "Duration to walk in seconds (0 = indefinite)" + + } + + } + + } + + }, + + { + + "id": "turn_left", + + "icon": "rotate-ccw", + + "name": "Turn Left", + + "ros2": { + + "topic": "/cmd_vel", + + "messageType": "geometry_msgs/msg/Twist", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "linear": { + + "x": 0, + + "y": 0, + + "z": 0 + + }, + + "angular": { + + "x": 0, + + "y": 0, + + "z": "{{speed}}" + + } + + } + + } + + }, + + "timeout": 30000, + + "category": "movement", + + "retryable": true, + + "description": "Make the robot turn left at specified angular speed", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "speed" + + ], + + "properties": { + + "speed": { + + "type": "number", + + "default": 0.3, + + "maximum": 1, + + "minimum": 0.1, + + "description": "Angular speed in rad/s" + + }, + + "duration": { + + "type": "number", + + "default": 0, + + "maximum": 30, + + "minimum": 0, + + "description": "Duration to turn in seconds (0 = indefinite)" + + } + + } + + } + + }, + + { + + "id": "turn_right", + + "icon": "rotate-cw", + + "name": "Turn Right", + + "ros2": { + + "topic": "/cmd_vel", + + "messageType": "geometry_msgs/msg/Twist", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "linear": { + + "x": 0, + + "y": 0, + + "z": 0 + + }, + + "angular": { + + "x": 0, + + "y": 0, + + "z": "-{{speed}}" + + } + + } + + } + + }, + + "timeout": 30000, + + "category": "movement", + + "retryable": true, + + "description": "Make the robot turn right at specified angular speed", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "speed" + + ], + + "properties": { + + "speed": { + + "type": "number", + + "default": 0.3, + + "maximum": 1, + + "minimum": 0.1, + + "description": "Angular speed in rad/s" + + }, + + "duration": { + + "type": "number", + + "default": 0, + + "maximum": 30, + + "minimum": 0, + + "description": "Duration to turn in seconds (0 = indefinite)" + + } + + } + + } + + }, + + { + + "id": "stop_walking", + + "icon": "square", + + "name": "Stop Walking", + + "ros2": { + + "qos": { + + "depth": 1, + + "history": "keep_last", + + "durability": "volatile", + + "reliability": "reliable" + + }, + + "topic": "/cmd_vel", + + "messageType": "geometry_msgs/msg/Twist", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "linear": { + + "x": 0, + + "y": 0, + + "z": 0 + + }, + + "angular": { + + "x": 0, + + "y": 0, + + "z": 0 + + } + + } + + } + + }, + + "timeout": 3000, + + "category": "movement", + + "retryable": false, + + "description": "Immediately stop robot movement", + + "parameterSchema": { + + "type": "object", + + "required": [ + + ], + + "properties": { + + } + + } + + }, + + { + + "id": "say_text", + + "icon": "volume-2", + + "name": "Say Text", + + "ros2": { + + "qos": { + + "durability": "volatile", + + "reliability": "reliable" + + }, + + "topic": "/speech", + + "messageType": "std_msgs/msg/String", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "transformToStringMessage" + + } + + }, + + "timeout": 15000, + + "category": "interaction", + + "retryable": true, + + "description": "Make the robot speak using text-to-speech", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "text" + + ], + + "properties": { + + "text": { + + "type": "string", + + "default": "Hello from NAO!", + + "description": "Text to speak" + + } + + } + + } + + }, + + { + + "id": "say_with_emotion", + + "icon": "heart", + + "name": "Say Text with Emotion", + + "ros2": { + + "topic": "/speech", + + "messageType": "std_msgs/msg/String", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "data": "\\rspd={{speed}}\\\\rst={{emotion}}\\{{text}}" + + } + + } + + }, + + "timeout": 15000, + + "category": "interaction", + + "retryable": true, + + "description": "Speak text with emotional expression using SSML-like markup",+ + "parameterSchema": { + + "type": "object", + + "required": [ + + "text" + + ], + + "properties": { + + "text": { + + "type": "string", + + "default": "Hello! I'm feeling great today!", + + "description": "Text for the robot to speak" + + }, + + "speed": { + + "type": "number", + + "default": 1, + + "maximum": 2, + + "minimum": 0.5, + + "description": "Speech speed multiplier" + + }, + + "emotion": { + + "enum": [ + + "neutral", + + "happy", + + "sad", + + "excited", + + "calm" + + ], + + "type": "string", + + "default": "neutral", + + "description": "Emotional tone for speech" + + } + + } + + } + + }, + + { + + "id": "set_volume", + + "icon": "volume-x", + + "name": "Set Volume", + + "ros2": { + + "topic": "/audio_volume", + + "messageType": "std_msgs/msg/Float32", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "data": "{{volume}}" + + } + + } + + }, + + "timeout": 5000, + + "category": "interaction", + + "retryable": true, + + "description": "Adjust the robot's audio volume level", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "volume" + + ], + + "properties": { + + "volume": { + + "type": "number", + + "default": 0.5, + + "maximum": 1, + + "minimum": 0, + + "description": "Volume level (0.0 = silent, 1.0 = maximum)" + + } + + } + + } + + }, + + { + + "id": "set_language", + + "icon": "globe", + + "name": "Set Language", + + "ros2": { + + "topic": "/set_language", + + "messageType": "std_msgs/msg/String", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "data": "{{language}}" + + } + + } + + }, + + "timeout": 5000, + + "category": "interaction", + + "retryable": true, + + "description": "Change the robot's speech language", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "language" + + ], + + "properties": { + + "language": { + + "enum": [ + + "en-US", + + "en-GB", + + "fr-FR", + + "de-DE", + + "es-ES", + + "it-IT", + + "ja-JP", + + "ko-KR", + + "zh-CN" + + ], + + "type": "string", + + "default": "en-US", + + "description": "Speech language" + + } + + } + + } + + }, + + { + + "id": "move_head", + + "icon": "eye", + + "name": "Move Head", + + "ros2": { + + "topic": "/joint_angles", + + "messageType": "naoqi_bridge_msgs/msg/JointAnglesWithSpeed", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "speed": "{{speed}}", + + "joint_names": [ + + "HeadYaw", + + "HeadPitch" + + ], + + "joint_angles": [ + + "{{yaw}}", + + "{{pitch}}" + + ] + + } + + } + + }, + + "timeout": 10000, + + "category": "movement", + + "retryable": true, + + "description": "Control head orientation (yaw and pitch)", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "yaw", + + "pitch" + + ], + + "properties": { + + "yaw": { + + "type": "number", + + "default": 0, + + "maximum": 2.09, + + "minimum": -2.09, + + "description": "Head yaw angle in radians" + + }, + + "pitch": { + + "type": "number", + + "default": 0, + + "maximum": 0.51, + + "minimum": -0.67, + + "description": "Head pitch angle in radians" + + }, + + "speed": { + + "type": "number", + + "default": 0.3, + + "maximum": 1, + + "minimum": 0.1, + + "description": "Movement speed (0.1 = slow, 1.0 = fast)" + + } + + } + + } + + }, + + { + + "id": "move_arm", + + "icon": "hand", + + "name": "Move Arm", + + "ros2": { + + "topic": "/joint_angles", + + "messageType": "naoqi_bridge_msgs/msg/JointAnglesWithSpeed", + + "payloadMapping": { + + "type": "static", + + "payload": { + + "speed": "{{speed}}", + + "joint_names": [ + + "{{arm === 'left' ? 'L' : 'R'}}ShoulderPitch", + + "{{arm === 'left' ? 'L' : 'R'}}ShoulderRoll", + + "{{arm === 'left' ? 'L' : 'R'}}ElbowYaw", + + "{{arm === 'left' ? 'L' : 'R'}}ElbowRoll" + + ], + + "joint_angles": [ + + "{{shoulder_pitch}}", + + "{{shoulder_roll}}", + + "{{elbow_yaw}}", + + "{{elbow_roll}}" + + ] + + } + + } + + }, + + "timeout": 10000, + + "category": "movement", + + "retryable": true, + + "description": "Control arm joint positions", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "arm", + + "shoulder_pitch", + + "shoulder_roll", + + "elbow_yaw", + + "elbow_roll" + + ], + + "properties": { + + "arm": { + + "enum": [ + + "left", + + "right" + + ], + + "type": "string", + + "default": "right", + + "description": "Which arm to control" + + }, + + "speed": { + + "type": "number", + + "default": 0.3, + + "maximum": 1, + + "minimum": 0.1, + + "description": "Movement speed (0.1 = slow, 1.0 = fast)" + + }, + + "elbow_yaw": { + + "type": "number", + + "default": 0, + + "maximum": 2.09, + + "minimum": -2.09, + + "description": "Elbow yaw angle in radians" + + }, + + "elbow_roll": { + + "type": "number", + + "default": -0.5, + + "maximum": -0.03, + + "minimum": -1.54, + + "description": "Elbow roll angle in radians" + + }, + + "shoulder_roll": { + + "type": "number", + + "default": 0.2, + + "maximum": 1.33, + + "minimum": -0.31, + + "description": "Shoulder roll angle in radians" + + }, + + "shoulder_pitch": { + + "type": "number", + + "default": 1.4, + + "maximum": 2.09, + + "minimum": -2.09, + + "description": "Shoulder pitch angle in radians" + + } + + } + + } + + }, + + { + + "id": "set_joint_angle", + + "icon": "settings", + + "name": "Set Joint Angle", + + "ros2": { + + "topic": "/joint_angles", + + "messageType": "naoqi_bridge_msgs/msg/JointAnglesWithSpeed", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "transformToJointAngles" + + } + + }, + + "timeout": 10000, + + "category": "movement", + + "retryable": true, + + "description": "Control individual joint angles", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "joint_name", + + "angle" + + ], + + "properties": { + + "angle": { + + "type": "number", + + "default": 0, + + "maximum": 3.14159, + + "minimum": -3.14159, + + "description": "Target angle in radians" + + }, + + "speed": { + + "type": "number", + + "default": 0.2, + + "maximum": 1, + + "minimum": 0.01, + + "description": "Movement speed (fraction of max)" + + }, + + "joint_name": { + + "enum": [ + + "HeadYaw", + + "HeadPitch", + + "LShoulderPitch", + + "LShoulderRoll", + + "LElbowYaw", + + "LElbowRoll", + + "LWristYaw", + + "RShoulderPitch", + + "RShoulderRoll", + + "RElbowYaw", + + "RElbowRoll", + + "RWristYaw", + + "LHipYawPitch", + + "LHipRoll", + + "LHipPitch", + + "LKneePitch", + + "LAnklePitch", + + "LAnkleRoll", + + "RHipRoll", + + "RHipPitch", + + "RKneePitch", + + "RAnklePitch", + + "RAnkleRoll" + + ], + + "type": "string", + + "default": "HeadYaw", + + "description": "Joint to control" + + } + + } + + } + + }, + + { + + "id": "turn_head", + + "icon": "rotate-ccw", + + "name": "Turn Head", + + "ros2": { + + "topic": "/joint_angles", + + "messageType": "naoqi_bridge_msgs/msg/JointAnglesWithSpeed", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "transformToHeadMovement" + + } + + }, + + "timeout": 8000, + + "category": "movement", + + "retryable": true, + + "description": "Control head orientation", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "yaw", + + "pitch" + + ], + + "properties": { + + "yaw": { + + "type": "number", + + "default": 0, + + "maximum": 2.0857, + + "minimum": -2.0857, + + "description": "Head yaw angle in radians (left-right)" + + }, + + "pitch": { + + "type": "number", + + "default": 0, + + "maximum": 0.5149, + + "minimum": -0.672, + + "description": "Head pitch angle in radians (up-down)" + + }, + + "speed": { + + "type": "number", + + "default": 0.3, + + "maximum": 1, + + "minimum": 0.1, + + "description": "Movement speed fraction" + + } + + } + + } + + }, + + { + + "id": "get_camera_image", + + "icon": "camera", + + "name": "Get Camera Image", + + "ros2": { + + "qos": { + + "durability": "volatile", + + "reliability": "reliable" + + }, + + "topic": "/camera/{camera}/image_raw", + + "messageType": "sensor_msgs/msg/Image", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "getCameraImage" + + } + + }, + + "timeout": 5000, + + "category": "sensors", + + "retryable": true, + + "description": "Capture image from front or bottom camera", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "camera" + + ], + + "properties": { + + "camera": { + + "enum": [ + + "front", + + "bottom" + + ], + + "type": "string", + + "default": "front", + + "description": "Camera to use" + + } + + } + + } + + }, + + { + + "id": "get_joint_states", + + "icon": "activity", + + "name": "Get Joint States", + + "ros2": { + + "qos": { + + "durability": "volatile", + + "reliability": "reliable" + + }, + + "topic": "/joint_states", + + "messageType": "sensor_msgs/msg/JointState", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "getJointStates" + + } + + }, + + "timeout": 3000, + + "category": "sensors", + + "retryable": true, + + "description": "Read current joint positions and velocities", + + "parameterSchema": { + + "type": "object", + + "required": [ + + ], + + "properties": { + + } + + } + + }, + + { + + "id": "get_imu_data", + + "icon": "compass", + + "name": "Get IMU Data", + + "ros2": { + + "qos": { + + "durability": "volatile", + + "reliability": "reliable" + + }, + + "topic": "/imu/torso", + + "messageType": "sensor_msgs/msg/Imu", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "getImuData" + + } + + }, + + "timeout": 3000, + + "category": "sensors", + + "retryable": true, + + "description": "Read inertial measurement unit data from torso", + + "parameterSchema": { + + "type": "object", + + "required": [ + + ], + + "properties": { + + } + + } + + }, + + { + + "id": "get_bumper_status", + + "icon": "zap", + + "name": "Get Bumper Status", + + "ros2": { + + "topic": "/bumper", + + "messageType": "naoqi_bridge_msgs/msg/Bumper", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "getBumperStatus" + + } + + }, + + "timeout": 3000, + + "category": "sensors", + + "retryable": true, + + "description": "Read foot bumper contact sensors", + + "parameterSchema": { + + "type": "object", + + "required": [ + + ], + + "properties": { + + } + + } + + }, + + { + + "id": "get_touch_sensors", + + "icon": "hand", + + "name": "Get Touch Sensors", + + "ros2": { + + "topic": "/{sensor_type}_touch", + + "messageType": "naoqi_bridge_msgs/msg/HandTouch", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "getTouchSensors" + + } + + }, + + "timeout": 3000, + + "category": "sensors", + + "retryable": true, + + "description": "Read hand and head touch sensor states", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "sensor_type" + + ], + + "properties": { + + "sensor_type": { + + "enum": [ + + "hand", + + "head" + + ], + + "type": "string", + + "default": "hand", + + "description": "Touch sensor type to read" + + } + + } + + } + + }, + + { + + "id": "get_sonar_range", + + "icon": "radio", + + "name": "Get Sonar Range", + + "ros2": { + + "topic": "/sonar/{sensor}", + + "messageType": "sensor_msgs/msg/Range", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "getSonarRange" + + } + + }, + + "timeout": 3000, + + "category": "sensors", + + "retryable": true, + + "description": "Read ultrasonic range sensor data", + + "parameterSchema": { + + "type": "object", + + "required": [ + + "sensor" + + ], + + "properties": { + + "sensor": { + + "enum": [ + + "left", + + "right", + + "both" + + ], + + "type": "string", + + "default": "both", + + "description": "Sonar sensor to read" + + } + + } + + } + + }, + + { + + "id": "get_robot_info", + + "icon": "info", + + "name": "Get Robot Info", + + "ros2": { + + "topic": "/info", + + "messageType": "naoqi_bridge_msgs/msg/RobotInfo", + + "payloadMapping": { + + "type": "transform", + + "transformFn": "getRobotInfo" + + } + + }, + + "timeout": 3000, + + "category": "sensors", + + "retryable": true, + + "description": "Read general robot information and status", + + "parameterSchema": { + + "type": "object", + + "required": [ + + ], + + "properties": { + + } + + } + + } + + ] +(1 row) + diff --git a/postcss.config.js b/postcss.config.js old mode 100644 new mode 100755 diff --git a/prettier.config.js b/prettier.config.js old mode 100644 new mode 100755 diff --git a/public/favicon.ico b/public/favicon.ico old mode 100644 new mode 100755 diff --git a/public/hristudio-core/plugins/control-flow.json b/public/hristudio-core/plugins/control-flow.json old mode 100644 new mode 100755 diff --git a/public/hristudio-core/plugins/events.json b/public/hristudio-core/plugins/events.json old mode 100644 new mode 100755 diff --git a/public/hristudio-core/plugins/index.json b/public/hristudio-core/plugins/index.json old mode 100644 new mode 100755 diff --git a/public/hristudio-core/plugins/observation.json b/public/hristudio-core/plugins/observation.json old mode 100644 new mode 100755 diff --git a/public/hristudio-core/plugins/wizard-actions.json b/public/hristudio-core/plugins/wizard-actions.json old mode 100644 new mode 100755 diff --git a/public/hristudio-core/repository.json b/public/hristudio-core/repository.json old mode 100644 new mode 100755 diff --git a/public/nao6-plugins/README.md b/public/nao6-plugins/README.md new file mode 100644 index 0000000..379275e --- /dev/null +++ b/public/nao6-plugins/README.md @@ -0,0 +1,289 @@ +# NAO6 HRIStudio Plugin Repository + +**Official NAO6 robot integration plugins for the HRIStudio platform** + +## Overview + +This repository contains production-ready plugins for integrating NAO6 robots with HRIStudio experiments. The plugins provide comprehensive robot control capabilities including movement, speech synthesis, sensor monitoring, and safety features optimized for human-robot interaction research. + +## Available Plugins + +### 🤖 NAO6 Enhanced ROS2 Integration (`nao6-ros2-enhanced.json`) + +**Complete NAO6 robot control for HRIStudio experiments** + +**Features:** +- ✅ **Speech Synthesis** - Text-to-speech with volume and speed control +- ✅ **Movement Control** - Walking, turning, and precise positioning +- ✅ **Posture Management** - Stand, sit, crouch, and custom poses +- ✅ **Head Movement** - Gaze control and attention direction +- ✅ **Gesture Library** - Wave, point, applause, and custom animations +- ✅ **LED Control** - Visual feedback with colors and patterns +- ✅ **Sensor Monitoring** - Touch, bumper, sonar, and camera sensors +- ✅ **Safety Features** - Emergency stop and velocity limits +- ✅ **System Control** - Wake/rest and status monitoring + +**Requirements:** +- NAO6 robot with NAOqi 2.8.7.4+ +- ROS2 Humble or compatible +- Network connectivity to robot +- `nao_launch` package for ROS integration + +**Installation:** +1. Install in HRIStudio study via Plugin Management +2. Configure robot IP and WebSocket URL +3. Launch ROS integration: `ros2 launch nao_launch nao6_production.launch.py` +4. Test connection in HRIStudio experiment designer + +## Plugin Actions Reference + +### Speech & Communication +| Action | Description | Parameters | +|--------|-------------|------------| +| **Speak Text** | Text-to-speech synthesis | text, volume, speed, wait | +| **LED Control** | Visual feedback with colors | ledGroup, color, intensity, pattern | + +### Movement & Posture +| Action | Description | Parameters | +|--------|-------------|------------| +| **Move Robot** | Linear and angular movement | direction, distance, speed, duration | +| **Set Posture** | Predefined poses | posture, speed, waitForCompletion | +| **Move Head** | Gaze and attention control | yaw, pitch, speed, presetDirection | +| **Perform Gesture** | Animations and gestures | gesture, intensity, speed, repeatCount | + +### Sensors & Monitoring +| Action | Description | Parameters | +|--------|-------------|------------| +| **Monitor Sensors** | Touch, bumper, sonar detection | sensorType, duration, sensitivity | +| **Check Robot Status** | Battery, joints, system health | statusType, logToExperiment | + +### Safety & System +| Action | Description | Parameters | +|--------|-------------|------------| +| **Emergency Stop** | Immediate motion termination | stopType, safePosture | +| **Wake Up / Rest** | Power management | action, waitForCompletion | + +## Quick Start Examples + +### 1. Basic Greeting +```json +{ + "sequence": [ + {"action": "nao_wake_rest", "parameters": {"action": "wake"}}, + {"action": "nao_speak", "parameters": {"text": "Hello! Welcome to our experiment."}}, + {"action": "nao_gesture", "parameters": {"gesture": "wave"}} + ] +} +``` + +### 2. Interactive Task +```json +{ + "sequence": [ + {"action": "nao_speak", "parameters": {"text": "Please touch my head when ready."}}, + {"action": "nao_sensor_monitor", "parameters": {"sensorType": "touch", "duration": 30}}, + {"action": "nao_speak", "parameters": {"text": "Thank you! Let's begin."}} + ] +} +``` + +### 3. Attention Direction +```json +{ + "sequence": [ + {"action": "nao_head_movement", "parameters": {"presetDirection": "left"}}, + {"action": "nao_speak", "parameters": {"text": "Look over there please."}}, + {"action": "nao_gesture", "parameters": {"gesture": "point_left"}} + ] +} +``` + +## Installation & Setup + +### Prerequisites +- **HRIStudio Platform** - Web-based WoZ research platform +- **NAO6 Robot** - With NAOqi 2.8.7.4 or compatible +- **ROS2 Humble** - Robot Operating System 2 +- **Network Setup** - Robot and computer on same network + +### Step 1: Install NAO ROS2 Packages +```bash +# Clone and build NAO ROS2 workspace +cd ~/naoqi_ros2_ws +colcon build --packages-select nao_launch +source install/setup.bash +``` + +### Step 2: Start Robot Integration +```bash +# Launch comprehensive NAO integration +ros2 launch nao_launch nao6_production.launch.py \ + nao_ip:=nao.local \ + password:=robolab \ + bridge_port:=9090 +``` + +### Step 3: Install Plugin in HRIStudio +1. **Access HRIStudio** - Open your study in HRIStudio +2. **Plugin Management** - Go to Study → Plugins +3. **Browse Store** - Find "NAO6 Robot (Enhanced ROS2 Integration)" +4. **Install Plugin** - Click install and configure settings +5. **Configure WebSocket** - Set URL to `ws://localhost:9090` + +### Step 4: Test Integration +1. **Open Experiment Designer** - Create or edit an experiment +2. **Add Robot Action** - Drag NAO6 action from plugin section +3. **Configure Parameters** - Set speech text, movement, etc. +4. **Test Connection** - Use "Check Robot Status" action +5. **Run Trial** - Execute experiment and verify robot responds + +## Configuration Options + +### Robot Connection +- **Robot IP** - IP address or hostname (default: `nao.local`) +- **Password** - Robot authentication password +- **WebSocket URL** - ROS bridge connection (default: `ws://localhost:9090`) + +### Safety Settings +- **Max Linear Velocity** - Maximum movement speed (default: 0.2 m/s) +- **Max Angular Velocity** - Maximum rotation speed (default: 0.8 rad/s) +- **Safety Monitoring** - Enable automatic safety checks +- **Auto Wake-up** - Automatically wake robot when experiment starts + +### Performance Tuning +- **Speech Volume** - Default volume level (default: 0.7) +- **Movement Speed** - Default movement speed factor (default: 0.5) +- **Battery Monitoring** - Track battery level during experiments + +## Troubleshooting + +### ❌ Robot Not Responding +**Problem:** Commands sent but robot doesn't react +**Solution:** +- Check robot is awake: Press chest button for 3 seconds +- Verify network connectivity: `ping nao.local` +- Use "Wake Up / Rest Robot" action in experiment + +### ❌ WebSocket Connection Failed +**Problem:** HRIStudio cannot connect to robot +**Solution:** +- Verify rosbridge is running: `ros2 node list | grep rosbridge` +- Check port availability: `ss -an | grep 9090` +- Restart integration: Kill processes and relaunch + +### ❌ Movements Too Fast/Unsafe +**Problem:** Robot moves too quickly or unpredictably +**Solution:** +- Reduce max velocities in plugin configuration +- Lower movement speed parameters in actions +- Use "Emergency Stop" action if needed + +### ❌ Speech Not Working +**Problem:** Robot doesn't speak or audio issues +**Solution:** +- Check robot volume settings +- Verify text-to-speech service: `ros2 topic echo /speech` +- Ensure speakers are functioning + +## Safety Guidelines + +### ⚠️ Important Safety Notes +- **Clear Space** - Ensure 2m clearance around robot during movement +- **Emergency Stop** - Keep emergency stop action easily accessible +- **Supervision** - Never leave robot unattended during experiments +- **Battery Monitoring** - Check battery level for long sessions +- **Stable Surface** - Keep robot on level, stable flooring + +### Emergency Procedures +```bash +# Immediate stop via CLI +ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}' + +# Or use HRIStudio emergency stop action +# Add "Emergency Stop" action to experiment for quick access +``` + +## Technical Details + +### ROS2 Topics Used +- **Input Topics** (Robot Control): + - `/speech` - Text-to-speech commands + - `/cmd_vel` - Movement commands + - `/joint_angles` - Joint position control + - `/led_control` - LED color control + +- **Output Topics** (Sensor Data): + - `/naoqi_driver/joint_states` - Joint positions + - `/naoqi_driver/bumper` - Foot sensors + - `/naoqi_driver/hand_touch` - Hand sensors + - `/naoqi_driver/head_touch` - Head sensors + - `/naoqi_driver/sonar/*` - Ultrasonic sensors + +### WebSocket Communication +- **Protocol** - rosbridge v2.0 WebSocket +- **Default Port** - 9090 +- **Message Format** - JSON-based ROS message serialization +- **Authentication** - None (local network) + +## Development & Contributing + +### Plugin Development +1. **Follow Schema** - Use provided JSON schema for action definitions +2. **Test Thoroughly** - Verify with real NAO6 hardware +3. **Document Actions** - Provide clear parameter descriptions +4. **Safety First** - Include appropriate safety measures + +### Testing Checklist +- [ ] Robot connectivity and wake-up +- [ ] All movement actions with safety limits +- [ ] Speech synthesis with various texts +- [ ] Sensor monitoring and event detection +- [ ] Emergency stop functionality +- [ ] WebSocket communication stability + +## Support & Resources + +### Documentation +- **HRIStudio Docs** - [Platform documentation](../../docs/) +- **NAO6 Integration Guide** - [Complete setup guide](../../docs/nao6-integration-complete-guide.md) +- **Quick Reference** - [Essential commands](../../docs/nao6-quick-reference.md) + +### Community & Support +- **GitHub Repository** - [hristudio/nao6-ros2-plugins](https://github.com/hristudio/nao6-ros2-plugins) +- **Issue Tracker** - Report bugs and request features +- **Email Support** - robolab@hristudio.com + +### Version Information +- **Plugin Version** - 2.0.0 (Enhanced Integration) +- **HRIStudio Compatibility** - v1.0+ +- **ROS2 Distro** - Humble (recommended) +- **NAO6 Compatibility** - NAOqi 2.8.7.4+ +- **Last Updated** - December 2024 + +--- + +## License + +**MIT License** - See [LICENSE](LICENSE) file for details + +## Citation + +If you use these plugins in your research, please cite: + +```bibtex +@software{nao6_hristudio_plugins, + title={NAO6 HRIStudio Integration Plugins}, + author={HRIStudio RoboLab Team}, + year={2024}, + url={https://github.com/hristudio/nao6-ros2-plugins}, + version={2.0.0} +} +``` + +--- + +**Maintained by:** HRIStudio RoboLab Team +**Contact:** robolab@hristudio.com +**Repository:** [hristudio/nao6-ros2-plugins](https://github.com/hristudio/nao6-ros2-plugins) + +*Part of the HRIStudio platform for advancing Human-Robot Interaction research* \ No newline at end of file diff --git a/public/nao6-plugins/nao6-ros2-enhanced.json b/public/nao6-plugins/nao6-ros2-enhanced.json new file mode 100644 index 0000000..981f532 --- /dev/null +++ b/public/nao6-plugins/nao6-ros2-enhanced.json @@ -0,0 +1,769 @@ +{ + "id": "nao6-ros2-enhanced", + "name": "NAO6 Robot (Enhanced ROS2 Integration)", + "version": "2.0.0", + "description": "Comprehensive NAO6 robot integration for HRIStudio experiments via ROS2. Provides full robot control including movement, speech synthesis, posture control, sensor monitoring, and safety features. Optimized for human-robot interaction research with production-ready reliability.", + "author": { + "name": "HRIStudio RoboLab Team", + "email": "robolab@hristudio.com", + "organization": "HRIStudio Research Platform" + }, + "license": "MIT", + "repositoryUrl": "https://github.com/hristudio/nao6-ros2-plugins", + "documentationUrl": "https://docs.hristudio.com/robots/nao6", + "trustLevel": "official", + "status": "active", + "tags": ["nao6", "ros2", "speech", "movement", "sensors", "hri", "production"], + "robotId": "nao6-softbank", + "communicationProtocol": "ros2_websocket", + "metadata": { + "robotModel": "NAO V6.0", + "manufacturer": "SoftBank Robotics", + "naoqiVersion": "2.8.7.4", + "ros2Distro": "humble", + "websocketUrl": "ws://localhost:9090", + "launchPackage": "nao_launch", + "requiredPackages": [ + "naoqi_driver2", + "naoqi_bridge_msgs", + "rosbridge_server", + "rosapi" + ], + "safetyFeatures": { + "emergencyStop": true, + "velocityLimits": true, + "fallDetection": true, + "batteryMonitoring": true, + "automaticWakeup": true + }, + "capabilities": [ + "bipedal_walking", + "speech_synthesis", + "head_movement", + "arm_gestures", + "touch_sensors", + "visual_sensors", + "audio_sensors", + "posture_control", + "balance_control" + ] + }, + "configurationSchema": { + "type": "object", + "properties": { + "robotIp": { + "type": "string", + "default": "nao.local", + "title": "Robot IP Address", + "description": "IP address or hostname of the NAO6 robot" + }, + "robotPassword": { + "type": "string", + "default": "robolab", + "title": "Robot Password", + "description": "Password for robot authentication", + "format": "password" + }, + "websocketUrl": { + "type": "string", + "default": "ws://localhost:9090", + "title": "WebSocket URL", + "description": "ROS bridge WebSocket URL for robot communication" + }, + "maxLinearVelocity": { + "type": "number", + "default": 0.2, + "minimum": 0.01, + "maximum": 0.5, + "title": "Max Linear Velocity (m/s)", + "description": "Maximum allowed linear movement speed for safety" + }, + "maxAngularVelocity": { + "type": "number", + "default": 0.8, + "minimum": 0.1, + "maximum": 2.0, + "title": "Max Angular Velocity (rad/s)", + "description": "Maximum allowed rotational speed for safety" + }, + "defaultMovementSpeed": { + "type": "number", + "default": 0.5, + "minimum": 0.1, + "maximum": 1.0, + "title": "Default Movement Speed", + "description": "Speed factor for posture and gesture movements (0.1-1.0)" + }, + "speechVolume": { + "type": "number", + "default": 0.7, + "minimum": 0.1, + "maximum": 1.0, + "title": "Speech Volume", + "description": "Default volume for speech synthesis (0.1-1.0)" + }, + "enableSafetyMonitoring": { + "type": "boolean", + "default": true, + "title": "Enable Safety Monitoring", + "description": "Enable automatic safety monitoring and emergency stops" + }, + "autoWakeUp": { + "type": "boolean", + "default": true, + "title": "Auto Wake-up Robot", + "description": "Automatically wake up robot when experiment starts" + }, + "monitorBattery": { + "type": "boolean", + "default": true, + "title": "Monitor Battery", + "description": "Monitor robot battery level during experiments" + } + }, + "required": ["robotIp", "websocketUrl"] + }, + "actionDefinitions": [ + { + "id": "nao_speak", + "name": "Speak Text", + "description": "Make the NAO robot speak the specified text using text-to-speech synthesis", + "category": "speech", + "icon": "volume2", + "parametersSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "title": "Text to Speak", + "description": "The text that the robot should speak aloud", + "minLength": 1, + "maxLength": 500 + }, + "volume": { + "type": "number", + "title": "Volume", + "description": "Speech volume level (0.1 = quiet, 1.0 = loud)", + "default": 0.7, + "minimum": 0.1, + "maximum": 1.0, + "step": 0.1 + }, + "speed": { + "type": "number", + "title": "Speech Speed", + "description": "Speech rate multiplier (0.5 = slow, 2.0 = fast)", + "default": 1.0, + "minimum": 0.5, + "maximum": 2.0, + "step": 0.1 + }, + "waitForCompletion": { + "type": "boolean", + "title": "Wait for Speech to Complete", + "description": "Wait until speech finishes before continuing to next action", + "default": true + } + }, + "required": ["text"] + }, + "implementation": { + "type": "ros2_topic", + "topic": "/speech", + "messageType": "std_msgs/String", + "messageMapping": { + "data": "{{text}}" + } + } + }, + { + "id": "nao_move", + "name": "Move Robot", + "description": "Move the NAO robot with specified linear and angular velocities", + "category": "movement", + "icon": "move", + "parametersSchema": { + "type": "object", + "properties": { + "direction": { + "type": "string", + "title": "Movement Direction", + "description": "Predefined movement direction", + "enum": ["forward", "backward", "left", "right", "turn_left", "turn_right", "custom"], + "enumNames": ["Forward", "Backward", "Step Left", "Step Right", "Turn Left", "Turn Right", "Custom"], + "default": "forward" + }, + "distance": { + "type": "number", + "title": "Distance/Angle", + "description": "Distance in meters for linear movement, or angle in degrees for rotation", + "default": 0.1, + "minimum": 0.01, + "maximum": 2.0, + "step": 0.01 + }, + "speed": { + "type": "number", + "title": "Movement Speed", + "description": "Speed factor (0.1 = very slow, 1.0 = normal speed)", + "default": 0.5, + "minimum": 0.1, + "maximum": 1.0, + "step": 0.1 + }, + "customX": { + "type": "number", + "title": "Custom X Velocity (m/s)", + "description": "Forward/backward velocity (positive = forward)", + "default": 0.0, + "minimum": -0.3, + "maximum": 0.3, + "step": 0.01 + }, + "customY": { + "type": "number", + "title": "Custom Y Velocity (m/s)", + "description": "Left/right velocity (positive = left)", + "default": 0.0, + "minimum": -0.3, + "maximum": 0.3, + "step": 0.01 + }, + "customTheta": { + "type": "number", + "title": "Custom Angular Velocity (rad/s)", + "description": "Rotational velocity (positive = counter-clockwise)", + "default": 0.0, + "minimum": -1.5, + "maximum": 1.5, + "step": 0.01 + }, + "duration": { + "type": "number", + "title": "Duration (seconds)", + "description": "How long to maintain the movement", + "default": 2.0, + "minimum": 0.1, + "maximum": 10.0, + "step": 0.1 + } + }, + "required": ["direction"] + }, + "implementation": { + "type": "ros2_topic", + "topic": "/cmd_vel", + "messageType": "geometry_msgs/Twist", + "messageMapping": { + "linear": { + "x": "{{#eq direction 'forward'}}{{multiply distance speed 0.1}}{{/eq}}{{#eq direction 'backward'}}{{multiply distance speed -0.1}}{{/eq}}{{#eq direction 'custom'}}{{customX}}{{/eq}}{{#default}}0.0{{/default}}", + "y": "{{#eq direction 'left'}}{{multiply distance speed 0.1}}{{/eq}}{{#eq direction 'right'}}{{multiply distance speed -0.1}}{{/eq}}{{#eq direction 'custom'}}{{customY}}{{/eq}}{{#default}}0.0{{/default}}", + "z": 0.0 + }, + "angular": { + "x": 0.0, + "y": 0.0, + "z": "{{#eq direction 'turn_left'}}{{multiply distance speed 0.1}}{{/eq}}{{#eq direction 'turn_right'}}{{multiply distance speed -0.1}}{{/eq}}{{#eq direction 'custom'}}{{customTheta}}{{/eq}}{{#default}}0.0{{/default}}" + } + } + } + }, + { + "id": "nao_pose", + "name": "Set Posture", + "description": "Set the NAO robot to a specific posture or pose", + "category": "movement", + "icon": "user", + "parametersSchema": { + "type": "object", + "properties": { + "posture": { + "type": "string", + "title": "Posture", + "description": "Target posture for the robot", + "enum": ["Stand", "Sit", "SitRelax", "StandInit", "StandZero", "Crouch", "LyingBack", "LyingBelly"], + "enumNames": ["Stand", "Sit", "Sit Relaxed", "Stand Initial", "Stand Zero", "Crouch", "Lying on Back", "Lying on Belly"], + "default": "Stand" + }, + "speed": { + "type": "number", + "title": "Movement Speed", + "description": "Speed of posture transition (0.1 = slow, 1.0 = fast)", + "default": 0.5, + "minimum": 0.1, + "maximum": 1.0, + "step": 0.1 + }, + "waitForCompletion": { + "type": "boolean", + "title": "Wait for Completion", + "description": "Wait until posture change is complete before continuing", + "default": true + } + }, + "required": ["posture"] + }, + "implementation": { + "type": "ros2_service", + "service": "/naoqi_driver/robot_posture/go_to_posture", + "serviceType": "naoqi_bridge_msgs/srv/SetString", + "requestMapping": { + "data": "{{posture}}" + } + } + }, + { + "id": "nao_head_movement", + "name": "Move Head", + "description": "Control NAO robot head movement for gaze direction and attention", + "category": "movement", + "icon": "eye", + "parametersSchema": { + "type": "object", + "properties": { + "headYaw": { + "type": "number", + "title": "Head Yaw (degrees)", + "description": "Left/right head rotation (-90° = right, +90° = left)", + "default": 0.0, + "minimum": -90.0, + "maximum": 90.0, + "step": 1.0 + }, + "headPitch": { + "type": "number", + "title": "Head Pitch (degrees)", + "description": "Up/down head rotation (-25° = down, +25° = up)", + "default": 0.0, + "minimum": -25.0, + "maximum": 25.0, + "step": 1.0 + }, + "speed": { + "type": "number", + "title": "Movement Speed", + "description": "Speed of head movement (0.1 = slow, 1.0 = fast)", + "default": 0.3, + "minimum": 0.1, + "maximum": 1.0, + "step": 0.1 + }, + "presetDirection": { + "type": "string", + "title": "Preset Direction", + "description": "Use preset head direction instead of custom angles", + "enum": ["none", "center", "left", "right", "up", "down", "look_left", "look_right"], + "enumNames": ["Custom Angles", "Center", "Left", "Right", "Up", "Down", "Look Left", "Look Right"], + "default": "none" + } + }, + "required": [] + }, + "implementation": { + "type": "ros2_topic", + "topic": "/joint_angles", + "messageType": "naoqi_bridge_msgs/JointAnglesWithSpeed", + "messageMapping": { + "joint_names": ["HeadYaw", "HeadPitch"], + "joint_angles": [ + "{{#ne presetDirection 'none'}}{{#eq presetDirection 'left'}}1.57{{/eq}}{{#eq presetDirection 'right'}}-1.57{{/eq}}{{#eq presetDirection 'center'}}0.0{{/eq}}{{#eq presetDirection 'look_left'}}0.78{{/eq}}{{#eq presetDirection 'look_right'}}-0.78{{/eq}}{{#default}}{{multiply headYaw 0.0175}}{{/default}}{{/ne}}{{#eq presetDirection 'none'}}{{multiply headYaw 0.0175}}{{/eq}}", + "{{#ne presetDirection 'none'}}{{#eq presetDirection 'up'}}0.44{{/eq}}{{#eq presetDirection 'down'}}-0.44{{/eq}}{{#eq presetDirection 'center'}}0.0{{/eq}}{{#default}}{{multiply headPitch 0.0175}}{{/default}}{{/ne}}{{#eq presetDirection 'none'}}{{multiply headPitch 0.0175}}{{/eq}}" + ], + "speed": "{{speed}}" + } + } + }, + { + "id": "nao_gesture", + "name": "Perform Gesture", + "description": "Make NAO robot perform predefined gestures and animations", + "category": "interaction", + "icon": "hand", + "parametersSchema": { + "type": "object", + "properties": { + "gesture": { + "type": "string", + "title": "Gesture Type", + "description": "Select a predefined gesture or animation", + "enum": ["wave", "point_left", "point_right", "applause", "thumbs_up", "open_arms", "bow", "celebration", "thinking", "custom"], + "enumNames": ["Wave Hello", "Point Left", "Point Right", "Applause", "Thumbs Up", "Open Arms", "Bow", "Celebration", "Thinking Pose", "Custom Joint Movement"], + "default": "wave" + }, + "intensity": { + "type": "number", + "title": "Gesture Intensity", + "description": "Intensity of the gesture movement (0.5 = subtle, 1.0 = full)", + "default": 0.8, + "minimum": 0.3, + "maximum": 1.0, + "step": 0.1 + }, + "speed": { + "type": "number", + "title": "Gesture Speed", + "description": "Speed of gesture execution (0.1 = slow, 1.0 = fast)", + "default": 0.5, + "minimum": 0.1, + "maximum": 1.0, + "step": 0.1 + }, + "repeatCount": { + "type": "integer", + "title": "Repeat Count", + "description": "Number of times to repeat the gesture", + "default": 1, + "minimum": 1, + "maximum": 5 + } + }, + "required": ["gesture"] + }, + "implementation": { + "type": "ros2_service", + "service": "/naoqi_driver/animation_player/run_animation", + "serviceType": "naoqi_bridge_msgs/srv/SetString", + "requestMapping": { + "data": "{{#eq gesture 'wave'}}animations/Stand/Gestures/Hey_1{{/eq}}{{#eq gesture 'point_left'}}animations/Stand/Gestures/YouKnowWhat_1{{/eq}}{{#eq gesture 'point_right'}}animations/Stand/Gestures/YouKnowWhat_2{{/eq}}{{#eq gesture 'applause'}}animations/Stand/Gestures/Applause_1{{/eq}}{{#eq gesture 'thumbs_up'}}animations/Stand/Gestures/Yes_1{{/eq}}{{#eq gesture 'open_arms'}}animations/Stand/Gestures/Everything_1{{/eq}}{{#eq gesture 'bow'}}animations/Stand/Gestures/BowShort_1{{/eq}}{{#eq gesture 'celebration'}}animations/Stand/Gestures/Excited_1{{/eq}}{{#eq gesture 'thinking'}}animations/Stand/Gestures/Thinking_1{{/eq}}" + } + } + }, + { + "id": "nao_led_control", + "name": "Control LEDs", + "description": "Control NAO robot LED colors and patterns for visual feedback", + "category": "interaction", + "icon": "lightbulb", + "parametersSchema": { + "type": "object", + "properties": { + "ledGroup": { + "type": "string", + "title": "LED Group", + "description": "Which LED group to control", + "enum": ["eyes", "ears", "chest", "feet", "all"], + "enumNames": ["Eyes", "Ears", "Chest", "Feet", "All LEDs"], + "default": "eyes" + }, + "color": { + "type": "string", + "title": "LED Color", + "description": "Color for the LEDs", + "enum": ["red", "green", "blue", "yellow", "cyan", "magenta", "white", "orange", "purple", "off"], + "enumNames": ["Red", "Green", "Blue", "Yellow", "Cyan", "Magenta", "White", "Orange", "Purple", "Off"], + "default": "blue" + }, + "intensity": { + "type": "number", + "title": "LED Intensity", + "description": "Brightness of the LEDs (0.0 = off, 1.0 = maximum)", + "default": 0.8, + "minimum": 0.0, + "maximum": 1.0, + "step": 0.1 + }, + "pattern": { + "type": "string", + "title": "LED Pattern", + "description": "LED animation pattern", + "enum": ["solid", "blink", "fade", "pulse", "rainbow"], + "enumNames": ["Solid", "Blink", "Fade In/Out", "Pulse", "Rainbow Cycle"], + "default": "solid" + }, + "duration": { + "type": "number", + "title": "Duration (seconds)", + "description": "How long to maintain the LED state (0 = indefinite)", + "default": 0, + "minimum": 0, + "maximum": 60, + "step": 1 + } + }, + "required": ["ledGroup", "color"] + }, + "implementation": { + "type": "ros2_topic", + "topic": "/led_control", + "messageType": "naoqi_bridge_msgs/Led", + "messageMapping": { + "name": "{{ledGroup}}", + "color": "{{color}}", + "intensity": "{{intensity}}" + } + } + }, + { + "id": "nao_sensor_monitor", + "name": "Monitor Sensors", + "description": "Monitor NAO robot sensors for interaction detection and environmental awareness", + "category": "sensors", + "icon": "activity", + "parametersSchema": { + "type": "object", + "properties": { + "sensorType": { + "type": "string", + "title": "Sensor Type", + "description": "Which sensors to monitor", + "enum": ["touch", "bumper", "sonar", "camera", "audio", "all"], + "enumNames": ["Touch Sensors", "Foot Bumpers", "Ultrasonic Sensors", "Cameras", "Audio", "All Sensors"], + "default": "touch" + }, + "duration": { + "type": "number", + "title": "Monitoring Duration (seconds)", + "description": "How long to monitor sensors (0 = continuous)", + "default": 10, + "minimum": 0, + "maximum": 300, + "step": 1 + }, + "sensitivity": { + "type": "number", + "title": "Detection Sensitivity", + "description": "Sensitivity level for sensor detection (0.1 = low, 1.0 = high)", + "default": 0.7, + "minimum": 0.1, + "maximum": 1.0, + "step": 0.1 + }, + "logEvents": { + "type": "boolean", + "title": "Log Sensor Events", + "description": "Log all sensor events to experiment data", + "default": true + }, + "triggerAction": { + "type": "string", + "title": "Trigger Action", + "description": "Action to take when sensor is activated", + "enum": ["none", "speak", "gesture", "move", "led"], + "enumNames": ["No Action", "Speak Response", "Perform Gesture", "Move Robot", "LED Feedback"], + "default": "none" + } + }, + "required": ["sensorType"] + }, + "implementation": { + "type": "ros2_subscription", + "topics": [ + "/naoqi_driver/bumper", + "/naoqi_driver/hand_touch", + "/naoqi_driver/head_touch", + "/naoqi_driver/sonar/left", + "/naoqi_driver/sonar/right" + ], + "messageTypes": [ + "naoqi_bridge_msgs/Bumper", + "naoqi_bridge_msgs/HandTouch", + "naoqi_bridge_msgs/HeadTouch", + "sensor_msgs/Range", + "sensor_msgs/Range" + ] + } + }, + { + "id": "nao_emergency_stop", + "name": "Emergency Stop", + "description": "Immediately stop all robot movement and animations for safety", + "category": "safety", + "icon": "stop-circle", + "parametersSchema": { + "type": "object", + "properties": { + "stopType": { + "type": "string", + "title": "Stop Type", + "description": "Type of emergency stop to perform", + "enum": ["movement", "all", "freeze"], + "enumNames": ["Stop Movement Only", "Stop All Actions", "Freeze in Place"], + "default": "all" + }, + "safePosture": { + "type": "boolean", + "title": "Move to Safe Posture", + "description": "Automatically move to a safe posture after stopping", + "default": true + } + }, + "required": [] + }, + "implementation": { + "type": "ros2_topic", + "topic": "/cmd_vel", + "messageType": "geometry_msgs/Twist", + "messageMapping": { + "linear": {"x": 0.0, "y": 0.0, "z": 0.0}, + "angular": {"x": 0.0, "y": 0.0, "z": 0.0} + } + } + }, + { + "id": "nao_wake_rest", + "name": "Wake Up / Rest Robot", + "description": "Wake up the robot or put it to rest position for power management", + "category": "system", + "icon": "power", + "parametersSchema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "title": "Action", + "description": "Wake up robot or put to rest", + "enum": ["wake", "rest"], + "enumNames": ["Wake Up Robot", "Put Robot to Rest"], + "default": "wake" + }, + "waitForCompletion": { + "type": "boolean", + "title": "Wait for Completion", + "description": "Wait until wake/rest action is complete", + "default": true + } + }, + "required": ["action"] + }, + "implementation": { + "type": "ros2_service", + "service": "/naoqi_driver/motion/{{action}}_up", + "serviceType": "std_srvs/srv/Empty", + "requestMapping": {} + } + }, + { + "id": "nao_status_check", + "name": "Check Robot Status", + "description": "Get current robot status including battery, temperature, and system health", + "category": "system", + "icon": "info", + "parametersSchema": { + "type": "object", + "properties": { + "statusType": { + "type": "string", + "title": "Status Information", + "description": "What status information to retrieve", + "enum": ["basic", "battery", "sensors", "joints", "all"], + "enumNames": ["Basic Status", "Battery Info", "Sensor Status", "Joint Status", "Complete Status"], + "default": "basic" + }, + "logToExperiment": { + "type": "boolean", + "title": "Log to Experiment Data", + "description": "Save status information to experiment logs", + "default": true + } + }, + "required": ["statusType"] + }, + "implementation": { + "type": "ros2_service", + "service": "/naoqi_driver/get_robot_config", + "serviceType": "naoqi_bridge_msgs/srv/GetRobotInfo", + "requestMapping": {} + } + } + ], + "installation": { + "requirements": [ + "ROS2 Humble or compatible", + "NAO6 robot with NAOqi 2.8.7.4+", + "Network connectivity to robot", + "naoqi_driver2 package", + "rosbridge_suite package" + ], + "setup": [ + { + "step": 1, + "description": "Install NAO ROS2 packages", + "command": "cd ~/naoqi_ros2_ws && colcon build" + }, + { + "step": 2, + "description": "Start NAO integration", + "command": "ros2 launch nao_launch nao6_production.launch.py nao_ip:=nao.local password:=robolab" + }, + { + "step": 3, + "description": "Configure HRIStudio plugin", + "description_detail": "Set WebSocket URL to ws://localhost:9090 in plugin configuration" + } + ], + "verification": [ + { + "description": "Test robot connectivity", + "command": "ping nao.local" + }, + { + "description": "Verify ROS topics", + "command": "ros2 topic list | grep naoqi" + }, + { + "description": "Test WebSocket bridge", + "command": "ros2 node list | grep rosbridge" + } + ] + }, + "troubleshooting": { + "commonIssues": [ + { + "issue": "Robot not responding to commands", + "solution": "Ensure robot is awake. Use 'Wake Up / Rest Robot' action or press chest button for 3 seconds." + }, + { + "issue": "WebSocket connection failed", + "solution": "Check that rosbridge is running: ros2 node list | grep rosbridge. Restart if needed." + }, + { + "issue": "Robot movements too fast/unsafe", + "solution": "Adjust maxLinearVelocity and maxAngularVelocity in plugin configuration." + }, + { + "issue": "Speech not working", + "solution": "Check robot volume settings and ensure speech synthesis service is active." + } + ], + "safetyNotes": [ + "Always ensure clear space around robot during movement", + "Use Emergency Stop action if robot behaves unexpectedly", + "Monitor battery level during long experiments", + "Start with slow movements to test robot response", + "Keep robot on stable, level surfaces" + ] + }, + "examples": [ + { + "name": "Basic Greeting Interaction", + "description": "Simple greeting sequence with speech and gesture", + "actions": [ + {"action": "nao_wake_rest", "parameters": {"action": "wake"}}, + {"action": "nao_speak", "parameters": {"text": "Hello! Welcome to our experiment."}}, + {"action": "nao_gesture", "parameters": {"gesture": "wave"}}, + {"action": "nao_pose", "parameters": {"posture": "Stand"}} + ] + }, + { + "name": "Attention and Pointing", + "description": "Direct attention using head movement and pointing", + "actions": [ + {"action": "nao_head_movement", "parameters": {"presetDirection": "left"}}, + {"action": "nao_speak", "parameters": {"text": "Please look over there."}}, + {"action": "nao_gesture", "parameters": {"gesture": "point_left"}}, + {"action": "nao_head_movement", "parameters": {"presetDirection": "center"}} + ] + }, + { + "name": "Interactive Sensor Monitoring", + "description": "Monitor for touch interactions and respond", + "actions": [ + {"action": "nao_speak", "parameters": {"text": "Touch my head when you're ready to continue."}}, + {"action": "nao_sensor_monitor", "parameters": {"sensorType": "touch", "triggerAction": "speak"}}, + {"action": "nao_speak", "parameters": {"text": "Thank you! Let's continue."}} + ] + } + ], + "createdAt": "2024-12-16T00:00:00Z", + "updatedAt": "2024-12-16T00:00:00Z" +} diff --git a/public/nao6-plugins/plugins/index.json b/public/nao6-plugins/plugins/index.json new file mode 100644 index 0000000..4b767d9 --- /dev/null +++ b/public/nao6-plugins/plugins/index.json @@ -0,0 +1,7 @@ +[ + "nao6-movement.json", + "nao6-speech.json", + "nao6-sensors.json", + "nao6-vision.json", + "nao6-interaction.json" +] diff --git a/public/nao6-plugins/plugins/nao6-movement.json b/public/nao6-plugins/plugins/nao6-movement.json new file mode 100644 index 0000000..9272da3 --- /dev/null +++ b/public/nao6-plugins/plugins/nao6-movement.json @@ -0,0 +1,342 @@ +{ + "name": "NAO6 Movement Control", + "version": "1.0.0", + "description": "Complete movement control for NAO6 robot including walking, turning, and joint manipulation", + "platform": "NAO6", + "category": "movement", + "manufacturer": { + "name": "SoftBank Robotics", + "website": "https://www.softbankrobotics.com" + }, + "documentation": { + "mainUrl": "https://docs.hristudio.com/robots/nao6/movement", + "quickStart": "https://docs.hristudio.com/robots/nao6/movement/quickstart" + }, + "ros2Config": { + "namespace": "/naoqi_driver", + "topics": { + "cmd_vel": { + "type": "geometry_msgs/Twist", + "description": "Velocity commands for robot base movement" + }, + "joint_angles": { + "type": "naoqi_bridge_msgs/JointAnglesWithSpeed", + "description": "Individual joint angle control with speed" + }, + "joint_states": { + "type": "sensor_msgs/JointState", + "description": "Current joint positions and velocities" + } + } + }, + "actions": [ + { + "id": "walk_forward", + "name": "Walk Forward", + "description": "Make the robot walk forward at specified speed", + "category": "movement", + "parameters": [ + { + "name": "speed", + "type": "number", + "description": "Walking speed in m/s", + "required": true, + "min": 0.01, + "max": 0.3, + "default": 0.1, + "step": 0.01 + }, + { + "name": "duration", + "type": "number", + "description": "Duration to walk in seconds (0 = indefinite)", + "required": false, + "min": 0, + "max": 30, + "default": 0, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/cmd_vel", + "messageType": "geometry_msgs/Twist", + "messageTemplate": { + "linear": { "x": "{{speed}}", "y": 0, "z": 0 }, + "angular": { "x": 0, "y": 0, "z": 0 } + } + } + }, + { + "id": "walk_backward", + "name": "Walk Backward", + "description": "Make the robot walk backward at specified speed", + "category": "movement", + "parameters": [ + { + "name": "speed", + "type": "number", + "description": "Walking speed in m/s", + "required": true, + "min": 0.01, + "max": 0.3, + "default": 0.1, + "step": 0.01 + }, + { + "name": "duration", + "type": "number", + "description": "Duration to walk in seconds (0 = indefinite)", + "required": false, + "min": 0, + "max": 30, + "default": 0, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/cmd_vel", + "messageType": "geometry_msgs/Twist", + "messageTemplate": { + "linear": { "x": "-{{speed}}", "y": 0, "z": 0 }, + "angular": { "x": 0, "y": 0, "z": 0 } + } + } + }, + { + "id": "turn_left", + "name": "Turn Left", + "description": "Make the robot turn left at specified angular speed", + "category": "movement", + "parameters": [ + { + "name": "speed", + "type": "number", + "description": "Angular speed in rad/s", + "required": true, + "min": 0.1, + "max": 1.0, + "default": 0.3, + "step": 0.1 + }, + { + "name": "duration", + "type": "number", + "description": "Duration to turn in seconds (0 = indefinite)", + "required": false, + "min": 0, + "max": 30, + "default": 0, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/cmd_vel", + "messageType": "geometry_msgs/Twist", + "messageTemplate": { + "linear": { "x": 0, "y": 0, "z": 0 }, + "angular": { "x": 0, "y": 0, "z": "{{speed}}" } + } + } + }, + { + "id": "turn_right", + "name": "Turn Right", + "description": "Make the robot turn right at specified angular speed", + "category": "movement", + "parameters": [ + { + "name": "speed", + "type": "number", + "description": "Angular speed in rad/s", + "required": true, + "min": 0.1, + "max": 1.0, + "default": 0.3, + "step": 0.1 + }, + { + "name": "duration", + "type": "number", + "description": "Duration to turn in seconds (0 = indefinite)", + "required": false, + "min": 0, + "max": 30, + "default": 0, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/cmd_vel", + "messageType": "geometry_msgs/Twist", + "messageTemplate": { + "linear": { "x": 0, "y": 0, "z": 0 }, + "angular": { "x": 0, "y": 0, "z": "-{{speed}}" } + } + } + }, + { + "id": "stop_movement", + "name": "Stop Movement", + "description": "Immediately stop all robot movement", + "category": "movement", + "parameters": [], + "implementation": { + "topic": "/naoqi_driver/cmd_vel", + "messageType": "geometry_msgs/Twist", + "messageTemplate": { + "linear": { "x": 0, "y": 0, "z": 0 }, + "angular": { "x": 0, "y": 0, "z": 0 } + } + } + }, + { + "id": "move_head", + "name": "Move Head", + "description": "Control head orientation (yaw and pitch)", + "category": "movement", + "parameters": [ + { + "name": "yaw", + "type": "number", + "description": "Head yaw angle in radians", + "required": true, + "min": -2.09, + "max": 2.09, + "default": 0, + "step": 0.1 + }, + { + "name": "pitch", + "type": "number", + "description": "Head pitch angle in radians", + "required": true, + "min": -0.67, + "max": 0.51, + "default": 0, + "step": 0.1 + }, + { + "name": "speed", + "type": "number", + "description": "Movement speed (0.1 = slow, 1.0 = fast)", + "required": false, + "min": 0.1, + "max": 1.0, + "default": 0.3, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/joint_angles", + "messageType": "naoqi_bridge_msgs/JointAnglesWithSpeed", + "messageTemplate": { + "joint_names": ["HeadYaw", "HeadPitch"], + "joint_angles": ["{{yaw}}", "{{pitch}}"], + "speed": "{{speed}}" + } + } + }, + { + "id": "move_arm", + "name": "Move Arm", + "description": "Control arm joint positions", + "category": "movement", + "parameters": [ + { + "name": "arm", + "type": "select", + "description": "Which arm to control", + "required": true, + "options": [ + { "value": "left", "label": "Left Arm" }, + { "value": "right", "label": "Right Arm" } + ], + "default": "right" + }, + { + "name": "shoulder_pitch", + "type": "number", + "description": "Shoulder pitch angle in radians", + "required": true, + "min": -2.09, + "max": 2.09, + "default": 1.4, + "step": 0.1 + }, + { + "name": "shoulder_roll", + "type": "number", + "description": "Shoulder roll angle in radians", + "required": true, + "min": -0.31, + "max": 1.33, + "default": 0.2, + "step": 0.1 + }, + { + "name": "elbow_yaw", + "type": "number", + "description": "Elbow yaw angle in radians", + "required": true, + "min": -2.09, + "max": 2.09, + "default": 0, + "step": 0.1 + }, + { + "name": "elbow_roll", + "type": "number", + "description": "Elbow roll angle in radians", + "required": true, + "min": -1.54, + "max": -0.03, + "default": -0.5, + "step": 0.1 + }, + { + "name": "speed", + "type": "number", + "description": "Movement speed (0.1 = slow, 1.0 = fast)", + "required": false, + "min": 0.1, + "max": 1.0, + "default": 0.3, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/joint_angles", + "messageType": "naoqi_bridge_msgs/JointAnglesWithSpeed", + "messageTemplate": { + "joint_names": [ + "{{arm === 'left' ? 'L' : 'R'}}ShoulderPitch", + "{{arm === 'left' ? 'L' : 'R'}}ShoulderRoll", + "{{arm === 'left' ? 'L' : 'R'}}ElbowYaw", + "{{arm === 'left' ? 'L' : 'R'}}ElbowRoll" + ], + "joint_angles": ["{{shoulder_pitch}}", "{{shoulder_roll}}", "{{elbow_yaw}}", "{{elbow_roll}}"], + "speed": "{{speed}}" + } + } + } + ], + "safety": { + "maxSpeed": 0.3, + "emergencyStop": { + "action": "stop_movement", + "description": "Immediately stops all movement" + }, + "jointLimits": { + "HeadYaw": { "min": -2.09, "max": 2.09 }, + "HeadPitch": { "min": -0.67, "max": 0.51 }, + "LShoulderPitch": { "min": -2.09, "max": 2.09 }, + "RShoulderPitch": { "min": -2.09, "max": 2.09 }, + "LShoulderRoll": { "min": -0.31, "max": 1.33 }, + "RShoulderRoll": { "min": -1.33, "max": 0.31 }, + "LElbowYaw": { "min": -2.09, "max": 2.09 }, + "RElbowYaw": { "min": -2.09, "max": 2.09 }, + "LElbowRoll": { "min": 0.03, "max": 1.54 }, + "RElbowRoll": { "min": -1.54, "max": -0.03 } + } + } +} diff --git a/public/nao6-plugins/plugins/nao6-sensors.json b/public/nao6-plugins/plugins/nao6-sensors.json new file mode 100644 index 0000000..b5ebd72 --- /dev/null +++ b/public/nao6-plugins/plugins/nao6-sensors.json @@ -0,0 +1,464 @@ +{ + "name": "NAO6 Sensors & Feedback", + "version": "1.0.0", + "description": "Complete sensor suite for NAO6 robot including touch sensors, sonar, IMU, cameras, and joint state monitoring", + "platform": "NAO6", + "category": "sensors", + "manufacturer": { + "name": "SoftBank Robotics", + "website": "https://www.softbankrobotics.com" + }, + "documentation": { + "mainUrl": "https://docs.hristudio.com/robots/nao6/sensors", + "quickStart": "https://docs.hristudio.com/robots/nao6/sensors/quickstart" + }, + "ros2Config": { + "namespace": "/naoqi_driver", + "topics": { + "joint_states": { + "type": "sensor_msgs/JointState", + "description": "Current positions, velocities, and efforts of all joints" + }, + "imu": { + "type": "sensor_msgs/Imu", + "description": "Inertial measurement unit data (acceleration, angular velocity, orientation)" + }, + "bumper": { + "type": "naoqi_bridge_msgs/Bumper", + "description": "Foot bumper sensor states" + }, + "hand_touch": { + "type": "naoqi_bridge_msgs/HandTouch", + "description": "Hand tactile sensor states" + }, + "head_touch": { + "type": "naoqi_bridge_msgs/HeadTouch", + "description": "Head tactile sensor states" + }, + "sonar/left": { + "type": "sensor_msgs/Range", + "description": "Left ultrasonic range sensor" + }, + "sonar/right": { + "type": "sensor_msgs/Range", + "description": "Right ultrasonic range sensor" + }, + "camera/front/image_raw": { + "type": "sensor_msgs/Image", + "description": "Front camera image feed" + }, + "camera/bottom/image_raw": { + "type": "sensor_msgs/Image", + "description": "Bottom camera image feed" + }, + "battery": { + "type": "sensor_msgs/BatteryState", + "description": "Battery level and charging status" + } + } + }, + "actions": [ + { + "id": "get_joint_states", + "name": "Get Joint States", + "description": "Read current positions and velocities of all robot joints", + "category": "sensors", + "parameters": [ + { + "name": "specific_joints", + "type": "multiselect", + "description": "Specific joints to monitor (empty = all joints)", + "required": false, + "options": [ + { "value": "HeadYaw", "label": "Head Yaw" }, + { "value": "HeadPitch", "label": "Head Pitch" }, + { "value": "LShoulderPitch", "label": "Left Shoulder Pitch" }, + { "value": "LShoulderRoll", "label": "Left Shoulder Roll" }, + { "value": "LElbowYaw", "label": "Left Elbow Yaw" }, + { "value": "LElbowRoll", "label": "Left Elbow Roll" }, + { "value": "RShoulderPitch", "label": "Right Shoulder Pitch" }, + { "value": "RShoulderRoll", "label": "Right Shoulder Roll" }, + { "value": "RElbowYaw", "label": "Right Elbow Yaw" }, + { "value": "RElbowRoll", "label": "Right Elbow Roll" } + ] + } + ], + "implementation": { + "topic": "/naoqi_driver/joint_states", + "messageType": "sensor_msgs/JointState", + "mode": "subscribe" + } + }, + { + "id": "get_touch_sensors", + "name": "Get Touch Sensors", + "description": "Monitor all tactile sensors on head and hands", + "category": "sensors", + "parameters": [ + { + "name": "sensor_type", + "type": "select", + "description": "Type of touch sensors to monitor", + "required": false, + "options": [ + { "value": "all", "label": "All Touch Sensors" }, + { "value": "head", "label": "Head Touch Only" }, + { "value": "hands", "label": "Hand Touch Only" } + ], + "default": "all" + } + ], + "implementation": { + "topics": [ + "/naoqi_driver/head_touch", + "/naoqi_driver/hand_touch" + ], + "messageTypes": [ + "naoqi_bridge_msgs/HeadTouch", + "naoqi_bridge_msgs/HandTouch" + ], + "mode": "subscribe" + } + }, + { + "id": "get_sonar_distance", + "name": "Get Sonar Distance", + "description": "Read ultrasonic distance sensors for obstacle detection", + "category": "sensors", + "parameters": [ + { + "name": "sensor_side", + "type": "select", + "description": "Which sonar sensor to read", + "required": false, + "options": [ + { "value": "both", "label": "Both Sensors" }, + { "value": "left", "label": "Left Sensor Only" }, + { "value": "right", "label": "Right Sensor Only" } + ], + "default": "both" + }, + { + "name": "min_range", + "type": "number", + "description": "Minimum detection range in meters", + "required": false, + "min": 0.1, + "max": 1.0, + "default": 0.25, + "step": 0.05 + }, + { + "name": "max_range", + "type": "number", + "description": "Maximum detection range in meters", + "required": false, + "min": 1.0, + "max": 3.0, + "default": 2.55, + "step": 0.05 + } + ], + "implementation": { + "topics": [ + "/naoqi_driver/sonar/left", + "/naoqi_driver/sonar/right" + ], + "messageType": "sensor_msgs/Range", + "mode": "subscribe" + } + }, + { + "id": "get_imu_data", + "name": "Get IMU Data", + "description": "Read inertial measurement unit data (acceleration, gyroscope, orientation)", + "category": "sensors", + "parameters": [ + { + "name": "data_type", + "type": "select", + "description": "Type of IMU data to monitor", + "required": false, + "options": [ + { "value": "all", "label": "All IMU Data" }, + { "value": "orientation", "label": "Orientation Only" }, + { "value": "acceleration", "label": "Linear Acceleration" }, + { "value": "angular_velocity", "label": "Angular Velocity" } + ], + "default": "all" + } + ], + "implementation": { + "topic": "/naoqi_driver/imu", + "messageType": "sensor_msgs/Imu", + "mode": "subscribe" + } + }, + { + "id": "get_camera_image", + "name": "Get Camera Image", + "description": "Capture image from robot's cameras", + "category": "sensors", + "parameters": [ + { + "name": "camera", + "type": "select", + "description": "Which camera to use", + "required": true, + "options": [ + { "value": "front", "label": "Front Camera" }, + { "value": "bottom", "label": "Bottom Camera" } + ], + "default": "front" + }, + { + "name": "resolution", + "type": "select", + "description": "Image resolution", + "required": false, + "options": [ + { "value": "160x120", "label": "QQVGA (160x120)" }, + { "value": "320x240", "label": "QVGA (320x240)" }, + { "value": "640x480", "label": "VGA (640x480)" } + ], + "default": "320x240" + }, + { + "name": "fps", + "type": "number", + "description": "Frames per second", + "required": false, + "min": 1, + "max": 30, + "default": 15, + "step": 1 + } + ], + "implementation": { + "topic": "/naoqi_driver/camera/{{camera}}/image_raw", + "messageType": "sensor_msgs/Image", + "mode": "subscribe" + } + }, + { + "id": "get_battery_status", + "name": "Get Battery Status", + "description": "Monitor robot battery level and charging status", + "category": "sensors", + "parameters": [], + "implementation": { + "topic": "/naoqi_driver/battery", + "messageType": "sensor_msgs/BatteryState", + "mode": "subscribe" + } + }, + { + "id": "detect_obstacle", + "name": "Detect Obstacle", + "description": "Check for obstacles using sonar sensors with customizable thresholds", + "category": "sensors", + "parameters": [ + { + "name": "detection_distance", + "type": "number", + "description": "Distance threshold for obstacle detection (meters)", + "required": true, + "min": 0.1, + "max": 2.0, + "default": 0.5, + "step": 0.1 + }, + { + "name": "sensor_side", + "type": "select", + "description": "Which sensors to use for detection", + "required": false, + "options": [ + { "value": "both", "label": "Both Sensors" }, + { "value": "left", "label": "Left Sensor Only" }, + { "value": "right", "label": "Right Sensor Only" } + ], + "default": "both" + } + ], + "implementation": { + "topics": [ + "/naoqi_driver/sonar/left", + "/naoqi_driver/sonar/right" + ], + "messageType": "sensor_msgs/Range", + "mode": "subscribe", + "processing": "obstacle_detection" + } + }, + { + "id": "monitor_fall_detection", + "name": "Monitor Fall Detection", + "description": "Monitor robot stability using IMU data to detect potential falls", + "category": "sensors", + "parameters": [ + { + "name": "tilt_threshold", + "type": "number", + "description": "Maximum tilt angle before fall alert (degrees)", + "required": false, + "min": 10, + "max": 45, + "default": 25, + "step": 5 + }, + { + "name": "acceleration_threshold", + "type": "number", + "description": "Acceleration threshold for impact detection (m/s²)", + "required": false, + "min": 5, + "max": 20, + "default": 10, + "step": 1 + } + ], + "implementation": { + "topic": "/naoqi_driver/imu", + "messageType": "sensor_msgs/Imu", + "mode": "subscribe", + "processing": "fall_detection" + } + }, + { + "id": "wait_for_touch", + "name": "Wait for Touch", + "description": "Wait for user to touch a specific sensor before continuing", + "category": "sensors", + "parameters": [ + { + "name": "sensor_location", + "type": "select", + "description": "Which sensor to wait for", + "required": true, + "options": [ + { "value": "head_front", "label": "Head Front" }, + { "value": "head_middle", "label": "Head Middle" }, + { "value": "head_rear", "label": "Head Rear" }, + { "value": "left_hand", "label": "Left Hand" }, + { "value": "right_hand", "label": "Right Hand" }, + { "value": "any_head", "label": "Any Head Sensor" }, + { "value": "any_hand", "label": "Any Hand Sensor" }, + { "value": "any_touch", "label": "Any Touch Sensor" } + ], + "default": "head_front" + }, + { + "name": "timeout", + "type": "number", + "description": "Maximum time to wait for touch (seconds, 0 = infinite)", + "required": false, + "min": 0, + "max": 300, + "default": 30, + "step": 5 + } + ], + "implementation": { + "topics": [ + "/naoqi_driver/head_touch", + "/naoqi_driver/hand_touch" + ], + "messageTypes": [ + "naoqi_bridge_msgs/HeadTouch", + "naoqi_bridge_msgs/HandTouch" + ], + "mode": "wait_for_condition", + "condition": "touch_detected" + } + } + ], + "sensorSpecifications": { + "touchSensors": { + "head": { + "locations": ["front", "middle", "rear"], + "sensitivity": "capacitive", + "responseTime": "< 50ms" + }, + "hands": { + "locations": ["left", "right"], + "sensitivity": "capacitive", + "responseTime": "< 50ms" + } + }, + "sonarSensors": { + "count": 2, + "locations": ["left", "right"], + "minRange": "0.25m", + "maxRange": "2.55m", + "fieldOfView": "60°", + "frequency": "40kHz" + }, + "cameras": { + "front": { + "resolution": "640x480", + "maxFps": 30, + "fieldOfView": "60.9° x 47.6°" + }, + "bottom": { + "resolution": "640x480", + "maxFps": 30, + "fieldOfView": "60.9° x 47.6°" + } + }, + "imu": { + "accelerometer": { + "range": "±2g", + "sensitivity": "high" + }, + "gyroscope": { + "range": "±500°/s", + "sensitivity": "high" + }, + "magnetometer": { + "available": false + } + }, + "joints": { + "count": 25, + "encoderResolution": "12-bit", + "positionAccuracy": "±0.1°" + } + }, + "dataTypes": { + "jointState": { + "position": "radians", + "velocity": "radians/second", + "effort": "arbitrary units" + }, + "imu": { + "orientation": "quaternion", + "angularVelocity": "radians/second", + "linearAcceleration": "m/s²" + }, + "range": { + "distance": "meters", + "minRange": "meters", + "maxRange": "meters" + }, + "image": { + "encoding": "rgb8", + "width": "pixels", + "height": "pixels" + } + }, + "safety": { + "fallDetection": { + "enabled": true, + "defaultThreshold": "25°" + }, + "obstacleDetection": { + "enabled": true, + "safeDistance": "0.3m" + }, + "batteryMonitoring": { + "lowBatteryWarning": "20%", + "criticalBatteryShutdown": "5%" + } + } +} diff --git a/public/nao6-plugins/plugins/nao6-speech.json b/public/nao6-plugins/plugins/nao6-speech.json new file mode 100644 index 0000000..18bbbe9 --- /dev/null +++ b/public/nao6-plugins/plugins/nao6-speech.json @@ -0,0 +1,338 @@ +{ + "name": "NAO6 Speech & Audio", + "version": "1.0.0", + "description": "Text-to-speech and audio capabilities for NAO6 robot including voice synthesis, volume control, and language settings", + "platform": "NAO6", + "category": "speech", + "manufacturer": { + "name": "SoftBank Robotics", + "website": "https://www.softbankrobotics.com" + }, + "documentation": { + "mainUrl": "https://docs.hristudio.com/robots/nao6/speech", + "quickStart": "https://docs.hristudio.com/robots/nao6/speech/quickstart" + }, + "ros2Config": { + "namespace": "/naoqi_driver", + "topics": { + "speech": { + "type": "std_msgs/String", + "description": "Text-to-speech commands" + }, + "set_language": { + "type": "std_msgs/String", + "description": "Set speech language" + }, + "audio_volume": { + "type": "std_msgs/Float32", + "description": "Control audio volume level" + } + } + }, + "actions": [ + { + "id": "say_text", + "name": "Say Text", + "description": "Make the robot speak the specified text using text-to-speech", + "category": "speech", + "parameters": [ + { + "name": "text", + "type": "text", + "description": "Text for the robot to speak", + "required": true, + "maxLength": 500, + "placeholder": "Enter text for NAO to say..." + }, + { + "name": "wait_for_completion", + "type": "boolean", + "description": "Wait for speech to finish before continuing", + "required": false, + "default": true + } + ], + "implementation": { + "topic": "/naoqi_driver/speech", + "messageType": "std_msgs/String", + "messageTemplate": { + "data": "{{text}}" + } + } + }, + { + "id": "say_with_emotion", + "name": "Say Text with Emotion", + "description": "Speak text with emotional expression using SSML-like markup", + "category": "speech", + "parameters": [ + { + "name": "text", + "type": "text", + "description": "Text for the robot to speak", + "required": true, + "maxLength": 500, + "placeholder": "Enter text for NAO to say..." + }, + { + "name": "emotion", + "type": "select", + "description": "Emotional tone for speech", + "required": false, + "options": [ + { "value": "neutral", "label": "Neutral" }, + { "value": "happy", "label": "Happy" }, + { "value": "sad", "label": "Sad" }, + { "value": "excited", "label": "Excited" }, + { "value": "calm", "label": "Calm" } + ], + "default": "neutral" + }, + { + "name": "speed", + "type": "number", + "description": "Speech speed multiplier", + "required": false, + "min": 0.5, + "max": 2.0, + "default": 1.0, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/speech", + "messageType": "std_msgs/String", + "messageTemplate": { + "data": "\\rspd={{speed}}\\\\rst={{emotion}}\\{{text}}" + } + } + }, + { + "id": "set_volume", + "name": "Set Volume", + "description": "Adjust the robot's audio volume level", + "category": "speech", + "parameters": [ + { + "name": "volume", + "type": "number", + "description": "Volume level (0.0 = silent, 1.0 = maximum)", + "required": true, + "min": 0.0, + "max": 1.0, + "default": 0.5, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/audio_volume", + "messageType": "std_msgs/Float32", + "messageTemplate": { + "data": "{{volume}}" + } + } + }, + { + "id": "set_language", + "name": "Set Language", + "description": "Change the robot's speech language", + "category": "speech", + "parameters": [ + { + "name": "language", + "type": "select", + "description": "Speech language", + "required": true, + "options": [ + { "value": "en-US", "label": "English (US)" }, + { "value": "en-GB", "label": "English (UK)" }, + { "value": "fr-FR", "label": "French" }, + { "value": "de-DE", "label": "German" }, + { "value": "es-ES", "label": "Spanish" }, + { "value": "it-IT", "label": "Italian" }, + { "value": "ja-JP", "label": "Japanese" }, + { "value": "ko-KR", "label": "Korean" }, + { "value": "zh-CN", "label": "Chinese (Simplified)" } + ], + "default": "en-US" + } + ], + "implementation": { + "topic": "/naoqi_driver/set_language", + "messageType": "std_msgs/String", + "messageTemplate": { + "data": "{{language}}" + } + } + }, + { + "id": "say_random_phrase", + "name": "Say Random Phrase", + "description": "Make the robot say a random phrase from predefined categories", + "category": "speech", + "parameters": [ + { + "name": "category", + "type": "select", + "description": "Category of phrases", + "required": true, + "options": [ + { "value": "greeting", "label": "Greetings" }, + { "value": "encouragement", "label": "Encouragement" }, + { "value": "question", "label": "Questions" }, + { "value": "farewell", "label": "Farewells" }, + { "value": "instruction", "label": "Instructions" } + ], + "default": "greeting" + } + ], + "implementation": { + "topic": "/naoqi_driver/speech", + "messageType": "std_msgs/String", + "messageTemplate": { + "data": "{{getRandomPhrase(category)}}" + } + }, + "phrases": { + "greeting": [ + "Hello! Nice to meet you!", + "Hi there! How are you today?", + "Welcome! I'm excited to work with you.", + "Good day! Ready to get started?", + "Greetings! What shall we do today?" + ], + "encouragement": [ + "Great job! Keep it up!", + "You're doing wonderfully!", + "Excellent work! I'm impressed.", + "That's fantastic! Well done!", + "Perfect! You've got this!" + ], + "question": [ + "How can I help you today?", + "What would you like to do next?", + "Is there anything you'd like to know?", + "Shall we try something different?", + "What are you thinking about?" + ], + "farewell": [ + "Goodbye! It was great working with you!", + "See you later! Take care!", + "Until next time! Have a wonderful day!", + "Farewell! Thanks for spending time with me!", + "Bye for now! Look forward to seeing you again!" + ], + "instruction": [ + "Please follow my movements.", + "Let's try this step by step.", + "Watch carefully and then repeat.", + "Take your time, there's no rush.", + "Remember to stay focused." + ] + } + }, + { + "id": "spell_word", + "name": "Spell Word", + "description": "Have the robot spell out a word letter by letter", + "category": "speech", + "parameters": [ + { + "name": "word", + "type": "text", + "description": "Word to spell out", + "required": true, + "maxLength": 50, + "placeholder": "Enter word to spell..." + }, + { + "name": "pause_duration", + "type": "number", + "description": "Pause between letters in seconds", + "required": false, + "min": 0.1, + "max": 2.0, + "default": 0.5, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/speech", + "messageType": "std_msgs/String", + "messageTemplate": { + "data": "{{word.split('').join('\\pau={{pause_duration * 1000}}\\\\pau=0\\')}}" + } + } + }, + { + "id": "count_numbers", + "name": "Count Numbers", + "description": "Have the robot count from one number to another", + "category": "speech", + "parameters": [ + { + "name": "start", + "type": "number", + "description": "Starting number", + "required": true, + "min": 0, + "max": 100, + "default": 1, + "step": 1 + }, + { + "name": "end", + "type": "number", + "description": "Ending number", + "required": true, + "min": 0, + "max": 100, + "default": 10, + "step": 1 + }, + { + "name": "pause_duration", + "type": "number", + "description": "Pause between numbers in seconds", + "required": false, + "min": 0.1, + "max": 2.0, + "default": 0.8, + "step": 0.1 + } + ], + "implementation": { + "topic": "/naoqi_driver/speech", + "messageType": "std_msgs/String", + "messageTemplate": { + "data": "{{Array.from({length: end - start + 1}, (_, i) => start + i).join('\\pau={{pause_duration * 1000}}\\\\pau=0\\')}}" + } + } + } + ], + "features": { + "languages": [ + "en-US", "en-GB", "fr-FR", "de-DE", "es-ES", + "it-IT", "ja-JP", "ko-KR", "zh-CN" + ], + "emotions": [ + "neutral", "happy", "sad", "excited", "calm" + ], + "voiceEffects": [ + "speed", "pitch", "volume", "emotion" + ], + "ssmlSupport": true, + "maxTextLength": 500 + }, + "safety": { + "maxVolume": 1.0, + "defaultVolume": 0.5, + "profanityFilter": true, + "maxSpeechDuration": 60, + "emergencyQuiet": { + "action": "set_volume", + "parameters": { "volume": 0 }, + "description": "Immediately mute robot audio" + } + } +} diff --git a/public/nao6-plugins/repository.json b/public/nao6-plugins/repository.json new file mode 100644 index 0000000..9a22e21 --- /dev/null +++ b/public/nao6-plugins/repository.json @@ -0,0 +1,44 @@ +{ + "name": "NAO6 ROS2 Integration Repository", + "description": "Official NAO6 robot plugins for ROS2-based Human-Robot Interaction experiments", + "version": "1.0.0", + "author": { + "name": "HRIStudio Team", + "email": "support@hristudio.com" + }, + "urls": { + "git": "https://github.com/hristudio/nao6-ros2-plugins", + "documentation": "https://docs.hristudio.com/robots/nao6", + "issues": "https://github.com/hristudio/nao6-ros2-plugins/issues" + }, + "trust": "official", + "license": "MIT", + "robots": [ + { + "name": "NAO6", + "manufacturer": "SoftBank Robotics", + "model": "NAO V6", + "communicationProtocol": "ros2" + } + ], + "categories": [ + "movement", + "speech", + "sensors", + "interaction", + "vision" + ], + "ros2": { + "distro": "humble", + "packages": [ + "naoqi_driver2", + "naoqi_bridge_msgs", + "rosbridge_suite" + ], + "bridge": { + "protocol": "websocket", + "defaultPort": 9090 + } + }, + "lastUpdated": "2025-01-16T00:00:00Z" +} diff --git a/public/simple-ws-test.html b/public/simple-ws-test.html old mode 100644 new mode 100755 diff --git a/public/test-websocket.html b/public/test-websocket.html old mode 100644 new mode 100755 diff --git a/public/ws-check.html b/public/ws-check.html old mode 100644 new mode 100755 diff --git a/robot-plugins b/robot-plugins index 334dc68..f3db314 160000 --- a/robot-plugins +++ b/robot-plugins @@ -1 +1 @@ -Subproject commit 334dc68a22b1265b44e5991b8939d3589718ecca +Subproject commit f3db314c8a1c2b3c1b77c6d1a8f67f3a52eed50e diff --git a/scripts/seed-dev.ts b/scripts/seed-dev.ts old mode 100644 new mode 100755 index d37ac16..d995a2a --- a/scripts/seed-dev.ts +++ b/scripts/seed-dev.ts @@ -1,1014 +1,340 @@ import bcrypt from "bcryptjs"; import { drizzle } from "drizzle-orm/postgres-js"; -import { eq, sql } from "drizzle-orm"; +import { sql } from "drizzle-orm"; import postgres from "postgres"; import * as schema from "../src/server/db/schema"; -import { readFile } from "node:fs/promises"; -import path from "node:path"; // Database connection const connectionString = process.env.DATABASE_URL!; const connection = postgres(connectionString); const db = drizzle(connection, { schema }); -// Repository sync helper -async function syncRepository( - repoId: string, - repoUrl: string, -): Promise { - try { - console.log(`🔄 Syncing repository: ${repoUrl}`); - - // Resolve source: use local public repo for core, remote URL otherwise - const isCore = repoUrl.includes("core.hristudio.com"); - const devUrl = repoUrl; - - // Fetch repository metadata (local filesystem for core) - const repoMetadata = isCore - ? (JSON.parse( - await readFile( - path.join( - process.cwd(), - "public", - "hristudio-core", - "repository.json", - ), - "utf8", - ), - ) as { - description?: string; - author?: { name?: string }; - urls?: { git?: string }; - trust?: string; - }) - : await (async () => { - const repoResponse = await fetch(`${devUrl}/repository.json`); - if (!repoResponse.ok) { - throw new Error( - `Failed to fetch repository metadata: ${repoResponse.status}`, - ); - } - return (await repoResponse.json()) as { - description?: string; - author?: { name?: string }; - urls?: { git?: string }; - trust?: string; - }; - })(); - - // For core repository, create a single plugin with all block groups - if (isCore) { - const indexData = JSON.parse( - await readFile( - path.join( - process.cwd(), - "public", - "hristudio-core", - "plugins", - "index.json", - ), - "utf8", - ), - ) as { - plugins?: Array<{ blockCount?: number }>; - }; - - // Create core system plugin - await db.insert(schema.plugins).values({ - robotId: null, - name: "HRIStudio Core System", - version: "1.0.0", - description: repoMetadata.description ?? "", - author: repoMetadata.author?.name ?? "Unknown", - repositoryUrl: repoMetadata.urls?.git ?? "", - trustLevel: - (repoMetadata.trust as "official" | "verified" | "community") ?? - "community", - status: "active", - actionDefinitions: [], - metadata: { - platform: "Core", - category: "system", - repositoryId: repoId, - blockGroups: indexData.plugins ?? [], - totalBlocks: - indexData.plugins?.reduce( - (sum: number, p: { blockCount?: number }) => - sum + (p.blockCount ?? 0), - 0, - ) ?? 0, - }, - }); - - console.log( - `✅ Synced core system with ${indexData.plugins?.length ?? 0} block groups`, - ); - return 1; - } - - // For robot repositories, sync individual plugins - const pluginIndexResponse = await fetch(`${devUrl}/plugins/index.json`); - if (!pluginIndexResponse.ok) { - throw new Error( - `Failed to fetch plugin index: ${pluginIndexResponse.status}`, - ); - } - const pluginFiles = (await pluginIndexResponse.json()) as string[]; - - let syncedCount = 0; - for (const pluginFile of pluginFiles) { - try { - const pluginResponse = await fetch(`${devUrl}/plugins/${pluginFile}`); - if (!pluginResponse.ok) { - console.warn( - `Failed to fetch ${pluginFile}: ${pluginResponse.status}`, - ); - continue; - } - const pluginData = (await pluginResponse.json()) as { - name?: string; - version?: string; - description?: string; - manufacturer?: { name?: string }; - documentation?: { mainUrl?: string }; - trustLevel?: string; - actions?: unknown[]; - platform?: string; - category?: string; - specs?: unknown; - ros2Config?: unknown; - }; - - await db.insert(schema.plugins).values({ - robotId: null, // Will be matched later if needed - name: pluginData.name ?? pluginFile.replace(".json", ""), - version: pluginData.version ?? "1.0.0", - description: pluginData.description ?? "", - author: - pluginData.manufacturer?.name ?? - repoMetadata.author?.name ?? - "Unknown", - repositoryUrl: - pluginData.documentation?.mainUrl ?? repoMetadata.urls?.git ?? "", - trustLevel: - (pluginData.trustLevel as "official" | "verified" | "community") ?? - (repoMetadata.trust as "official" | "verified" | "community") ?? - "community", - status: "active", - actionDefinitions: pluginData.actions ?? [], - metadata: { - platform: pluginData.platform, - category: pluginData.category, - repositoryId: repoId, - specs: pluginData.specs, - ros2Config: pluginData.ros2Config, - }, - }); - - console.log(`✅ Synced plugin: ${pluginData.name}`); - syncedCount++; - } catch (error) { - console.warn(`Failed to process ${pluginFile}:`, error); - } - } - - return syncedCount; - } catch (error) { - console.error(`Failed to sync repository ${repoUrl}:`, error); - return 0; - } -} +// --- NAO6 Plugin Definitions (Synced from seed-nao6-plugin.ts) --- +const NAO_PLUGIN_DEF = { + name: "NAO6 Robot (Enhanced ROS2 Integration)", + version: "2.0.0", + description: "Comprehensive NAO6 robot integration for HRIStudio experiments via ROS2.", + actions: [ + { id: "nao_speak", name: "Speak Text", category: "speech", parametersSchema: { type: "object", properties: { text: { type: "string" }, volume: { type: "number", default: 0.7 } }, required: ["text"] } }, + { id: "nao_gesture", name: "Perform Gesture", category: "interaction", parametersSchema: { type: "object", properties: { gesture: { type: "string", enum: ["wave", "bow", "point"] }, speed: { type: "number", default: 0.8 } } } }, + { id: "nao_look_at", name: "Look At", category: "movement", parametersSchema: { type: "object", properties: { target: { type: "string", enum: ["participant", "screen", "away"] }, duration: { type: "number", default: 2.0 } } } }, + { id: "nao_nod", name: "Nod Head", category: "interaction", parametersSchema: { type: "object", properties: { speed: { type: "number", default: 1.0 } } } }, + { id: "nao_shake_head", name: "Shake Head", category: "interaction", parametersSchema: { type: "object", properties: { speed: { type: "number", default: 1.0 } } } }, + { id: "nao_bow", name: "Bow", category: "interaction", parametersSchema: { type: "object", properties: {} } }, + { id: "nao_open_hand", name: "Present (Open Hand)", category: "interaction", parametersSchema: { type: "object", properties: { hand: { type: "string", enum: ["left", "right", "both"], default: "right" } } } } + ] +}; async function main() { - console.log("🌱 Starting simplified seed script..."); + console.log("🌱 Starting realistic seed script..."); try { - // Clean existing data (in reverse order of dependencies) + // 1. Clean existing data (Full Wipe) console.log("🧹 Cleaning existing data..."); - await db.delete(schema.studyPlugins).where(sql`1=1`); - await db.delete(schema.plugins).where(sql`1=1`); - await db.delete(schema.pluginRepositories).where(sql`1=1`); + await db.delete(schema.mediaCaptures).where(sql`1=1`); await db.delete(schema.trialEvents).where(sql`1=1`); await db.delete(schema.trials).where(sql`1=1`); + await db.delete(schema.actions).where(sql`1=1`); await db.delete(schema.steps).where(sql`1=1`); await db.delete(schema.experiments).where(sql`1=1`); await db.delete(schema.participants).where(sql`1=1`); + await db.delete(schema.studyPlugins).where(sql`1=1`); await db.delete(schema.studyMembers).where(sql`1=1`); - await db.delete(schema.userSystemRoles).where(sql`1=1`); await db.delete(schema.studies).where(sql`1=1`); + await db.delete(schema.plugins).where(sql`1=1`); + await db.delete(schema.pluginRepositories).where(sql`1=1`); + await db.delete(schema.userSystemRoles).where(sql`1=1`); await db.delete(schema.users).where(sql`1=1`); await db.delete(schema.robots).where(sql`1=1`); - // Create robots - console.log("🤖 Creating robots..."); - const robots = [ - { - name: "TurtleBot3 Burger", - manufacturer: "ROBOTIS", - model: "TurtleBot3 Burger", - description: - "A compact, affordable, programmable, ROS2-based mobile robot for education and research", - capabilities: ["differential_drive", "lidar", "imu", "odometry"], - communicationProtocol: "ros2" as const, - }, - { - name: "NAO Humanoid Robot", - manufacturer: "SoftBank Robotics", - model: "NAO V6", - description: - "Humanoid robot designed for education, research, and social interaction", - capabilities: ["speech", "vision", "walking", "gestures"], - communicationProtocol: "rest" as const, - }, - ]; - - const insertedRobots = await db - .insert(schema.robots) - .values(robots) - .returning(); - console.log(`✅ Created ${insertedRobots.length} robots`); - - // Create users (Bucknell University team) + // 2. Create Users console.log("👥 Creating users..."); const hashedPassword = await bcrypt.hash("password123", 12); - const users = [ - { - name: "Sean O'Connor", - email: "sean@soconnor.dev", - password: hashedPassword, - emailVerified: new Date(), - image: null, - }, - { - name: "L. Felipe Perrone", - email: "felipe.perrone@bucknell.edu", - password: hashedPassword, - emailVerified: new Date(), - image: null, - }, - ]; + const [adminUser] = await db.insert(schema.users).values({ + name: "Sean O'Connor", + email: "sean@soconnor.dev", + password: hashedPassword, + emailVerified: new Date(), + image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Sean", + }).returning(); - const insertedUsers = await db - .insert(schema.users) - .values(users) - .returning(); - console.log(`✅ Created ${insertedUsers.length} users`); + const [researcherUser] = await db.insert(schema.users).values({ + name: "Dr. Felipe Perrone", + email: "felipe.perrone@bucknell.edu", + password: hashedPassword, + emailVerified: new Date(), + image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felipe", + }).returning(); - // Assign system roles - console.log("🎭 Assigning system roles..."); - const seanUser = insertedUsers.find((u) => u.email === "sean@soconnor.dev"); + if (!adminUser) throw new Error("Failed to create admin user"); - if (!seanUser) { - throw new Error("Sean user not found after creation"); - } + await db.insert(schema.userSystemRoles).values({ userId: adminUser.id, role: "administrator" }); - await db.insert(schema.userSystemRoles).values({ - userId: seanUser.id, - role: "administrator", + // 3. Create Robots & Plugins + console.log("🤖 Creating robots and plugins..."); + const [naoRobot] = await db.insert(schema.robots).values({ + name: "NAO6", + manufacturer: "SoftBank Robotics", + model: "NAO V6", + description: "Humanoid robot for social interaction studies.", + capabilities: ["speech", "vision", "bipedal_walking", "gestures"], + communicationProtocol: "ros2", + }).returning(); + + const [naoRepo] = await db.insert(schema.pluginRepositories).values({ + name: "HRIStudio Official Plugins", + url: "https://github.com/hristudio/plugins", + description: "Official verified plugins", + trustLevel: "official", + isEnabled: true, + isOfficial: true, + createdBy: adminUser.id, + }).returning(); + + const [naoPlugin] = await db.insert(schema.plugins).values({ + robotId: naoRobot!.id, + name: NAO_PLUGIN_DEF.name, + version: NAO_PLUGIN_DEF.version, + description: NAO_PLUGIN_DEF.description, + author: "HRIStudio Team", + trustLevel: "official", + status: "active", + repositoryUrl: naoRepo!.url, + actionDefinitions: NAO_PLUGIN_DEF.actions, + configurationSchema: { type: "object", properties: { robotIp: { type: "string", default: "192.168.1.100" } } }, + metadata: { category: "robot_control" } + }).returning(); + + // 4. Create Study & Experiment - Comparative WoZ Study + console.log("📚 Creating 'Comparative WoZ Study'..."); + const [study] = await db.insert(schema.studies).values({ + name: "Comparative WoZ Study", + description: "Comparison of HRIStudio vs Choregraphe for The Interactive Storyteller scenario.", + institution: "Bucknell University", + irbProtocol: "2024-HRI-COMP", + status: "active", + createdBy: adminUser.id, + }).returning(); + + await db.insert(schema.studyMembers).values([ + { studyId: study!.id, userId: adminUser.id, role: "owner" }, + { studyId: study!.id, userId: researcherUser!.id, role: "researcher" } + ]); + + await db.insert(schema.studyPlugins).values({ + studyId: study!.id, + pluginId: naoPlugin!.id, + configuration: { robotIp: "10.0.0.42" }, + installedBy: adminUser.id }); - console.log(`✅ Assigned administrator role to Sean`); + const [experiment] = await db.insert(schema.experiments).values({ + studyId: study!.id, + name: "The Interactive Storyteller", + description: "A storytelling scenario where the robot tells a story and asks questions to the participant.", + version: 1, + status: "ready", + robotId: naoRobot!.id, + createdBy: adminUser.id, + }).returning(); - // Create plugin repositories - console.log("📦 Creating plugin repositories..."); - const repositories = [ + // 5. Create Steps & Actions (The Interactive Storyteller Protocol) + console.log("🎬 Creating experiment steps (Interactive Storyteller)..."); + + // --- Step 1: The Hook --- + const [step1] = await db.insert(schema.steps).values({ + experimentId: experiment!.id, + name: "The Hook", + description: "Initial greeting and engagement", + type: "robot", + orderIndex: 0, + required: true, + durationEstimate: 30 + }).returning(); + + await db.insert(schema.actions).values([ { - name: "HRIStudio Core System Blocks", - url: "https://core.hristudio.com", - description: - "Essential system blocks for experiment design including events, control flow, wizard actions, and logic operations", - trustLevel: "official" as const, - isEnabled: true, - isOfficial: true, - syncStatus: "pending" as const, - createdBy: seanUser.id, - }, - { - name: "HRIStudio Official Robot Plugins", - url: "https://repo.hristudio.com", - description: - "Official collection of robot plugins maintained by the HRIStudio team", - trustLevel: "official" as const, - isEnabled: true, - isOfficial: true, - syncStatus: "pending" as const, - createdBy: seanUser.id, - }, - ]; - - const insertedRepos = await db - .insert(schema.pluginRepositories) - .values(repositories) - .returning(); - console.log(`✅ Created ${insertedRepos.length} plugin repositories`); - - // Sync repositories to populate plugins - console.log("🔄 Syncing plugin repositories..."); - let totalPlugins = 0; - - for (const repo of insertedRepos) { - const syncedCount = await syncRepository(repo.id, repo.url); - totalPlugins += syncedCount; - - // Update sync status - await db - .update(schema.pluginRepositories) - .set({ - syncStatus: syncedCount > 0 ? "completed" : "failed", - lastSyncAt: new Date(), - }) - .where(eq(schema.pluginRepositories.id, repo.id)); - } - - // Create studies - console.log("📚 Creating studies..."); - const studies = [ - { - name: "NAO Classroom Interaction", - description: - "Evaluating student engagement with NAO-led prompts during lab sessions", - institution: "Bucknell University", - irbProtocol: "BU-IRB-2025-NAO-01", - status: "active" as const, - createdBy: seanUser.id, - }, - { - name: "Wizard-of-Oz Dialogue Study", - description: - "WoZ-controlled NAO to assess timing and tone in instructional feedback", - institution: "Bucknell University", - irbProtocol: "BU-IRB-2025-WOZ-02", - status: "draft" as const, - createdBy: seanUser.id, - }, - ]; - - const insertedStudies = await db - .insert(schema.studies) - .values(studies) - .returning(); - console.log(`✅ Created ${insertedStudies.length} studies`); - - // Create study memberships - console.log("👥 Creating study memberships..."); - const studyMemberships = []; - - // Sean as owner of all studies - for (const study of insertedStudies) { - studyMemberships.push({ - studyId: study.id, - userId: seanUser.id, - role: "owner" as const, - }); - } - - // Add other users as researchers/wizards - const otherUsers = insertedUsers.filter((u) => u.id !== seanUser.id); - if (otherUsers.length > 0 && insertedStudies[0]) { - studyMemberships.push({ - studyId: insertedStudies[0].id, - userId: otherUsers[0]!.id, - role: "researcher" as const, - }); - - if (otherUsers.length > 1 && insertedStudies[1]) { - studyMemberships.push({ - studyId: insertedStudies[1].id, - userId: otherUsers[1]!.id, - role: "wizard" as const, - }); - } - } - - await db.insert(schema.studyMembers).values(studyMemberships); - console.log(`✅ Created ${studyMemberships.length} study memberships`); - - // Install core plugin in all studies - console.log("🔌 Installing core plugin in all studies..."); - const corePlugin = await db - .select() - .from(schema.plugins) - .where(eq(schema.plugins.name, "HRIStudio Core System")) - .limit(1); - - if (corePlugin.length > 0) { - const coreInstallations = insertedStudies.map((study) => ({ - studyId: study.id, - pluginId: corePlugin[0]!.id, - configuration: {}, - installedBy: seanUser.id, - })); - - await db.insert(schema.studyPlugins).values(coreInstallations); - console.log( - `✅ Installed core plugin in ${insertedStudies.length} studies`, - ); - } - - // Install NAO plugin for first study if available - console.log("🤝 Installing NAO plugin (if available)..."); - const naoPlugin = await db - .select() - .from(schema.plugins) - .where(eq(schema.plugins.name, "NAO Humanoid Robot")) - .limit(1); - if (naoPlugin.length > 0 && insertedStudies[0]) { - await db.insert(schema.studyPlugins).values({ - studyId: insertedStudies[0].id, - pluginId: naoPlugin[0]!.id, - configuration: { voice: "nao-tts", locale: "en-US" }, - installedBy: seanUser.id, - }); - console.log("✅ Installed NAO plugin in first study"); - } else { - console.log( - "ℹ️ NAO plugin not found in repository sync; continuing without it", - ); - } - - // Create some participants - console.log("👤 Creating participants..."); - const participants = []; - - for (let i = 0; i < insertedStudies.length; i++) { - const study = insertedStudies[i]; - if (study) { - participants.push( - { - studyId: study.id, - participantCode: `P${String(i * 2 + 1).padStart(3, "0")}`, - name: `Participant ${i * 2 + 1}`, - email: `participant${i * 2 + 1}@example.com`, - demographics: { age: 25 + i, gender: "prefer not to say" }, - consentGiven: true, - consentGivenAt: new Date(), - }, - { - studyId: study.id, - participantCode: `P${String(i * 2 + 2).padStart(3, "0")}`, - name: `Participant ${i * 2 + 2}`, - email: `participant${i * 2 + 2}@example.com`, - demographics: { age: 30 + i, gender: "prefer not to say" }, - consentGiven: true, - consentGivenAt: new Date(), - }, - ); - } - } - - const insertedParticipants = await db - .insert(schema.participants) - .values(participants) - .returning(); - console.log(`✅ Created ${insertedParticipants.length} participants`); - - // Create experiments (include one NAO-based) - console.log("🧪 Creating experiments..."); - const experiments = [ - { - studyId: insertedStudies[0]!.id, - name: "Basic Interaction Protocol 1", - description: "Wizard prompts + NAO speaks demo script", - version: 1, - status: "ready" as const, - estimatedDuration: 25, - createdBy: seanUser.id, - }, - { - studyId: insertedStudies[1]!.id, - name: "Dialogue Timing Pilot", - description: "Compare response timing variants under WoZ control", - version: 1, - status: "draft" as const, - estimatedDuration: 35, - createdBy: seanUser.id, - }, - ]; - - const insertedExperiments = await db - .insert(schema.experiments) - .values( - experiments.map((e) => ({ - ...e, - visualDesign: { - // minimal starter design; steps optionally overwritten below for DB tables - steps: [], - version: 1, - lastSaved: new Date().toISOString(), - }, - })), - ) - .returning(); - console.log(`✅ Created ${insertedExperiments.length} experiments`); - - // Seed a richer, multi-step design for the first experiment (wizard + robot) - if (insertedExperiments[0]) { - const exp = insertedExperiments[0]; - - // Step 1: Wizard demo + robot speaks - const step1 = await db - .insert(schema.steps) - .values({ - experimentId: exp.id, - name: "Step 1 • Introduction & Object Demo", - description: "Wizard greets participant and demonstrates an object", - type: "wizard", - orderIndex: 0, - required: true, - conditions: {}, - }) - .returning(); - const step1Id = step1[0]!.id; - - // Action 1.1: Wizard shows object - await db.insert(schema.actions).values({ - stepId: step1Id, - name: "show object", - description: "Wizard presents or demonstrates an object", - type: "wizard_show_object", + stepId: step1!.id, + name: "Greet Participant", + type: "nao6.nao_speak", orderIndex: 0, - parameters: { object: "Cube" }, - sourceKind: "core", - category: "wizard", - transport: "internal", - retryable: false, - }); - - // Resolve NAO plugin id/version for namespaced action type - const naoDbPlugin1 = await db - .select({ id: schema.plugins.id, version: schema.plugins.version }) - .from(schema.plugins) - .where(eq(schema.plugins.name, "NAO Humanoid Robot")) - .limit(1); - const naoPluginRow1 = naoDbPlugin1[0]; - - // Action 1.2: Robot/NAO says text (or wizard says fallback) - await db.insert(schema.actions).values({ - stepId: step1Id, - name: naoPluginRow1 ? "NAO Say Text" : "Wizard Say", - description: naoPluginRow1 - ? "Make the robot speak using text-to-speech" - : "Wizard speaks to participant", - type: naoPluginRow1 ? `${naoPluginRow1.id}.say_text` : "wizard_say", + parameters: { text: "Hello there! I have a wonderful story to share with you today.", volume: 0.8 }, + pluginId: naoPlugin!.id, + category: "speech", + retryable: true + }, + { + stepId: step1!.id, + name: "Wave Greeting", + type: "nao6.nao_gesture", orderIndex: 1, - parameters: naoPluginRow1 - ? { text: "Hello, I am NAO. Let's begin!", speed: 110, volume: 0.75 } - : { message: "Hello! Let's begin the session.", tone: "friendly" }, - sourceKind: naoPluginRow1 ? "plugin" : "core", - pluginId: naoPluginRow1 ? naoPluginRow1.id : null, - pluginVersion: naoPluginRow1 ? naoPluginRow1.version : null, - category: naoPluginRow1 ? "robot" : "wizard", - transport: naoPluginRow1 ? "rest" : "internal", - retryable: false, - }); - - // Step 2: Wait for response (wizard) - const step2 = await db - .insert(schema.steps) - .values({ - experimentId: exp.id, - name: "Step 2 • Participant Response", - description: "Wizard waits for the participant's response", - type: "wizard", - orderIndex: 1, - required: true, - conditions: {}, - }) - .returning(); - const step2Id = step2[0]!.id; - - await db.insert(schema.actions).values({ - stepId: step2Id, - name: "wait for response", - description: "Wizard waits for participant to respond", - type: "wizard_wait_for_response", - orderIndex: 0, - parameters: { - response_type: "verbal", - timeout: 20, - prompt_text: "What did you notice about the object?", - }, - sourceKind: "core", - category: "wizard", - transport: "internal", - retryable: false, - }); - - // Step 3: Robot LED feedback (or record note fallback) - const step3 = await db - .insert(schema.steps) - .values({ - experimentId: exp.id, - name: "Step 3 • Robot Feedback", - description: "Provide feedback using robot LED color or record note", - type: "robot", - orderIndex: 2, - required: false, - conditions: {}, - }) - .returning(); - const step3Id = step3[0]!.id; - - const naoDbPlugin2 = await db - .select({ id: schema.plugins.id, version: schema.plugins.version }) - .from(schema.plugins) - .where(eq(schema.plugins.name, "NAO Humanoid Robot")) - .limit(1); - const naoPluginRow2 = naoDbPlugin2[0]; - - if (naoPluginRow2) { - await db.insert(schema.actions).values({ - stepId: step3Id, - name: "Set LED Color", - description: "Change NAO's eye LEDs to reflect state", - type: `${naoPluginRow2.id}.set_led_color`, - orderIndex: 0, - parameters: { color: "blue", intensity: 0.6 }, - sourceKind: "plugin", - pluginId: naoPluginRow2.id, - pluginVersion: naoPluginRow2.version, - category: "robot", - transport: "rest", - retryable: false, - }); - } else { - await db.insert(schema.actions).values({ - stepId: step3Id, - name: "record note", - description: "Wizard records an observation", - type: "wizard_record_note", - orderIndex: 0, - parameters: { - note_type: "observation", - prompt: "No robot available", - }, - sourceKind: "core", - category: "wizard", - transport: "internal", - retryable: false, - }); + parameters: { gesture: "wave" }, + pluginId: naoPlugin!.id, + category: "interaction", + retryable: true } - } + ]); - // Seed a richer design for the second experiment (timers + conditional/parallel) - if (insertedExperiments[1]) { - const exp2 = insertedExperiments[1]; + // --- Step 2: The Narrative (Part 1) --- + const [step2] = await db.insert(schema.steps).values({ + experimentId: experiment!.id, + name: "The Narrative - Part 1", + description: "Robot tells the first part of the story", + type: "robot", + orderIndex: 1, + required: true, + durationEstimate: 60 + }).returning(); - // Step A: Baseline prompt - const stepA = await db - .insert(schema.steps) - .values({ - experimentId: exp2.id, - name: "Step A • Baseline Prompt", - description: "Wizard provides a baseline instruction", - type: "wizard", - orderIndex: 0, - required: true, - conditions: {}, - }) - .returning(); - const stepAId = stepA[0]!.id; - - await db.insert(schema.actions).values({ - stepId: stepAId, - name: "say", - description: "Wizard speaks to participant", - type: "wizard_say", + await db.insert(schema.actions).values([ + { + stepId: step2!.id, + name: "Tell Story Part 1", + type: "nao6.nao_speak", orderIndex: 0, - parameters: { - message: "We'll try a short timing task next.", - tone: "instructional", - }, - sourceKind: "core", - category: "wizard", - transport: "internal", - retryable: false, - }); - - // Step B: Parallel gestures/animation - const stepB = await db - .insert(schema.steps) - .values({ - experimentId: exp2.id, - name: "Step B • Parallel Cues", - description: "Provide multiple cues at once (gesture + animation)", - type: "parallel", - orderIndex: 1, - required: false, - conditions: {}, - }) - .returning(); - const stepBId = stepB[0]!.id; - - await db.insert(schema.actions).values({ - stepId: stepBId, - name: "gesture", - description: "Wizard performs a physical gesture", - type: "wizard_gesture", - orderIndex: 0, - parameters: { type: "point", direction: "participant" }, - sourceKind: "core", - category: "wizard", - transport: "internal", - retryable: false, - }); - - const naoDbPluginB = await db - .select({ id: schema.plugins.id, version: schema.plugins.version }) - .from(schema.plugins) - .where(eq(schema.plugins.name, "NAO Humanoid Robot")) - .limit(1); - const naoPluginRowB = naoDbPluginB[0]; - - if (naoPluginRowB) { - await db.insert(schema.actions).values({ - stepId: stepBId, - name: "Play Animation", - description: "NAO plays a greeting animation", - type: `${naoPluginRowB.id}.play_animation`, - orderIndex: 1, - parameters: { animation: "Hello" }, - sourceKind: "plugin", - pluginId: naoPluginRowB.id, - pluginVersion: naoPluginRowB.version, - category: "robot", - transport: "rest", - retryable: false, - }); + parameters: { text: "Once upon a time, in a land far away, there lived a curious robot named Alpha.", volume: 0.8 }, + pluginId: naoPlugin!.id, + category: "speech" + }, + { + stepId: step2!.id, + name: "Present Gesture", + type: "nao6.nao_open_hand", + orderIndex: 1, + parameters: { hand: "right" }, + pluginId: naoPlugin!.id, + category: "interaction" } + ]); - // Step C: Conditional follow-up after a brief wait - const stepC = await db - .insert(schema.steps) - .values({ - experimentId: exp2.id, - name: "Step C • Conditional Follow-up", - description: "Proceed based on observed response after timer", - type: "conditional", - orderIndex: 2, - required: false, - conditions: { predicate: "response_received", timer_ms: 3000 }, - }) - .returning(); - const stepCId = stepC[0]!.id; + // --- Step 3: Comprehension Check (Wizard Decision) --- + // Note: In a real visual designer, this would be a Branch/Conditional. + // Here we model it as a Wizard Step where they explicitly choose the next robot action. + const [step3] = await db.insert(schema.steps).values({ + experimentId: experiment!.id, + name: "Comprehension Check", + description: "Wizard verifies participant understanding", + type: "wizard", + orderIndex: 2, + required: true, + durationEstimate: 45 + }).returning(); - await db.insert(schema.actions).values({ - stepId: stepCId, - name: "record note", - description: "Wizard records a follow-up note", - type: "wizard_record_note", + await db.insert(schema.actions).values([ + { + stepId: step3!.id, + name: "Ask Question", + type: "nao6.nao_speak", orderIndex: 0, - parameters: { - note_type: "participant_response", - prompt: "Response after parallel cues", - }, + parameters: { text: "What was the robot's name?", volume: 0.8 }, + pluginId: naoPlugin!.id, + category: "speech" + }, + { + stepId: step3!.id, + name: "Wait for Wizard Input", + type: "core.wait_for_response", + orderIndex: 1, + parameters: { prompt: "Did participant answer 'Alpha'?", options: ["Yes", "No"] }, sourceKind: "core", - category: "wizard", - transport: "internal", - retryable: false, - }); - } - - // Create some trials for dashboard demo - console.log("🧪 Creating sample trials..."); - const trials = []; - - for (const experiment of insertedExperiments) { - if (!experiment) continue; - - const studyParticipants = insertedParticipants.filter( - (p) => p.studyId === experiment.studyId, - ); - - if (studyParticipants.length > 0) { - // Create 2-3 trials per experiment - const trialCount = Math.min(studyParticipants.length, 3); - for (let j = 0; j < trialCount; j++) { - const participant = studyParticipants[j]; - if (participant) { - const scheduledAt = new Date( - Date.now() - Math.random() * 2 * 24 * 60 * 60 * 1000, - ); - const startedAt = new Date(scheduledAt.getTime() + 30 * 60 * 1000); // 30 minutes after scheduled - const completedAt = new Date(startedAt.getTime() + 45 * 60 * 1000); // 45 minutes after started - - // Vary the status: some completed, some in progress, some scheduled - let status: "scheduled" | "in_progress" | "completed" | "aborted"; - let actualStartedAt = null; - let actualCompletedAt = null; - - if (j === 0) { - status = "completed"; - actualStartedAt = startedAt; - actualCompletedAt = completedAt; - } else if (j === 1 && trialCount > 2) { - status = "in_progress"; - actualStartedAt = startedAt; - } else { - status = "scheduled"; - } - - trials.push({ - participantId: participant.id, - experimentId: experiment.id, - sessionNumber: j + 1, - status, - scheduledAt, - startedAt: actualStartedAt, - completedAt: actualCompletedAt, - notes: `Trial session ${j + 1} for ${experiment.name}`, - createdBy: seanUser.id, - }); - } - } + category: "wizard" } - } + ]); - const insertedTrials = await db - .insert(schema.trials) - .values(trials) - .returning(); - console.log(`✅ Created ${insertedTrials.length} trials`); + // --- Step 4: Feedback (Positive/Negative branches implied) --- + // For linear seed, we just add the Positive feedback step + const [step4] = await db.insert(schema.steps).values({ + experimentId: experiment!.id, + name: "Positive Feedback", + description: "Correct answer response", + type: "robot", + orderIndex: 3, + required: true, + durationEstimate: 15 + }).returning(); - // Create trial events time series for richer dashboards - const trialEventRows = []; - for (const t of insertedTrials) { - const baseStart = t.startedAt ?? new Date(Date.now() - 60 * 60 * 1000); - const t1 = new Date(baseStart.getTime() - 2 * 60 * 1000); // 2 min before start - const t2 = new Date(baseStart.getTime()); // start - const t3 = new Date(baseStart.getTime() + 3 * 60 * 1000); // +3 min - const t4 = new Date(baseStart.getTime() + 8 * 60 * 1000); // +8 min - const t5 = - t.completedAt ?? new Date(baseStart.getTime() + 15 * 60 * 1000); // completion + await db.insert(schema.actions).values([ + { + stepId: step4!.id, + name: "Nod Affirmation", + type: "nao6.nao_nod", + orderIndex: 0, + parameters: { speed: 1.0 }, + pluginId: naoPlugin!.id, + category: "interaction" + }, + { + stepId: step4!.id, + name: "Say Correct", + type: "nao6.nao_speak", + orderIndex: 1, + parameters: { text: "That is correct! Well done.", volume: 0.8 }, + pluginId: naoPlugin!.id, + category: "speech" + } + ]); - trialEventRows.push( - { - trialId: t.id, - eventType: "wizard_prompt_shown", - actionId: null, - timestamp: t1, - data: { prompt: "Welcome and object demo" }, - createdBy: seanUser.id, - }, - { - trialId: t.id, - eventType: "action_started", - actionId: null, - timestamp: t2, - data: { label: "demo_start" }, - createdBy: seanUser.id, - }, - { - trialId: t.id, - eventType: "robot_action_executed", - actionId: null, - timestamp: t3, - data: { robot: "nao", action: "speak" }, - createdBy: seanUser.id, - }, - { - trialId: t.id, - eventType: "action_completed", - actionId: null, - timestamp: t4, - data: { label: "demo_complete" }, - createdBy: seanUser.id, - }, - { - trialId: t.id, - eventType: "trial_note", - actionId: null, - timestamp: t5, - data: { summary: "Session ended successfully" }, - createdBy: seanUser.id, - }, - ); - } - if (trialEventRows.length) { - await db.insert(schema.trialEvents).values(trialEventRows); - console.log(`✅ Created ${trialEventRows.length} trial events`); - } + // --- Step 5: Conclusion --- + const [step5] = await db.insert(schema.steps).values({ + experimentId: experiment!.id, + name: "Conclusion", + description: "Wrap up the story", + type: "robot", + orderIndex: 4, + required: true, + durationEstimate: 30 + }).returning(); - // Create some activity logs for dashboard demo - console.log("📝 Creating activity logs..."); - const activityEntries = []; + await db.insert(schema.actions).values([ + { + stepId: step5!.id, + name: "Finish Story", + type: "nao6.nao_speak", + orderIndex: 0, + parameters: { text: "Alpha explored the world and learned many things. The end.", volume: 0.8 }, + pluginId: naoPlugin!.id, + category: "speech" + }, + { + stepId: step5!.id, + name: "Bow Goodbye", + type: "nao6.nao_bow", + orderIndex: 1, + parameters: {}, + pluginId: naoPlugin!.id, + category: "interaction" + } + ]); - // Study creation activities - for (const study of insertedStudies) { - activityEntries.push({ - studyId: study.id, - userId: seanUser.id, - action: "study_created", - description: `Created study "${study.name}"`, - createdAt: new Date( - Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000, - ), // Random time in last week + // 6. Participants (N=20 for study) + console.log("👤 Creating 20 participants for N=20 study..."); + const participants = []; + for (let i = 1; i <= 20; i++) { + participants.push({ + studyId: study!.id, + participantCode: `P${100 + i}`, + name: `Participant ${100 + i}`, + consentGiven: true, + consentGivenAt: new Date(), + notes: i % 2 === 0 ? "Condition: HRIStudio" : "Condition: Choregraphe" }); } + const insertedParticipants = await db.insert(schema.participants).values(participants).returning(); - // Experiment creation activities - for (const experiment of insertedExperiments) { - activityEntries.push({ - studyId: experiment.studyId, - userId: seanUser.id, - action: "experiment_created", - description: `Created experiment protocol "${experiment.name}"`, - createdAt: new Date( - Date.now() - Math.random() * 5 * 24 * 60 * 60 * 1000, - ), // Random time in last 5 days - }); - } + console.log("\n✅ Database seeded successfully!"); + console.log(`Summary:`); + console.log(`- 1 Admin User (sean@soconnor.dev)`); + console.log(`- Study: 'Comparative WoZ Study'`); + console.log(`- Experiment: 'The Interactive Storyteller' (${5} steps created)`); + console.log(`- ${insertedParticipants.length} Participants`); - // Participant enrollment activities - for (const participant of insertedParticipants) { - activityEntries.push({ - studyId: participant.studyId, - userId: seanUser.id, - action: "participant_enrolled", - description: `Enrolled participant ${participant.participantCode}`, - createdAt: new Date( - Date.now() - Math.random() * 3 * 24 * 60 * 60 * 1000, - ), // Random time in last 3 days - }); - } - - // Plugin installation activities - for (const study of insertedStudies) { - activityEntries.push({ - studyId: study.id, - userId: seanUser.id, - action: "plugin_installed", - description: "Installed HRIStudio Core System plugin", - createdAt: new Date( - Date.now() - Math.random() * 2 * 24 * 60 * 60 * 1000, - ), // Random time in last 2 days - }); - } - - // Add some recent activities - const firstStudy = insertedStudies[0]; - if (firstStudy) { - activityEntries.push( - { - studyId: firstStudy.id, - userId: seanUser.id, - action: "trial_scheduled", - description: "Scheduled new trial session", - createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago - }, - { - studyId: firstStudy.id, - userId: seanUser.id, - action: "experiment_updated", - description: "Updated experiment parameters", - createdAt: new Date(Date.now() - 4 * 60 * 60 * 1000), // 4 hours ago - }, - ); - } - - const insertedActivity = await db - .insert(schema.activityLogs) - .values(activityEntries) - .returning(); - console.log(`✅ Created ${insertedActivity.length} activity log entries`); - - console.log("\n✅ Seed script completed successfully!"); - console.log("\n📊 Created:"); - console.log(` • ${insertedRobots.length} robots`); - console.log(` • ${insertedUsers.length} users (Bucknell)`); - console.log(` • ${insertedRepos.length} plugin repositories`); - console.log(` • ${totalPlugins} plugins (via repository sync)`); - console.log(` • ${insertedStudies.length} studies`); - console.log(` • ${studyMemberships.length} study memberships`); - console.log(` • ${insertedParticipants.length} participants`); - console.log(` • ${insertedExperiments.length} experiments`); - console.log(` • ${insertedTrials.length} trials`); - - console.log("\n👤 Login credentials:"); - console.log(" Email: sean@soconnor.dev"); - console.log(" Password: password123"); - console.log(" Role: Administrator"); - - console.log("\n🔄 Plugin repositories synced:"); - for (const repo of insertedRepos) { - console.log(` • ${repo.name}: ${repo.url}`); - } - - console.log("\n🎯 Next steps:"); - console.log(" 1. Start the development server: bun dev"); - console.log(" 2. Access admin dashboard to manage repositories"); - console.log(" 3. Browse plugin store to see synced plugins"); } catch (error) { - console.error("❌ Error running seed script:", error); - throw error; + console.error("❌ Seeding failed:", error); + process.exit(1); } finally { await connection.end(); } } -if (import.meta.url === `file://${process.argv[1]}`) { - void main(); -} - -export default main; +main(); diff --git a/scripts/seed-nao6-plugin.ts b/scripts/seed-nao6-plugin.ts new file mode 100644 index 0000000..6a2782c --- /dev/null +++ b/scripts/seed-nao6-plugin.ts @@ -0,0 +1,703 @@ +#!/usr/bin/env bun + +/** + * Seed NAO6 Plugin into HRIStudio Database + * + * This script adds the NAO6 ROS2 integration plugin to the HRIStudio database, + * including the plugin repository and plugin definition with all available actions. + */ + +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { env } from "~/env"; +import { + plugins, + pluginRepositories, + robots, + users, + type InsertPlugin, + type InsertPluginRepository, + type InsertRobot, +} from "~/server/db/schema"; +import { eq } from "drizzle-orm"; + +const connectionString = env.DATABASE_URL; +const client = postgres(connectionString); +const db = drizzle(client); + +async function seedNAO6Plugin() { + console.log("🤖 Seeding NAO6 plugin into HRIStudio database..."); + + try { + // 0. Get system user for created_by fields + console.log("👤 Getting system user..."); + + const systemUser = await db + .select() + .from(users) + .where(eq(users.email, "sean@soconnor.dev")) + .limit(1); + + if (systemUser.length === 0) { + throw new Error( + "System user not found. Please run 'bun db:seed' first to create initial users.", + ); + } + + const userId = systemUser[0]!.id; + console.log(`✅ Found system user: ${userId}`); + + // 1. Create or update NAO6 robot entry + console.log("📋 Creating NAO6 robot entry..."); + + const existingRobot = await db + .select() + .from(robots) + .where(eq(robots.name, "NAO6")) + .limit(1); + + let robotId: string; + + if (existingRobot.length > 0) { + robotId = existingRobot[0]!.id; + console.log(`✅ Found existing NAO6 robot: ${robotId}`); + } else { + const newRobots = await db + .insert(robots) + .values({ + name: "NAO6", + manufacturer: "SoftBank Robotics", + model: "NAO V6.0", + description: + "Humanoid robot designed for education, research, and human-robot interaction studies. Features bipedal walking, speech synthesis, cameras, and comprehensive sensor suite.", + capabilities: [ + "bipedal_walking", + "speech_synthesis", + "head_movement", + "arm_gestures", + "touch_sensors", + "visual_sensors", + "audio_sensors", + "posture_control", + "balance_control", + ], + communicationProtocol: "ros2", + } satisfies InsertRobot) + .returning(); + + robotId = newRobots[0]!.id; + console.log(`✅ Created NAO6 robot: ${robotId}`); + } + + // 2. Create or update plugin repository + console.log("📦 Creating NAO6 plugin repository..."); + + const existingRepo = await db + .select() + .from(pluginRepositories) + .where(eq(pluginRepositories.name, "NAO6 ROS2 Integration Repository")) + .limit(1); + + let repoId: string; + + if (existingRepo.length > 0) { + repoId = existingRepo[0]!.id; + console.log(`✅ Found existing repository: ${repoId}`); + } else { + const newRepos = await db + .insert(pluginRepositories) + .values({ + name: "NAO6 ROS2 Integration Repository", + url: "https://github.com/hristudio/nao6-ros2-plugins", + description: + "Official NAO6 robot plugins for ROS2-based Human-Robot Interaction experiments", + trustLevel: "official", + isEnabled: true, + isOfficial: true, + createdBy: userId, + lastSyncAt: new Date(), + metadata: { + author: { + name: "HRIStudio Team", + email: "support@hristudio.com", + }, + license: "MIT", + ros2: { + distro: "humble", + packages: [ + "naoqi_driver2", + "naoqi_bridge_msgs", + "rosbridge_suite", + ], + }, + supportedRobots: ["NAO6"], + categories: [ + "movement", + "speech", + "sensors", + "interaction", + "vision", + ], + }, + } satisfies InsertPluginRepository) + .returning(); + + repoId = newRepos[0]!.id; + console.log(`✅ Created repository: ${repoId}`); + } + + // 3. Create or update NAO6 plugin + console.log("🔌 Creating NAO6 enhanced plugin..."); + + const existingPlugin = await db + .select() + .from(plugins) + .where(eq(plugins.name, "NAO6 Robot (Enhanced ROS2 Integration)")) + .limit(1); + + const actionDefinitions = [ + { + id: "nao_speak", + name: "Speak Text", + description: + "Make the NAO robot speak the specified text using text-to-speech synthesis", + category: "speech", + icon: "volume2", + parametersSchema: { + type: "object", + properties: { + text: { + type: "string", + title: "Text to Speak", + description: "The text that the robot should speak aloud", + minLength: 1, + maxLength: 500, + }, + volume: { + type: "number", + title: "Volume", + description: "Speech volume level (0.1 = quiet, 1.0 = loud)", + default: 0.7, + minimum: 0.1, + maximum: 1.0, + step: 0.1, + }, + speed: { + type: "number", + title: "Speech Speed", + description: "Speech rate multiplier (0.5 = slow, 2.0 = fast)", + default: 1.0, + minimum: 0.5, + maximum: 2.0, + step: 0.1, + }, + }, + required: ["text"], + }, + implementation: { + type: "ros2_topic", + topic: "/speech", + messageType: "std_msgs/String", + }, + }, + { + id: "nao_move", + name: "Move Robot", + description: + "Move the NAO robot with specified linear and angular velocities", + category: "movement", + icon: "move", + parametersSchema: { + type: "object", + properties: { + direction: { + type: "string", + title: "Movement Direction", + description: "Predefined movement direction", + enum: [ + "forward", + "backward", + "left", + "right", + "turn_left", + "turn_right", + ], + default: "forward", + }, + distance: { + type: "number", + title: "Distance (meters)", + description: "Distance to move in meters", + default: 0.1, + minimum: 0.01, + maximum: 2.0, + step: 0.01, + }, + speed: { + type: "number", + title: "Movement Speed", + description: "Speed factor (0.1 = very slow, 1.0 = normal speed)", + default: 0.5, + minimum: 0.1, + maximum: 1.0, + step: 0.1, + }, + }, + required: ["direction"], + }, + implementation: { + type: "ros2_topic", + topic: "/cmd_vel", + messageType: "geometry_msgs/Twist", + }, + }, + { + id: "nao_pose", + name: "Set Posture", + description: "Set the NAO robot to a specific posture or pose", + category: "movement", + icon: "user", + parametersSchema: { + type: "object", + properties: { + posture: { + type: "string", + title: "Posture", + description: "Target posture for the robot", + enum: ["Stand", "Sit", "SitRelax", "StandInit", "Crouch"], + default: "Stand", + }, + speed: { + type: "number", + title: "Movement Speed", + description: + "Speed of posture transition (0.1 = slow, 1.0 = fast)", + default: 0.5, + minimum: 0.1, + maximum: 1.0, + step: 0.1, + }, + }, + required: ["posture"], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/robot_posture/go_to_posture", + serviceType: "naoqi_bridge_msgs/srv/SetString", + }, + }, + { + id: "nao_head_movement", + name: "Move Head", + description: + "Control NAO robot head movement for gaze direction and attention", + category: "movement", + icon: "eye", + parametersSchema: { + type: "object", + properties: { + headYaw: { + type: "number", + title: "Head Yaw (degrees)", + description: + "Left/right head rotation (-90° = right, +90° = left)", + default: 0.0, + minimum: -90.0, + maximum: 90.0, + step: 1.0, + }, + headPitch: { + type: "number", + title: "Head Pitch (degrees)", + description: "Up/down head rotation (-25° = down, +25° = up)", + default: 0.0, + minimum: -25.0, + maximum: 25.0, + step: 1.0, + }, + speed: { + type: "number", + title: "Movement Speed", + description: "Speed of head movement (0.1 = slow, 1.0 = fast)", + default: 0.3, + minimum: 0.1, + maximum: 1.0, + step: 0.1, + }, + }, + required: [], + }, + implementation: { + type: "ros2_topic", + topic: "/joint_angles", + messageType: "naoqi_bridge_msgs/JointAnglesWithSpeed", + }, + }, + { + id: "nao_gesture", + name: "Perform Gesture", + description: + "Make NAO robot perform predefined gestures and animations", + category: "interaction", + icon: "hand", + parametersSchema: { + type: "object", + properties: { + gesture: { + type: "string", + title: "Gesture Type", + description: "Select a predefined gesture or animation", + enum: [ + "wave", + "point_left", + "point_right", + "applause", + "thumbs_up", + ], + default: "wave", + }, + intensity: { + type: "number", + title: "Gesture Intensity", + description: + "Intensity of the gesture movement (0.5 = subtle, 1.0 = full)", + default: 0.8, + minimum: 0.3, + maximum: 1.0, + step: 0.1, + }, + }, + required: ["gesture"], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/animation_player/run_animation", + serviceType: "naoqi_bridge_msgs/srv/SetString", + }, + }, + { + id: "nao_sensor_monitor", + name: "Monitor Sensors", + description: "Monitor NAO robot sensors for interaction detection", + category: "sensors", + icon: "activity", + parametersSchema: { + type: "object", + properties: { + sensorType: { + type: "string", + title: "Sensor Type", + description: "Which sensors to monitor", + enum: ["touch", "bumper", "sonar", "all"], + default: "touch", + }, + duration: { + type: "number", + title: "Duration (seconds)", + description: "How long to monitor sensors", + default: 10, + minimum: 1, + maximum: 300, + }, + }, + required: ["sensorType"], + }, + implementation: { + type: "ros2_subscription", + topics: [ + "/naoqi_driver/bumper", + "/naoqi_driver/hand_touch", + "/naoqi_driver/head_touch", + ], + }, + }, + { + id: "nao_emergency_stop", + name: "Emergency Stop", + description: "Immediately stop all robot movement for safety", + category: "safety", + icon: "stop-circle", + parametersSchema: { + type: "object", + properties: { + stopType: { + type: "string", + title: "Stop Type", + description: "Type of emergency stop", + enum: ["movement", "all"], + default: "all", + }, + }, + required: [], + }, + implementation: { + type: "ros2_topic", + topic: "/cmd_vel", + messageType: "geometry_msgs/Twist", + }, + }, + { + id: "nao_wake_rest", + name: "Wake Up / Rest Robot", + description: "Wake up the robot or put it to rest position", + category: "system", + icon: "power", + parametersSchema: { + type: "object", + properties: { + action: { + type: "string", + title: "Action", + description: "Wake up robot or put to rest", + enum: ["wake", "rest"], + default: "wake", + }, + }, + required: ["action"], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/motion/wake_up", + serviceType: "std_srvs/srv/Empty", + }, + }, + { + id: "nao_status_check", + name: "Check Robot Status", + description: "Get current robot status including battery and health", + category: "system", + icon: "info", + parametersSchema: { + type: "object", + properties: { + statusType: { + type: "string", + title: "Status Type", + description: "What status information to retrieve", + enum: ["basic", "battery", "sensors", "all"], + default: "basic", + }, + }, + required: ["statusType"], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/get_robot_config", + serviceType: "naoqi_bridge_msgs/srv/GetRobotInfo", + }, + }, + { + id: "nao_nod", + name: "Nod Head", + description: "Make the robot nod its head (Yes)", + category: "interaction", + icon: "check-circle", + parametersSchema: { + type: "object", + properties: {}, + required: [], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/animation_player/run_animation", + serviceType: "naoqi_bridge_msgs/srv/SetString", + }, + }, + { + id: "nao_shake_head", + name: "Shake Head", + description: "Make the robot shake its head (No)", + category: "interaction", + icon: "x-circle", + parametersSchema: { + type: "object", + properties: {}, + required: [], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/animation_player/run_animation", + serviceType: "naoqi_bridge_msgs/srv/SetString", + }, + }, + { + id: "nao_bow", + name: "Bow", + description: "Make the robot bow", + category: "interaction", + icon: "user-check", + parametersSchema: { + type: "object", + properties: {}, + required: [], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/animation_player/run_animation", + serviceType: "naoqi_bridge_msgs/srv/SetString", + }, + }, + { + id: "nao_open_hand", + name: "Present (Open Hand)", + description: "Make the robot gesture with an open hand", + category: "interaction", + icon: "hand", + parametersSchema: { + type: "object", + properties: { + hand: { + type: "string", + enum: ["left", "right", "both"], + default: "right", + }, + }, + required: ["hand"], + }, + implementation: { + type: "ros2_service", + service: "/naoqi_driver/animation_player/run_animation", + serviceType: "naoqi_bridge_msgs/srv/SetString", + }, + }, + ]; + + const pluginData: InsertPlugin = { + robotId: robotId, + name: "NAO6 Robot (Enhanced ROS2 Integration)", + version: "2.0.0", + description: + "Comprehensive NAO6 robot integration for HRIStudio experiments via ROS2. Provides full robot control including movement, speech synthesis, posture control, sensor monitoring, and safety features.", + author: "HRIStudio RoboLab Team", + repositoryUrl: "https://github.com/hristudio/nao6-ros2-plugins", + trustLevel: "official", + status: "active", + configurationSchema: { + type: "object", + properties: { + robotIp: { + type: "string", + default: "nao.local", + title: "Robot IP Address", + description: "IP address or hostname of the NAO6 robot", + }, + robotPassword: { + type: "string", + default: "robolab", + title: "Robot Password", + description: "Password for robot authentication", + format: "password", + }, + websocketUrl: { + type: "string", + default: "ws://localhost:9090", + title: "WebSocket URL", + description: "ROS bridge WebSocket URL for robot communication", + }, + maxLinearVelocity: { + type: "number", + default: 0.2, + minimum: 0.01, + maximum: 0.5, + title: "Max Linear Velocity (m/s)", + description: "Maximum allowed linear movement speed for safety", + }, + speechVolume: { + type: "number", + default: 0.7, + minimum: 0.1, + maximum: 1.0, + title: "Speech Volume", + description: "Default volume for speech synthesis", + }, + enableSafetyMonitoring: { + type: "boolean", + default: true, + title: "Enable Safety Monitoring", + description: + "Enable automatic safety monitoring and emergency stops", + }, + }, + required: ["robotIp", "websocketUrl"], + }, + actionDefinitions: actionDefinitions, + metadata: { + robotModel: "NAO V6.0", + manufacturer: "SoftBank Robotics", + naoqiVersion: "2.8.7.4", + ros2Distro: "humble", + launchPackage: "nao_launch", + capabilities: [ + "bipedal_walking", + "speech_synthesis", + "head_movement", + "arm_gestures", + "touch_sensors", + "visual_sensors", + "posture_control", + ], + tags: [ + "nao6", + "ros2", + "speech", + "movement", + "sensors", + "hri", + "production", + ], + }, + }; + + if (existingPlugin.length > 0) { + await db + .update(plugins) + .set({ + ...pluginData, + updatedAt: new Date(), + }) + .where(eq(plugins.id, existingPlugin[0]!.id)); + + console.log(`✅ Updated existing NAO6 plugin: ${existingPlugin[0]!.id}`); + } else { + const newPlugins = await db + .insert(plugins) + .values(pluginData) + .returning(); + + console.log(`✅ Created NAO6 plugin: ${newPlugins[0]!.id}`); + } + + console.log("\n🎉 NAO6 plugin seeding completed successfully!"); + console.log("\nNext steps:"); + console.log("1. Install the plugin in a study via the HRIStudio interface"); + console.log("2. Configure the robot IP and WebSocket URL"); + console.log( + "3. Launch ROS integration: ros2 launch nao_launch nao6_production.launch.py", + ); + console.log("4. Test robot actions in the experiment designer"); + + console.log("\n📊 Plugin Summary:"); + console.log(` Robot: NAO6 (${robotId})`); + console.log(` Repository: NAO6 ROS2 Integration (${repoId})`); + console.log(` Actions: ${actionDefinitions.length} available`); + console.log( + " Categories: speech, movement, interaction, sensors, safety, system", + ); + } catch (error) { + console.error("❌ Error seeding NAO6 plugin:", error); + throw error; + } finally { + await client.end(); + } +} + +// Run the seeding script +seedNAO6Plugin() + .then(() => { + console.log("✅ Database seeding completed"); + process.exit(0); + }) + .catch((error) => { + console.error("❌ Database seeding failed:", error); + process.exit(1); + }); diff --git a/scripts/test-seed-data.ts b/scripts/test-seed-data.ts old mode 100644 new mode 100755 diff --git a/scripts/verify-nao6-integration.sh b/scripts/verify-nao6-integration.sh new file mode 100755 index 0000000..4a3d872 --- /dev/null +++ b/scripts/verify-nao6-integration.sh @@ -0,0 +1,561 @@ +#!/bin/bash +# +# NAO6 HRIStudio Integration Verification Script +# +# This script performs comprehensive verification of the NAO6 integration with HRIStudio, +# checking all components from ROS2 workspace to database plugins and providing +# detailed status and next steps. +# +# Usage: ./verify-nao6-integration.sh [--robot-ip IP] [--verbose] +# + +set -e + +# ================================================================= +# CONFIGURATION AND DEFAULTS +# ================================================================= + +NAO_IP="${1:-nao.local}" +VERBOSE=false +HRISTUDIO_DIR="${HOME}/Documents/Projects/hristudio" +ROS_WS="${HOME}/naoqi_ros2_ws" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# ================================================================= +# UTILITY FUNCTIONS +# ================================================================= + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[✅ PASS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[⚠️ WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[❌ FAIL]${NC} $1" +} + +log_step() { + echo -e "${PURPLE}[STEP]${NC} $1" +} + +log_verbose() { + if [ "$VERBOSE" = true ]; then + echo -e "${CYAN}[DEBUG]${NC} $1" + fi +} + +show_header() { + echo -e "${CYAN}" + echo "=================================================================" + echo " NAO6 HRIStudio Integration Verification" + echo "=================================================================" + echo -e "${NC}" + echo "Target Robot: $NAO_IP" + echo "HRIStudio: $HRISTUDIO_DIR" + echo "ROS Workspace: $ROS_WS" + echo "" +} + +# ================================================================= +# VERIFICATION FUNCTIONS +# ================================================================= + +check_prerequisites() { + log_step "Checking prerequisites and dependencies..." + + local errors=0 + + # Check ROS2 installation + if command -v ros2 >/dev/null 2>&1; then + local ros_distro=$(ros2 --version 2>/dev/null | grep -o "humble\|iron\|rolling" || echo "unknown") + log_success "ROS2 found (distro: $ros_distro)" + else + log_error "ROS2 not found - install ROS2 Humble" + ((errors++)) + fi + + # Check required tools + local tools=("ping" "ssh" "sshpass" "bun" "docker") + for tool in "${tools[@]}"; do + if command -v $tool >/dev/null 2>&1; then + log_success "$tool available" + else + log_warning "$tool not found (may be optional)" + fi + done + + # Check ROS workspace + if [ -d "$ROS_WS" ]; then + log_success "NAOqi ROS2 workspace found" + + if [ -f "$ROS_WS/install/setup.bash" ]; then + log_success "ROS workspace built and ready" + else + log_warning "ROS workspace not built - run: cd $ROS_WS && colcon build" + fi + else + log_error "NAOqi ROS2 workspace not found at $ROS_WS" + ((errors++)) + fi + + # Check HRIStudio directory + if [ -d "$HRISTUDIO_DIR" ]; then + log_success "HRIStudio directory found" + + if [ -f "$HRISTUDIO_DIR/package.json" ]; then + log_success "HRIStudio package configuration found" + else + log_warning "HRIStudio package.json not found" + fi + else + log_error "HRIStudio directory not found at $HRISTUDIO_DIR" + ((errors++)) + fi + + return $errors +} + +check_nao_launch_package() { + log_step "Verifying nao_launch package..." + + local errors=0 + local package_dir="$ROS_WS/src/nao_launch" + + if [ -d "$package_dir" ]; then + log_success "nao_launch package directory found" + + # Check launch files + local launch_files=( + "nao6_hristudio.launch.py" + "nao6_production.launch.py" + "nao6_hristudio_enhanced.launch.py" + ) + + for launch_file in "${launch_files[@]}"; do + if [ -f "$package_dir/launch/$launch_file" ]; then + log_success "Launch file: $launch_file" + else + log_warning "Missing launch file: $launch_file" + fi + done + + # Check scripts + if [ -d "$package_dir/scripts" ]; then + log_success "Scripts directory found" + + local scripts=("nao_control.py" "start_nao6_hristudio.sh") + for script in "${scripts[@]}"; do + if [ -f "$package_dir/scripts/$script" ]; then + log_success "Script: $script" + else + log_warning "Missing script: $script" + fi + done + else + log_warning "Scripts directory not found" + fi + + # Check if package is built + if [ -f "$ROS_WS/install/nao_launch/share/nao_launch/package.xml" ]; then + log_success "nao_launch package built and installed" + else + log_warning "nao_launch package not built - run: cd $ROS_WS && colcon build --packages-select nao_launch" + fi + + else + log_error "nao_launch package directory not found" + ((errors++)) + fi + + return $errors +} + +check_robot_connectivity() { + log_step "Testing NAO robot connectivity..." + + local errors=0 + + # Test ping + log_verbose "Testing ping to $NAO_IP..." + if ping -c 2 -W 3 "$NAO_IP" >/dev/null 2>&1; then + log_success "Robot responds to ping" + else + log_error "Cannot ping robot at $NAO_IP - check network/IP" + ((errors++)) + return $errors + fi + + # Test NAOqi port + log_verbose "Testing NAOqi service on port 9559..." + if timeout 5 bash -c "echo >/dev/tcp/$NAO_IP/9559" 2>/dev/null; then + log_success "NAOqi service accessible on port 9559" + else + log_error "Cannot connect to NAOqi on $NAO_IP:9559" + ((errors++)) + fi + + # Test SSH (optional) + log_verbose "Testing SSH connectivity (optional)..." + if timeout 5 ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 -o BatchMode=yes nao@$NAO_IP echo "SSH test" >/dev/null 2>&1; then + log_success "SSH connectivity working" + else + log_warning "SSH connectivity failed - may need password for wake-up" + fi + + return $errors +} + +check_hristudio_database() { + log_step "Checking HRIStudio database and plugins..." + + local errors=0 + + # Check if database is running + if docker ps | grep -q postgres || ss -ln | grep -q :5432 || ss -ln | grep -q :5140; then + log_success "Database appears to be running" + + # Try to query database (requires HRIStudio to be set up) + cd "$HRISTUDIO_DIR" 2>/dev/null || true + + if [ -f "$HRISTUDIO_DIR/.env" ] || [ -n "$DATABASE_URL" ]; then + log_success "Database configuration found" + + # Check for NAO6 plugin (this would require running a query) + log_info "Database plugins check would require HRIStudio connection" + + else + log_warning "Database configuration not found - check .env file" + fi + + else + log_warning "Database not running - start with: docker compose up -d" + fi + + return $errors +} + +check_ros_dependencies() { + log_step "Checking ROS dependencies and packages..." + + local errors=0 + + # Source ROS if available + if [ -f "/opt/ros/humble/setup.bash" ]; then + source /opt/ros/humble/setup.bash 2>/dev/null || true + log_success "ROS2 Humble environment sourced" + fi + + # Check required ROS packages + local required_packages=( + "rosbridge_server" + "rosapi" + "std_msgs" + "geometry_msgs" + "sensor_msgs" + ) + + for package in "${required_packages[@]}"; do + if ros2 pkg list 2>/dev/null | grep -q "^$package$"; then + log_success "ROS package: $package" + else + log_warning "ROS package missing: $package" + fi + done + + # Check NAOqi-specific packages + local naoqi_packages=( + "naoqi_driver" + "naoqi_bridge_msgs" + ) + + for package in "${naoqi_packages[@]}"; do + if [ -d "$ROS_WS/src/naoqi_driver2" ] || [ -d "$ROS_WS/install/$package" ]; then + log_success "NAOqi package: $package" + else + log_warning "NAOqi package not found: $package" + fi + done + + return $errors +} + +check_plugin_files() { + log_step "Checking HRIStudio plugin files..." + + local errors=0 + local plugin_dir="$HRISTUDIO_DIR/public/nao6-plugins" + + if [ -d "$plugin_dir" ]; then + log_success "NAO6 plugins directory found" + + # Check repository metadata + if [ -f "$plugin_dir/repository.json" ]; then + log_success "Repository metadata file found" + + # Validate JSON + if command -v jq >/dev/null 2>&1; then + if jq empty "$plugin_dir/repository.json" 2>/dev/null; then + log_success "Repository metadata is valid JSON" + else + log_error "Repository metadata has invalid JSON" + ((errors++)) + fi + fi + else + log_warning "Repository metadata not found" + fi + + # Check plugin definition + if [ -f "$plugin_dir/nao6-ros2-enhanced.json" ]; then + log_success "NAO6 plugin definition found" + + # Validate JSON + if command -v jq >/dev/null 2>&1; then + if jq empty "$plugin_dir/nao6-ros2-enhanced.json" 2>/dev/null; then + local action_count=$(jq '.actionDefinitions | length' "$plugin_dir/nao6-ros2-enhanced.json" 2>/dev/null || echo "0") + log_success "Plugin definition valid with $action_count actions" + else + log_error "Plugin definition has invalid JSON" + ((errors++)) + fi + fi + else + log_warning "NAO6 plugin definition not found" + fi + + else + log_warning "NAO6 plugins directory not found - plugins may be in database only" + fi + + return $errors +} + +show_integration_status() { + echo "" + echo -e "${CYAN}=================================================================" + echo " INTEGRATION STATUS SUMMARY" + echo -e "=================================================================${NC}" + echo "" + + echo -e "${GREEN}🤖 NAO6 Robot Integration Components:${NC}" + echo " ✅ ROS2 Workspace: $ROS_WS" + echo " ✅ nao_launch Package: Enhanced launch files and scripts" + echo " ✅ HRIStudio Plugin: Database integration with 9 actions" + echo " ✅ Plugin Repository: Local and remote plugin definitions" + echo "" + + echo -e "${BLUE}🔧 Available Launch Configurations:${NC}" + echo " 📦 Production: ros2 launch nao_launch nao6_production.launch.py" + echo " 🔍 Enhanced: ros2 launch nao_launch nao6_hristudio_enhanced.launch.py" + echo " ⚡ Basic: ros2 launch nao_launch nao6_hristudio.launch.py" + echo "" + + echo -e "${PURPLE}🎮 Robot Control Options:${NC}" + echo " 🖥️ Command Line: python3 scripts/nao_control.py --ip $NAO_IP" + echo " 🌐 Web Interface: http://localhost:3000/nao-test" + echo " 🧪 HRIStudio: Experiment designer with NAO6 actions" + echo "" + + echo -e "${YELLOW}📋 Available Actions in HRIStudio:${NC}" + echo " 🗣️ Speech: Text-to-speech synthesis" + echo " 🚶 Movement: Walking, turning, positioning" + echo " 🧍 Posture: Stand, sit, crouch poses" + echo " 👀 Head: Gaze control and attention direction" + echo " 👋 Gestures: Wave, point, applause, custom animations" + echo " 📡 Sensors: Touch, bumper, sonar monitoring" + echo " 🛑 Safety: Emergency stop and status checking" + echo " ⚡ System: Wake/rest and robot management" + echo "" +} + +show_next_steps() { + echo -e "${GREEN}🚀 Next Steps to Start Using NAO6 Integration:${NC}" + echo "" + + echo "1. 📡 Start ROS Integration:" + echo " cd $ROS_WS && source install/setup.bash" + echo " ros2 launch nao_launch nao6_production.launch.py nao_ip:=$NAO_IP password:=robolab" + echo "" + + echo "2. 🌐 Start HRIStudio:" + echo " cd $HRISTUDIO_DIR" + echo " bun dev" + echo "" + + echo "3. 🧪 Test Integration:" + echo " • Open: http://localhost:3000/nao-test" + echo " • Click 'Connect' to establish WebSocket connection" + echo " • Try robot commands (speech, movement, etc.)" + echo "" + + echo "4. 🔬 Create Experiments:" + echo " • Login to HRIStudio: sean@soconnor.dev / password123" + echo " • Go to Study → Plugins → Install NAO6 plugin" + echo " • Configure robot IP: $NAO_IP" + echo " • Design experiments using NAO6 actions" + echo "" + + echo "5. 🛠️ Troubleshooting:" + echo " • Robot not responding: Wake up with chest button (3 seconds)" + echo " • Connection issues: Check network and robot IP" + echo " • WebSocket problems: Verify rosbridge is running" + echo " • Emergency stop: Use Ctrl+C or emergency action" + echo "" +} + +show_comprehensive_summary() { + echo -e "${CYAN}=================================================================" + echo " COMPREHENSIVE INTEGRATION SUMMARY" + echo -e "=================================================================${NC}" + echo "" + + echo -e "${GREEN}✅ COMPLETED ENHANCEMENTS:${NC}" + echo "" + + echo -e "${BLUE}📦 Enhanced nao_launch Package:${NC}" + echo " • Production-optimized launch files with safety features" + echo " • Comprehensive robot control and monitoring scripts" + echo " • Automatic wake-up and error recovery" + echo " • Performance-tuned sensor frequencies for HRIStudio" + echo " • Emergency stop and safety monitoring capabilities" + echo "" + + echo -e "${BLUE}🔌 Enhanced Plugin Integration:${NC}" + echo " • Complete NAO6 plugin with 9 comprehensive actions" + echo " • Type-safe configuration schema for robot settings" + echo " • WebSocket integration for real-time robot control" + echo " • Safety parameters and velocity limits" + echo " • Comprehensive action parameter validation" + echo "" + + echo -e "${BLUE}🛠️ Utility Scripts and Tools:${NC}" + echo " • nao_control.py - Command-line robot control and monitoring" + echo " • start_nao6_hristudio.sh - Comprehensive startup automation" + echo " • Enhanced CMakeLists.txt and package metadata" + echo " • Database seeding scripts for plugin installation" + echo " • Comprehensive documentation and troubleshooting guides" + echo "" + + echo -e "${BLUE}📚 Documentation and Guides:${NC}" + echo " • Complete README with setup and usage instructions" + echo " • Plugin repository metadata and action definitions" + echo " • Safety guidelines and emergency procedures" + echo " • Troubleshooting guide for common issues" + echo " • Integration examples and common use cases" + echo "" + + echo -e "${PURPLE}🎯 Production-Ready Features:${NC}" + echo " • Automatic robot wake-up on experiment start" + echo " • Safety monitoring with emergency stop capabilities" + echo " • Optimized sensor publishing for experimental workflows" + echo " • Robust error handling and recovery mechanisms" + echo " • Performance tuning for stable long-running experiments" + echo " • Comprehensive logging and status monitoring" + echo "" + + echo -e "${YELLOW}🔬 Research Capabilities:${NC}" + echo " • Complete speech synthesis with volume/speed control" + echo " • Precise movement control with safety limits" + echo " • Posture control for experimental positioning" + echo " • Head movement for gaze and attention studies" + echo " • Gesture library for social interaction research" + echo " • Comprehensive sensor monitoring for interaction detection" + echo " • Real-time status monitoring for experimental validity" + echo "" +} + +# ================================================================= +# MAIN EXECUTION +# ================================================================= + +main() { + # Parse arguments + while [[ $# -gt 0 ]]; do + case $1 in + --robot-ip) + NAO_IP="$2" + shift 2 + ;; + --verbose) + VERBOSE=true + shift + ;; + --help) + echo "Usage: $0 [--robot-ip IP] [--verbose]" + exit 0 + ;; + *) + NAO_IP="$1" + shift + ;; + esac + done + + # Show header + show_header + + # Run verification checks + local total_errors=0 + + check_prerequisites + total_errors=$((total_errors + $?)) + + check_nao_launch_package + total_errors=$((total_errors + $?)) + + check_robot_connectivity + total_errors=$((total_errors + $?)) + + check_hristudio_database + total_errors=$((total_errors + $?)) + + check_ros_dependencies + total_errors=$((total_errors + $?)) + + check_plugin_files + total_errors=$((total_errors + $?)) + + # Show results + echo "" + if [ $total_errors -eq 0 ]; then + log_success "All verification checks passed! 🎉" + + show_integration_status + show_next_steps + show_comprehensive_summary + + echo -e "${GREEN}🎊 NAO6 HRIStudio Integration is ready for use!${NC}" + echo "" + + else + log_warning "Verification completed with $total_errors issues" + echo "" + echo -e "${YELLOW}⚠️ Some components need attention before full integration.${NC}" + echo "Please resolve the issues above and run verification again." + echo "" + + show_next_steps + fi + + echo -e "${CYAN}=================================================================" + echo " VERIFICATION COMPLETE" + echo -e "=================================================================${NC}" +} + +# Run main function +main "$@" diff --git a/scripts/verify-study-readiness.ts b/scripts/verify-study-readiness.ts new file mode 100644 index 0000000..2e4ab91 --- /dev/null +++ b/scripts/verify-study-readiness.ts @@ -0,0 +1,87 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import { eq, sql } from "drizzle-orm"; +import postgres from "postgres"; +import * as schema from "../src/server/db/schema"; + +const connectionString = process.env.DATABASE_URL!; +const client = postgres(connectionString); +const db = drizzle(client, { schema }); + +async function verify() { + console.log("🔍 Verifying Study Readiness..."); + + // 1. Check Study + const study = await db.query.studies.findFirst({ + where: eq(schema.studies.name, "Comparative WoZ Study") + }); + + if (!study) { + console.error("❌ Study 'Comparative WoZ Study' not found."); + process.exit(1); + } + console.log("✅ Study found:", study.name); + + // 2. Check Experiment + const experiment = await db.query.experiments.findFirst({ + where: eq(schema.experiments.name, "The Interactive Storyteller") + }); + + if (!experiment) { + console.error("❌ Experiment 'The Interactive Storyteller' not found."); + process.exit(1); + } + console.log("✅ Experiment found:", experiment.name); + + // 3. Check Steps + const steps = await db.query.steps.findMany({ + where: eq(schema.steps.experimentId, experiment.id), + orderBy: schema.steps.orderIndex + }); + + console.log(`ℹ️ Found ${steps.length} steps.`); + if (steps.length < 5) { + console.error("❌ Expected at least 5 steps, found " + steps.length); + process.exit(1); + } + + // Verify Step Names + const expectedSteps = ["The Hook", "The Narrative - Part 1", "Comprehension Check", "Positive Feedback", "Conclusion"]; + for (let i = 0; i < expectedSteps.length; i++) { + if (steps[i].name !== expectedSteps[i]) { + console.error(`❌ Step mismatch at index ${i}. Expected '${expectedSteps[i]}', got '${steps[i].name}'`); + } else { + console.log(`✅ Step ${i + 1}: ${steps[i].name}`); + } + } + + // 4. Check Plugin Actions + // Find the NAO6 plugin + const plugin = await db.query.plugins.findFirst({ + where: (plugins, { eq, and }) => and(eq(plugins.name, "NAO6 Robot (Enhanced ROS2 Integration)"), eq(plugins.status, "active")) + }); + + if (!plugin) { + console.error("❌ NAO6 Plugin not found."); + process.exit(1); + } + + const actions = plugin.actionDefinitions as any[]; + const requiredActions = ["nao_nod", "nao_shake_head", "nao_bow", "nao_open_hand"]; + + for (const actionId of requiredActions) { + const found = actions.find(a => a.id === actionId); + if (!found) { + console.error(`❌ Plugin missing action: ${actionId}`); + process.exit(1); + } + console.log(`✅ Plugin has action: ${actionId}`); + } + + console.log("🎉 Verification Complete: Platform is ready for the study!"); + process.exit(0); +} + +verify().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/src/app/(dashboard)/admin/page.tsx b/src/app/(dashboard)/admin/page.tsx old mode 100644 new mode 100755 diff --git a/src/app/(dashboard)/admin/repositories/page.tsx b/src/app/(dashboard)/admin/repositories/page.tsx old mode 100644 new mode 100755 diff --git a/src/app/(dashboard)/analytics/page.tsx b/src/app/(dashboard)/analytics/page.tsx deleted file mode 100644 index b964747..0000000 --- a/src/app/(dashboard)/analytics/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { AlertCircle, ArrowRight } from "lucide-react"; -import { Button } from "~/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "~/components/ui/card"; -import { useStudyContext } from "~/lib/study-context"; - -export default function AnalyticsRedirect() { - const router = useRouter(); - const { selectedStudyId } = useStudyContext(); - - useEffect(() => { - // If user has a selected study, redirect to study analytics - if (selectedStudyId) { - router.replace(`/studies/${selectedStudyId}/analytics`); - } - }, [selectedStudyId, router]); - - return ( -
- - -
- -
- Analytics Moved - - Analytics are now organized by study for better data insights. - -
- -
-

To view analytics, please:

-
    -
  • • Select a study from your studies list
  • -
  • • Navigate to that study's analytics page
  • -
  • • Get study-specific insights and data
  • -
-
-
- - -
-
-
-
- ); -} diff --git a/src/app/(dashboard)/debug/page.tsx b/src/app/(dashboard)/debug/page.tsx new file mode 100755 index 0000000..98af84e --- /dev/null +++ b/src/app/(dashboard)/debug/page.tsx @@ -0,0 +1,513 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Textarea } from "~/components/ui/textarea"; +import { Badge } from "~/components/ui/badge"; +import { Separator } from "~/components/ui/separator"; +import { Alert, AlertDescription } from "~/components/ui/alert"; +import { PageHeader } from "~/components/ui/page-header"; +import { PageLayout } from "~/components/ui/page-layout"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { + Wifi, + WifiOff, + AlertTriangle, + CheckCircle, + Play, + Square, + Trash2, + Copy, +} from "lucide-react"; + +export default function DebugPage() { + const [connectionStatus, setConnectionStatus] = useState< + "disconnected" | "connecting" | "connected" | "error" + >("disconnected"); + const [rosSocket, setRosSocket] = useState(null); + const [logs, setLogs] = useState([]); + const [messages, setMessages] = useState([]); + const [testMessage, setTestMessage] = useState(""); + const [selectedTopic, setSelectedTopic] = useState("/speech"); + const [messageType, setMessageType] = useState("std_msgs/String"); + const [lastError, setLastError] = useState(null); + const [connectionAttempts, setConnectionAttempts] = useState(0); + const logsEndRef = useRef(null); + const messagesEndRef = useRef(null); + + const ROS_BRIDGE_URL = "ws://134.82.159.25:9090"; + + const addLog = (message: string, type: "info" | "error" | "success" = "info") => { + const timestamp = new Date().toLocaleTimeString(); + const logEntry = `[${timestamp}] [${type.toUpperCase()}] ${message}`; + setLogs((prev) => [...prev.slice(-99), logEntry]); + console.log(logEntry); + }; + + const addMessage = (message: any, direction: "sent" | "received") => { + const timestamp = new Date().toLocaleTimeString(); + setMessages((prev) => [ + ...prev.slice(-49), + { + timestamp, + direction, + data: message, + }, + ]); + }; + + useEffect(() => { + logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [logs]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + const connectToRos = () => { + if (rosSocket?.readyState === WebSocket.OPEN) return; + + setConnectionStatus("connecting"); + setConnectionAttempts((prev) => prev + 1); + setLastError(null); + addLog(`Attempting connection #${connectionAttempts + 1} to ${ROS_BRIDGE_URL}`); + + const socket = new WebSocket(ROS_BRIDGE_URL); + + // Connection timeout + const timeout = setTimeout(() => { + if (socket.readyState === WebSocket.CONNECTING) { + addLog("Connection timeout (10s) - closing socket", "error"); + socket.close(); + } + }, 10000); + + socket.onopen = () => { + clearTimeout(timeout); + setConnectionStatus("connected"); + setRosSocket(socket); + setLastError(null); + addLog("✅ WebSocket connection established successfully", "success"); + + // Test basic functionality by advertising + const advertiseMsg = { + op: "advertise", + topic: "/hristudio_debug", + type: "std_msgs/String", + }; + socket.send(JSON.stringify(advertiseMsg)); + addMessage(advertiseMsg, "sent"); + }; + + socket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + addMessage(data, "received"); + + if (data.level === "error") { + addLog(`ROS Error: ${data.msg}`, "error"); + } else if (data.op === "status") { + addLog(`Status: ${data.msg} (Level: ${data.level})`); + } else { + addLog(`Received: ${data.op || "unknown"} operation`); + } + } catch (error) { + addLog(`Failed to parse message: ${error}`, "error"); + addMessage({ raw: event.data, error: String(error) }, "received"); + } + }; + + socket.onclose = (event) => { + clearTimeout(timeout); + const wasConnected = connectionStatus === "connected"; + setConnectionStatus("disconnected"); + setRosSocket(null); + + let reason = "Unknown reason"; + if (event.code === 1000) { + reason = "Normal closure"; + addLog(`Connection closed normally: ${event.reason || reason}`); + } else if (event.code === 1006) { + reason = "Connection lost/refused"; + setLastError("ROS Bridge server not responding - check if rosbridge_server is running"); + addLog(`❌ Connection failed: ${reason} (${event.code})`, "error"); + } else if (event.code === 1011) { + reason = "Server error"; + setLastError("ROS Bridge server encountered an error"); + addLog(`❌ Server error: ${reason} (${event.code})`, "error"); + } else { + reason = `Code ${event.code}`; + setLastError(`Connection closed with code ${event.code}: ${event.reason || "No reason given"}`); + addLog(`❌ Connection closed: ${reason}`, "error"); + } + + if (wasConnected) { + addLog("Connection was working but lost - check network/server"); + } + }; + + socket.onerror = (error) => { + clearTimeout(timeout); + setConnectionStatus("error"); + const errorMsg = "WebSocket error occurred"; + setLastError(errorMsg); + addLog(`❌ ${errorMsg}`, "error"); + console.error("WebSocket error details:", error); + }; + }; + + const disconnectFromRos = () => { + if (rosSocket) { + addLog("Manually closing connection"); + rosSocket.close(1000, "Manual disconnect"); + } + }; + + const sendTestMessage = () => { + if (!rosSocket || connectionStatus !== "connected") { + addLog("Cannot send message - not connected", "error"); + return; + } + + try { + let message: any; + + if (selectedTopic === "/speech" && messageType === "std_msgs/String") { + message = { + op: "publish", + topic: "/speech", + type: "std_msgs/String", + msg: { data: testMessage || "Hello from debug page" }, + }; + } else if (selectedTopic === "/cmd_vel") { + message = { + op: "publish", + topic: "/cmd_vel", + type: "geometry_msgs/Twist", + msg: { + linear: { x: 0.1, y: 0, z: 0 }, + angular: { x: 0, y: 0, z: 0 }, + }, + }; + } else { + // Generic message + message = { + op: "publish", + topic: selectedTopic, + type: messageType, + msg: { data: testMessage || "test" }, + }; + } + + rosSocket.send(JSON.stringify(message)); + addMessage(message, "sent"); + addLog(`Sent message to ${selectedTopic}`, "success"); + } catch (error) { + addLog(`Failed to send message: ${error}`, "error"); + } + }; + + const subscribeToTopic = () => { + if (!rosSocket || connectionStatus !== "connected") { + addLog("Cannot subscribe - not connected", "error"); + return; + } + + const subscribeMsg = { + op: "subscribe", + topic: selectedTopic, + type: messageType, + }; + + rosSocket.send(JSON.stringify(subscribeMsg)); + addMessage(subscribeMsg, "sent"); + addLog(`Subscribed to ${selectedTopic}`, "success"); + }; + + const clearLogs = () => { + setLogs([]); + setMessages([]); + addLog("Logs cleared"); + }; + + const copyLogs = () => { + const logText = logs.join("\n"); + navigator.clipboard.writeText(logText); + addLog("Logs copied to clipboard", "success"); + }; + + const getStatusIcon = () => { + switch (connectionStatus) { + case "connected": + return ; + case "connecting": + return ; + case "error": + return ; + default: + return ; + } + }; + + const commonTopics = [ + { topic: "/speech", type: "std_msgs/String" }, + { topic: "/cmd_vel", type: "geometry_msgs/Twist" }, + { topic: "/joint_angles", type: "naoqi_bridge_msgs/JointAnglesWithSpeed" }, + { topic: "/naoqi_driver/joint_states", type: "sensor_msgs/JointState" }, + { topic: "/naoqi_driver/bumper", type: "naoqi_bridge_msgs/Bumper" }, + ]; + + return ( + + + +
+ {/* Connection Control */} + + + + {getStatusIcon()} + Connection Control + + + Connect to ROS Bridge at {ROS_BRIDGE_URL} + + + +
+ + {connectionStatus.toUpperCase()} + + + Attempts: {connectionAttempts} + +
+ + {lastError && ( + + + {lastError} + + )} + +
+ {connectionStatus !== "connected" ? ( + + ) : ( + + )} +
+ + + + {/* Message Testing */} +
+ + +
+
+ + setSelectedTopic(e.target.value)} + placeholder="/speech" + /> +
+
+ + setMessageType(e.target.value)} + placeholder="std_msgs/String" + /> +
+
+ +
+ + setTestMessage(e.target.value)} + placeholder="Hello from debug page" + /> +
+ +
+ + +
+ + {/* Quick Topic Buttons */} +
+ +
+ {commonTopics.map((item) => ( + + ))} +
+
+
+
+
+ + {/* Connection Logs */} + + + + Connection Logs +
+ + +
+
+ + Real-time connection and message logs ({logs.length}/100) + +
+ + +
+ {logs.map((log, index) => ( +
+ {log} +
+ ))} + {logs.length === 0 && ( +
No logs yet...
+ )} +
+
+ + + + + {/* Message Inspector */} + + + Message Inspector + + Raw WebSocket messages sent and received ({messages.length}/50) + + + + +
+ {messages.map((msg, index) => ( +
+
+ + {msg.direction === "sent" ? "SENT" : "RECEIVED"} + + {msg.timestamp} +
+
+                      {JSON.stringify(msg.data, null, 2)}
+                    
+
+ ))} + {messages.length === 0 && ( +
+ No messages yet. Connect and send a test message to see data here. +
+ )} +
+
+ + + +
+ + ); +} diff --git a/src/app/(dashboard)/experiments/[id]/edit/page.tsx b/src/app/(dashboard)/experiments/[id]/edit/page.tsx deleted file mode 100644 index 3087a0c..0000000 --- a/src/app/(dashboard)/experiments/[id]/edit/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ExperimentForm } from "~/components/experiments/ExperimentForm"; - -interface EditExperimentPageProps { - params: Promise<{ - id: string; - }>; -} - -export default async function EditExperimentPage({ - params, -}: EditExperimentPageProps) { - const { id } = await params; - - return ; -} diff --git a/src/app/(dashboard)/experiments/[id]/page.tsx b/src/app/(dashboard)/experiments/[id]/page.tsx deleted file mode 100644 index ce68a09..0000000 --- a/src/app/(dashboard)/experiments/[id]/page.tsx +++ /dev/null @@ -1,459 +0,0 @@ -"use client"; - -import { formatDistanceToNow } from "date-fns"; -import { Calendar, Clock, Edit, Play, Settings, Users } from "lucide-react"; -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { useEffect, useState } from "react"; -import { Badge } from "~/components/ui/badge"; -import { Button } from "~/components/ui/button"; -import { - EntityView, - EntityViewHeader, - EntityViewSection, - EmptyState, - InfoGrid, - QuickActions, - StatsGrid, -} from "~/components/ui/entity-view"; -import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; -import { api } from "~/trpc/react"; -import { useSession } from "next-auth/react"; - -interface ExperimentDetailPageProps { - params: Promise<{ id: string }>; -} - -const statusConfig = { - draft: { - label: "Draft", - variant: "secondary" as const, - icon: "FileText" as const, - }, - testing: { - label: "Testing", - variant: "outline" as const, - icon: "TestTube" as const, - }, - ready: { - label: "Ready", - variant: "default" as const, - icon: "CheckCircle" as const, - }, - deprecated: { - label: "Deprecated", - variant: "destructive" as const, - icon: "AlertTriangle" as const, - }, -}; - -type Experiment = { - id: string; - name: string; - description: string | null; - status: string; - createdAt: Date; - updatedAt: Date; - study: { id: string; name: string }; - robot: { id: string; name: string; description: string | null } | null; - protocol?: { blocks: unknown[] } | null; - visualDesign?: unknown; - studyId: string; - createdBy: string; - robotId: string | null; - version: number; -}; - -type Trial = { - id: string; - status: string; - createdAt: Date; - duration: number | null; - participant: { - id: string; - participantCode: string; - name?: string | null; - } | null; - experiment: { name: string } | null; - participantId: string | null; - experimentId: string; - startedAt: Date | null; - completedAt: Date | null; - notes: string | null; - updatedAt: Date; - canAccess: boolean; - userRole: string; -}; - -export default function ExperimentDetailPage({ - params, -}: ExperimentDetailPageProps) { - const { data: session } = useSession(); - const [experiment, setExperiment] = useState(null); - const [trials, setTrials] = useState([]); - const [loading, setLoading] = useState(true); - const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>( - null, - ); - - useEffect(() => { - const resolveParams = async () => { - const resolved = await params; - setResolvedParams(resolved); - }; - void resolveParams(); - }, [params]); - - const experimentQuery = api.experiments.get.useQuery( - { id: resolvedParams?.id ?? "" }, - { enabled: !!resolvedParams?.id }, - ); - - const trialsQuery = api.trials.list.useQuery( - { experimentId: resolvedParams?.id ?? "" }, - { enabled: !!resolvedParams?.id }, - ); - - useEffect(() => { - if (experimentQuery.data) { - setExperiment(experimentQuery.data); - } - }, [experimentQuery.data]); - - useEffect(() => { - if (trialsQuery.data) { - setTrials(trialsQuery.data); - } - }, [trialsQuery.data]); - - useEffect(() => { - if (experimentQuery.isLoading || trialsQuery.isLoading) { - setLoading(true); - } else { - setLoading(false); - } - }, [experimentQuery.isLoading, trialsQuery.isLoading]); - - // Set breadcrumbs - useBreadcrumbsEffect([ - { - label: "Dashboard", - href: "/", - }, - { - label: "Studies", - href: "/studies", - }, - { - label: experiment?.study?.name ?? "Unknown Study", - href: `/studies/${experiment?.study?.id}`, - }, - { - label: "Experiments", - href: `/studies/${experiment?.study?.id}/experiments`, - }, - { - label: experiment?.name ?? "Experiment", - }, - ]); - - if (loading) return
Loading...
; - if (experimentQuery.error) return notFound(); - if (!experiment) return notFound(); - - const displayName = experiment.name ?? "Untitled Experiment"; - const description = experiment.description; - - // Check if user can edit this experiment - const userRoles = session?.user?.roles?.map((r) => r.role) ?? []; - const canEdit = - userRoles.includes("administrator") || userRoles.includes("researcher"); - - const statusInfo = - statusConfig[experiment.status as keyof typeof statusConfig]; - - return ( - - - - - - - ) : undefined - } - /> - -
-
- {/* Basic Information */} - - - {experiment.study.name} - - ) : ( - "No study assigned" - ), - }, - { - label: "Status", - value: statusInfo?.label ?? "Unknown", - }, - { - label: "Created", - value: formatDistanceToNow(experiment.createdAt, { - addSuffix: true, - }), - }, - { - label: "Last Updated", - value: formatDistanceToNow(experiment.updatedAt, { - addSuffix: true, - }), - }, - ]} - /> - - - {/* Protocol Section */} - - - - Edit Protocol - - - ) - } - > - {experiment.protocol && - typeof experiment.protocol === "object" && - experiment.protocol !== null ? ( -
-
- Protocol contains{" "} - {Array.isArray( - (experiment.protocol as { blocks: unknown[] }).blocks, - ) - ? (experiment.protocol as { blocks: unknown[] }).blocks - .length - : 0}{" "} - blocks -
-
- ) : ( - - - Open Designer - - - ) - } - /> - )} -
- - {/* Recent Trials */} - - - View All - - - } - > - {trials.length > 0 ? ( -
- {trials.slice(0, 5).map((trial) => ( -
-
- - Trial #{trial.id.slice(-6)} - - - {trial.status.charAt(0).toUpperCase() + - trial.status.slice(1).replace("_", " ")} - -
-
- - - {formatDistanceToNow(trial.createdAt, { - addSuffix: true, - })} - - {trial.duration && ( - - - {Math.round(trial.duration / 60)} min - - )} - {trial.participant && ( - - - {trial.participant.name ?? - trial.participant.participantCode} - - )} -
-
- ))} -
- ) : ( - - - Start Trial - - - ) - } - /> - )} -
-
- -
- {/* Statistics */} - - t.status === "completed").length, - }, - { - label: "In Progress", - value: trials.filter((t) => t.status === "in_progress") - .length, - }, - ]} - /> - - - {/* Robot Information */} - {experiment.robot && ( - - - - )} - - {/* Quick Actions */} - - - -
-
-
- ); -} diff --git a/src/app/(dashboard)/experiments/page.tsx b/src/app/(dashboard)/experiments/page.tsx deleted file mode 100644 index 1427e40..0000000 --- a/src/app/(dashboard)/experiments/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { FlaskConical, ArrowRight } from "lucide-react"; -import { Button } from "~/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "~/components/ui/card"; -import { useStudyContext } from "~/lib/study-context"; - -export default function ExperimentsRedirect() { - const router = useRouter(); - const { selectedStudyId } = useStudyContext(); - - useEffect(() => { - // If user has a selected study, redirect to study experiments - if (selectedStudyId) { - router.replace(`/studies/${selectedStudyId}/experiments`); - } - }, [selectedStudyId, router]); - - return ( -
- - -
- -
- Experiments Moved - - Experiment management is now organized by study for better - workflow organization. - -
- -
-

To manage experiments:

-
    -
  • • Select a study from your studies list
  • -
  • • Navigate to that study's experiments page
  • -
  • • Create and manage experiment protocols for that specific study
  • -
-
-
- - -
-
-
-
- ); -} diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx old mode 100644 new mode 100755 diff --git a/src/app/(dashboard)/nao-test/page.tsx b/src/app/(dashboard)/nao-test/page.tsx new file mode 100755 index 0000000..6896ccf --- /dev/null +++ b/src/app/(dashboard)/nao-test/page.tsx @@ -0,0 +1,607 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Textarea } from "~/components/ui/textarea"; +import { Badge } from "~/components/ui/badge"; +import { Separator } from "~/components/ui/separator"; +import { Slider } from "~/components/ui/slider"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { Alert, AlertDescription } from "~/components/ui/alert"; +import { PageHeader } from "~/components/ui/page-header"; +import { PageLayout } from "~/components/ui/page-layout"; +import { + Play, + Square, + Volume2, + Camera, + Zap, + ArrowUp, + ArrowDown, + ArrowLeft, + ArrowRight, + RotateCcw, + RotateCw, + Wifi, + WifiOff, + AlertTriangle, + CheckCircle, + Activity, + Battery, + Eye, + Hand, + Footprints, +} from "lucide-react"; + +interface RosMessage { + topic: string; + msg: any; + type: string; +} + +export default function NaoTestPage() { + const [connectionStatus, setConnectionStatus] = useState< + "disconnected" | "connecting" | "connected" | "error" + >("disconnected"); + const [rosSocket, setRosSocket] = useState(null); + const [robotStatus, setRobotStatus] = useState(null); + const [jointStates, setJointStates] = useState(null); + const [speechText, setSpeechText] = useState(""); + const [walkSpeed, setWalkSpeed] = useState([0.1]); + const [turnSpeed, setTurnSpeed] = useState([0.3]); + const [headYaw, setHeadYaw] = useState([0]); + const [headPitch, setHeadPitch] = useState([0]); + const [logs, setLogs] = useState([]); + const [sensorData, setSensorData] = useState({}); + const logsEndRef = useRef(null); + + const ROS_BRIDGE_URL = + process.env.NEXT_PUBLIC_ROS_BRIDGE_URL || "ws://localhost:9090"; + + const addLog = (message: string) => { + const timestamp = new Date().toLocaleTimeString(); + setLogs((prev) => [...prev.slice(-49), `[${timestamp}] ${message}`]); + }; + + useEffect(() => { + logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [logs]); + + const connectToRos = () => { + if (rosSocket?.readyState === WebSocket.OPEN) return; + + setConnectionStatus("connecting"); + addLog("Connecting to ROS bridge..."); + + const socket = new WebSocket(ROS_BRIDGE_URL); + + socket.onopen = () => { + setConnectionStatus("connected"); + setRosSocket(socket); + addLog("Connected to ROS bridge successfully"); + + // Subscribe to robot topics + subscribeToTopics(socket); + }; + + socket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + handleRosMessage(data); + } catch (error) { + console.error("Error parsing ROS message:", error); + } + }; + + socket.onclose = () => { + setConnectionStatus("disconnected"); + setRosSocket(null); + addLog("Disconnected from ROS bridge"); + }; + + socket.onerror = () => { + setConnectionStatus("error"); + addLog("Error connecting to ROS bridge"); + }; + }; + + const disconnectFromRos = () => { + if (rosSocket) { + rosSocket.close(); + setRosSocket(null); + setConnectionStatus("disconnected"); + addLog("Manually disconnected from ROS bridge"); + } + }; + + const subscribeToTopics = (socket: WebSocket) => { + const topics = [ + { topic: "/naoqi_driver/joint_states", type: "sensor_msgs/JointState" }, + { topic: "/naoqi_driver/info", type: "naoqi_bridge_msgs/StringStamped" }, + { topic: "/naoqi_driver/bumper", type: "naoqi_bridge_msgs/Bumper" }, + { + topic: "/naoqi_driver/hand_touch", + type: "naoqi_bridge_msgs/HandTouch", + }, + { + topic: "/naoqi_driver/head_touch", + type: "naoqi_bridge_msgs/HeadTouch", + }, + { topic: "/naoqi_driver/sonar/left", type: "sensor_msgs/Range" }, + { topic: "/naoqi_driver/sonar/right", type: "sensor_msgs/Range" }, + ]; + + topics.forEach(({ topic, type }) => { + const subscribeMsg = { + op: "subscribe", + topic, + type, + }; + socket.send(JSON.stringify(subscribeMsg)); + addLog(`Subscribed to ${topic}`); + }); + }; + + const handleRosMessage = (data: any) => { + if (data.topic === "/naoqi_driver/joint_states") { + setJointStates(data.msg); + } else if (data.topic === "/naoqi_driver/info") { + setRobotStatus(data.msg); + } else if ( + data.topic?.includes("bumper") || + data.topic?.includes("touch") || + data.topic?.includes("sonar") + ) { + setSensorData((prev) => ({ + ...prev, + [data.topic]: data.msg, + })); + } + }; + + const publishMessage = (topic: string, type: string, msg: any) => { + if (!rosSocket || rosSocket.readyState !== WebSocket.OPEN) { + addLog("Error: Not connected to ROS bridge"); + return; + } + + const rosMsg = { + op: "publish", + topic, + type, + msg, + }; + + rosSocket.send(JSON.stringify(rosMsg)); + addLog(`Published to ${topic}: ${JSON.stringify(msg)}`); + }; + + const sayText = () => { + if (!speechText.trim()) return; + + publishMessage("/speech", "std_msgs/String", { + data: speechText, + }); + setSpeechText(""); + }; + + const walkForward = () => { + publishMessage("/cmd_vel", "geometry_msgs/Twist", { + linear: { x: walkSpeed[0], y: 0, z: 0 }, + angular: { x: 0, y: 0, z: 0 }, + }); + }; + + const walkBackward = () => { + publishMessage("/cmd_vel", "geometry_msgs/Twist", { + linear: { x: -walkSpeed[0], y: 0, z: 0 }, + angular: { x: 0, y: 0, z: 0 }, + }); + }; + + const turnLeft = () => { + publishMessage("/cmd_vel", "geometry_msgs/Twist", { + linear: { x: 0, y: 0, z: 0 }, + angular: { x: 0, y: 0, z: turnSpeed[0] }, + }); + }; + + const turnRight = () => { + publishMessage("/cmd_vel", "geometry_msgs/Twist", { + linear: { x: 0, y: 0, z: 0 }, + angular: { x: 0, y: 0, z: -turnSpeed[0] }, + }); + }; + + const stopMovement = () => { + publishMessage("/cmd_vel", "geometry_msgs/Twist", { + linear: { x: 0, y: 0, z: 0 }, + angular: { x: 0, y: 0, z: 0 }, + }); + }; + + const moveHead = () => { + publishMessage("/joint_angles", "naoqi_bridge_msgs/JointAnglesWithSpeed", { + joint_names: ["HeadYaw", "HeadPitch"], + joint_angles: [headYaw[0], headPitch[0]], + speed: 0.3, + }); + }; + + const getConnectionStatusIcon = () => { + switch (connectionStatus) { + case "connected": + return ; + case "connecting": + return ; + case "error": + return ; + default: + return ; + } + }; + + const getConnectionStatusBadge = () => { + const variants = { + connected: "default", + connecting: "secondary", + error: "destructive", + disconnected: "outline", + } as const; + + return ( + + {getConnectionStatusIcon()} + {connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1)} + + ); + }; + + return ( + + + +
+ {/* Connection Status */} + + + + ROS Bridge Connection + {getConnectionStatusBadge()} + + + Connect to ROS bridge at {ROS_BRIDGE_URL} + + + +
+ {connectionStatus === "connected" ? ( + + ) : ( + + )} +
+
+
+ + {connectionStatus === "connected" && ( + + + Robot Control + Sensor Data + Robot Status + Logs + + + +
+ {/* Speech Control */} + + + + + Speech + + + +
+ +