Update for new HRIStudio build
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
docs/
|
||||||
247
.github/workflows/validate.yml
vendored
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
name: Validate Plugins
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main", "develop"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm install -g ajv-cli
|
||||||
|
npm install -g jsonlint
|
||||||
|
|
||||||
|
- name: Validate JSON syntax
|
||||||
|
run: |
|
||||||
|
echo "Validating repository.json..."
|
||||||
|
jsonlint repository.json
|
||||||
|
|
||||||
|
echo "Validating plugins/index.json..."
|
||||||
|
jsonlint plugins/index.json
|
||||||
|
|
||||||
|
echo "Validating plugin files..."
|
||||||
|
for file in plugins/*.json; do
|
||||||
|
if [ "$file" != "plugins/index.json" ]; then
|
||||||
|
echo "Validating $file..."
|
||||||
|
jsonlint "$file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Check plugin index consistency
|
||||||
|
run: |
|
||||||
|
echo "Checking plugin index consistency..."
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
|
||||||
|
const files = fs.readdirSync('plugins').filter(f => f.endsWith('.json') && f !== 'index.json');
|
||||||
|
|
||||||
|
console.log('Index files:', index);
|
||||||
|
console.log('Actual files:', files);
|
||||||
|
|
||||||
|
const missing = files.filter(f => !index.includes(f));
|
||||||
|
const extra = index.filter(f => !files.includes(f));
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error('Files missing from index:', missing);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extra.length > 0) {
|
||||||
|
console.error('Extra files in index:', extra);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Plugin index is consistent ✓');
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Validate plugin schemas
|
||||||
|
run: |
|
||||||
|
echo "Validating plugin schemas..."
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Basic plugin schema validation
|
||||||
|
function validatePlugin(pluginPath) {
|
||||||
|
const plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
// Required fields
|
||||||
|
const required = ['robotId', 'name', 'platform', 'version', 'pluginApiVersion', 'hriStudioVersion', 'trustLevel', 'category'];
|
||||||
|
for (const field of required) {
|
||||||
|
if (!plugin[field]) {
|
||||||
|
errors.push(\`Missing required field: \${field}\`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate trustLevel enum
|
||||||
|
if (plugin.trustLevel && !['official', 'verified', 'community'].includes(plugin.trustLevel)) {
|
||||||
|
errors.push(\`Invalid trustLevel: \${plugin.trustLevel}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate actions
|
||||||
|
if (plugin.actions && Array.isArray(plugin.actions)) {
|
||||||
|
plugin.actions.forEach((action, i) => {
|
||||||
|
if (!action.id) errors.push(\`Action \${i}: missing id\`);
|
||||||
|
if (!action.name) errors.push(\`Action \${i}: missing name\`);
|
||||||
|
if (!action.category) errors.push(\`Action \${i}: missing category\`);
|
||||||
|
if (action.category && !['movement', 'interaction', 'sensors', 'logic'].includes(action.category)) {
|
||||||
|
errors.push(\`Action \${i}: invalid category \${action.category}\`);
|
||||||
|
}
|
||||||
|
if (!action.parameterSchema) errors.push(\`Action \${i}: missing parameterSchema\`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
for (const pluginFile of index) {
|
||||||
|
console.log(\`Validating \${pluginFile}...\`);
|
||||||
|
const errors = validatePlugin(\`plugins/\${pluginFile}\`);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(\`Errors in \${pluginFile}:\`);
|
||||||
|
errors.forEach(error => console.error(\` - \${error}\`));
|
||||||
|
hasErrors = true;
|
||||||
|
} else {
|
||||||
|
console.log(\` ✓ Valid\`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All plugins are valid ✓');
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Check asset references
|
||||||
|
run: |
|
||||||
|
echo "Checking asset references..."
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function checkAssets(pluginPath) {
|
||||||
|
const plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (plugin.assets) {
|
||||||
|
const checkAsset = (assetPath, description) => {
|
||||||
|
if (assetPath && assetPath.startsWith('assets/')) {
|
||||||
|
const fullPath = assetPath;
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
errors.push(\`\${description}: \${assetPath} not found\`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAsset(plugin.assets.thumbnailUrl, 'Thumbnail');
|
||||||
|
|
||||||
|
if (plugin.assets.images) {
|
||||||
|
checkAsset(plugin.assets.images.main, 'Main image');
|
||||||
|
checkAsset(plugin.assets.images.logo, 'Logo');
|
||||||
|
|
||||||
|
if (plugin.assets.images.angles) {
|
||||||
|
Object.entries(plugin.assets.images.angles).forEach(([angle, path]) => {
|
||||||
|
checkAsset(path, \`\${angle} angle image\`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
for (const pluginFile of index) {
|
||||||
|
console.log(\`Checking assets for \${pluginFile}...\`);
|
||||||
|
const errors = checkAssets(\`plugins/\${pluginFile}\`);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(\`Asset errors in \${pluginFile}:\`);
|
||||||
|
errors.forEach(error => console.error(\` - \${error}\`));
|
||||||
|
hasErrors = true;
|
||||||
|
} else {
|
||||||
|
console.log(\` ✓ All assets found\`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.warn('Some assets are missing - this may cause display issues');
|
||||||
|
} else {
|
||||||
|
console.log('All assets are available ✓');
|
||||||
|
}
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Validate repository metadata
|
||||||
|
run: |
|
||||||
|
echo "Validating repository metadata..."
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const repo = JSON.parse(fs.readFileSync('repository.json', 'utf8'));
|
||||||
|
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
|
||||||
|
|
||||||
|
// Check plugin count matches
|
||||||
|
const actualCount = index.length;
|
||||||
|
const reportedCount = repo.stats?.plugins || 0;
|
||||||
|
|
||||||
|
if (actualCount !== reportedCount) {
|
||||||
|
console.error(\`Plugin count mismatch: reported \${reportedCount}, actual \${actualCount}\`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(\`Plugin count is correct: \${actualCount} ✓\`);
|
||||||
|
|
||||||
|
// Check required repository fields
|
||||||
|
const required = ['id', 'name', 'apiVersion', 'pluginApiVersion', 'trust'];
|
||||||
|
for (const field of required) {
|
||||||
|
if (!repo[field]) {
|
||||||
|
console.error(\`Missing required repository field: \${field}\`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Repository metadata is valid ✓');
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Test web interface
|
||||||
|
run: |
|
||||||
|
echo "Testing web interface..."
|
||||||
|
# Start a simple HTTP server
|
||||||
|
python3 -m http.server 8000 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Wait for server to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test that index.html loads
|
||||||
|
curl -f http://localhost:8000/index.html > /dev/null
|
||||||
|
|
||||||
|
# Test that repository.json is accessible
|
||||||
|
curl -f http://localhost:8000/repository.json > /dev/null
|
||||||
|
|
||||||
|
# Test that plugins/index.json is accessible
|
||||||
|
curl -f http://localhost:8000/plugins/index.json > /dev/null
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
kill $SERVER_PID
|
||||||
|
|
||||||
|
echo "Web interface test passed ✓"
|
||||||
333
README.md
@@ -1,92 +1,275 @@
|
|||||||
# HRIStudio Robot Plugins Repository
|
# HRIStudio Robot Plugins Repository
|
||||||
|
|
||||||
This repository contains robot plugins for use with HRIStudio. Each plugin provides a standardized interface for controlling and interacting with different types of robots.
|
Official collection of robot plugins for the HRIStudio platform, providing standardized interfaces for controlling and interacting with different types of robots in Human-Robot Interaction research.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This repository contains robot plugins that enable HRIStudio to work with various robot platforms including mobile robots, humanoid robots, manipulators, and drones. Each plugin provides a standardized interface for robot control, sensor data collection, and experiment execution.
|
||||||
|
|
||||||
|
## Available Plugins
|
||||||
|
|
||||||
|
### Mobile Robots
|
||||||
|
- **TurtleBot3 Burger** (`turtlebot3-burger`) - Compact educational robot platform
|
||||||
|
- **TurtleBot3 Waffle** (`turtlebot3-waffle`) - Extended TurtleBot3 with camera and additional sensors
|
||||||
|
|
||||||
|
### Humanoid Robots
|
||||||
|
- **NAO Humanoid** (`nao-humanoid`) - SoftBank Robotics NAO for social interaction research
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Using Plugins in HRIStudio
|
||||||
|
|
||||||
|
1. **Add Repository**: In HRIStudio Admin panel, add this repository URL
|
||||||
|
2. **Install Plugins**: Browse and install plugins for your study
|
||||||
|
3. **Design Experiments**: Use plugin actions in the experiment designer
|
||||||
|
4. **Run Trials**: Execute experiments with real-time robot control
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/soconnor0919/robot-plugins.git
|
||||||
|
cd robot-plugins
|
||||||
|
|
||||||
|
# Install development dependencies (optional)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
./validate.sh serve
|
||||||
|
|
||||||
|
# Validate all plugins
|
||||||
|
./validate.sh validate
|
||||||
|
|
||||||
|
# Create a new plugin
|
||||||
|
./validate.sh create my-robot
|
||||||
|
```
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
repository.json # Repository metadata and configuration
|
robot-plugins/
|
||||||
index.html # Web interface for viewing repository information
|
├── repository.json # Repository metadata
|
||||||
plugins/ # Directory containing all plugin files
|
├── index.html # Web interface
|
||||||
index.json # List of available plugins
|
├── plugins/ # Plugin definitions
|
||||||
plugin1.json # Individual plugin definition
|
│ ├── index.json # Plugin list
|
||||||
plugin2.json # Individual plugin definition
|
│ ├── turtlebot3-burger.json
|
||||||
...
|
│ ├── turtlebot3-waffle.json
|
||||||
assets/ # Optional directory for repository assets
|
│ └── nao-humanoid.json
|
||||||
repository-icon.png # Repository icon
|
├── assets/ # Visual assets
|
||||||
repository-logo.png # Repository logo
|
│ ├── repository-*.png # Repository branding
|
||||||
repository-banner.png # Repository banner
|
│ ├── turtlebot3-burger/ # Robot images
|
||||||
|
│ ├── turtlebot3-waffle/
|
||||||
|
│ └── nao-humanoid/
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── schema.md # Plugin schema reference
|
||||||
|
│ └── plugins.md # Plugin development guide
|
||||||
|
├── scripts/ # Development tools
|
||||||
|
│ └── validate-plugin.js # Plugin validator
|
||||||
|
└── .github/workflows/ # CI/CD pipelines
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Development
|
||||||
|
|
||||||
|
### Creating a New Plugin
|
||||||
|
|
||||||
|
1. **Generate Template**:
|
||||||
|
```bash
|
||||||
|
./validate.sh create my-robot
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Edit Plugin Definition**: Update `plugins/my-robot.json` with robot details
|
||||||
|
|
||||||
|
3. **Add Assets**: Place robot images in `assets/my-robot/`
|
||||||
|
|
||||||
|
4. **Validate Plugin**:
|
||||||
|
```bash
|
||||||
|
./validate.sh validate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Update Index**:
|
||||||
|
```bash
|
||||||
|
./validate.sh update-index
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Schema
|
||||||
|
|
||||||
|
Each plugin must include:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"robotId": "unique-robot-id",
|
||||||
|
"name": "Robot Display Name",
|
||||||
|
"platform": "ROS2|NAOqi|Custom",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"pluginApiVersion": "1.0",
|
||||||
|
"hriStudioVersion": ">=0.1.0",
|
||||||
|
"trustLevel": "official|verified|community",
|
||||||
|
"category": "mobile-robot|humanoid-robot|manipulator|drone",
|
||||||
|
"actions": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Action Definitions
|
||||||
|
|
||||||
|
Actions define robot operations available in experiments:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "action_name",
|
||||||
|
"name": "Action Display Name",
|
||||||
|
"category": "movement|interaction|sensors|logic",
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {...},
|
||||||
|
"required": [...]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/Twist",
|
||||||
|
"topic": "/cmd_vel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Tools
|
||||||
|
|
||||||
|
### Validation Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate all plugins and repository
|
||||||
|
./validate.sh validate
|
||||||
|
|
||||||
|
# Run full test suite
|
||||||
|
./validate.sh test
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
./validate.sh build
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
./validate.sh serve [port]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate specific plugin
|
||||||
|
node scripts/validate-plugin.js validate plugins/my-robot.json
|
||||||
|
|
||||||
|
# Validate all plugins
|
||||||
|
npm run validate
|
||||||
|
|
||||||
|
# Update plugin index
|
||||||
|
npm run update-index
|
||||||
|
|
||||||
|
# Show repository statistics
|
||||||
|
npm run stats
|
||||||
```
|
```
|
||||||
|
|
||||||
## Web Interface
|
## Web Interface
|
||||||
|
|
||||||
The repository includes a built-in web interface (`index.html`) that provides a user-friendly way to view repository information. When hosting your repository, this interface will automatically:
|
The repository includes a built-in web interface accessible at the repository URL. It provides:
|
||||||
|
|
||||||
- Display repository name, description, and metadata
|
- Repository information and statistics
|
||||||
- Show repository statistics (plugin count)
|
- Plugin catalog with search and filtering
|
||||||
- List author information and compatibility details
|
- Individual plugin details and documentation
|
||||||
- Display repository tags and categories
|
- Asset preview and download links
|
||||||
- Show repository assets (icon, banner, logo)
|
- Installation instructions for HRIStudio
|
||||||
|
|
||||||
The web interface is automatically available when you host your repository, making it easy for users to browse repository information before adding it to HRIStudio.
|
|
||||||
|
|
||||||
## Repository Configuration
|
|
||||||
|
|
||||||
The `repository.json` file contains the repository's metadata and configuration:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "unique-repository-id",
|
|
||||||
"name": "Repository Name",
|
|
||||||
"description": "Repository description",
|
|
||||||
"urls": {
|
|
||||||
"repository": "https://example.com/repository",
|
|
||||||
"git": "https://github.com/user/repo.git"
|
|
||||||
},
|
|
||||||
"official": false,
|
|
||||||
"trust": "community",
|
|
||||||
"author": {
|
|
||||||
"name": "Author Name",
|
|
||||||
"organization": "Organization Name",
|
|
||||||
"url": "https://example.com"
|
|
||||||
},
|
|
||||||
"compatibility": {
|
|
||||||
"hristudio": {
|
|
||||||
"min": "1.0.0",
|
|
||||||
"recommended": "1.1.0"
|
|
||||||
},
|
|
||||||
"ros2": {
|
|
||||||
"distributions": ["humble", "iron"],
|
|
||||||
"recommended": "iron"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"assets": {
|
|
||||||
"icon": "assets/repository-icon.png",
|
|
||||||
"banner": "assets/repository-banner.png",
|
|
||||||
"logo": "assets/repository-logo.png"
|
|
||||||
},
|
|
||||||
"stats": {
|
|
||||||
"plugins": 0
|
|
||||||
},
|
|
||||||
"tags": ["robots", "simulation", "education"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plugin Structure
|
|
||||||
|
|
||||||
Each plugin is defined in a JSON file within the `plugins` directory. The `plugins/index.json` file contains a list of all available plugin files.
|
|
||||||
|
|
||||||
For detailed information about plugin structure and requirements, see the [Plugin Documentation](docs/plugins.md).
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork or clone this repository
|
### Adding a Plugin
|
||||||
2. Create your plugin branch
|
|
||||||
3. Add your plugin JSON file to the `plugins` directory
|
1. **Fork** this repository
|
||||||
4. Update `plugins/index.json` to include your plugin
|
2. **Create** your plugin using the template
|
||||||
5. Test your changes locally
|
3. **Add** comprehensive robot assets
|
||||||
6. Submit your changes
|
4. **Validate** your plugin thoroughly
|
||||||
|
5. **Submit** a pull request
|
||||||
|
|
||||||
|
### Plugin Requirements
|
||||||
|
|
||||||
|
- Valid JSON syntax and schema compliance
|
||||||
|
- Complete action definitions with parameter schemas
|
||||||
|
- High-quality robot images (thumbnail, main, angles)
|
||||||
|
- Accurate robot specifications
|
||||||
|
- Working communication protocol configuration
|
||||||
|
|
||||||
|
### Review Process
|
||||||
|
|
||||||
|
All plugins undergo review for:
|
||||||
|
- Technical correctness
|
||||||
|
- Schema compliance
|
||||||
|
- Asset quality
|
||||||
|
- Documentation completeness
|
||||||
|
- Security considerations
|
||||||
|
|
||||||
|
## Integration with HRIStudio
|
||||||
|
|
||||||
|
### Repository Registration
|
||||||
|
|
||||||
|
Administrators can add this repository in HRIStudio:
|
||||||
|
|
||||||
|
1. Navigate to **Admin > Plugin Repositories**
|
||||||
|
2. Add repository URL: `https://repo.hristudio.com`
|
||||||
|
3. Set trust level and enable synchronization
|
||||||
|
4. Plugins become available for installation
|
||||||
|
|
||||||
|
### Study Installation
|
||||||
|
|
||||||
|
Researchers can install plugins for studies:
|
||||||
|
|
||||||
|
1. Go to **Study > Plugins**
|
||||||
|
2. Browse available plugins from registered repositories
|
||||||
|
3. Install required plugins for your research
|
||||||
|
4. Configure plugin settings as needed
|
||||||
|
|
||||||
|
### Experiment Design
|
||||||
|
|
||||||
|
Plugin actions appear in the experiment designer:
|
||||||
|
|
||||||
|
1. Drag actions from the **Block Library**
|
||||||
|
2. Configure parameters in the **Properties Panel**
|
||||||
|
3. Connect actions to create experiment flow
|
||||||
|
4. Test and validate your protocol
|
||||||
|
|
||||||
|
### Trial Execution
|
||||||
|
|
||||||
|
During live trials:
|
||||||
|
|
||||||
|
1. HRIStudio establishes robot connections
|
||||||
|
2. Wizard controls actions in real-time
|
||||||
|
3. All robot commands are logged
|
||||||
|
4. Sensor data is captured automatically
|
||||||
|
|
||||||
|
## API Compatibility
|
||||||
|
|
||||||
|
This repository supports:
|
||||||
|
- **Plugin API Version**: 1.0
|
||||||
|
- **HRIStudio Version**: 0.1.0+
|
||||||
|
- **Schema Version**: Latest
|
||||||
|
|
||||||
|
## Trust Levels
|
||||||
|
|
||||||
|
Plugins are classified by trust level:
|
||||||
|
|
||||||
|
- **Official**: Maintained by HRIStudio team or robot manufacturers
|
||||||
|
- **Verified**: Third-party plugins reviewed and tested
|
||||||
|
- **Community**: User-contributed plugins (use with caution)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Documentation**: [Plugin Development Guide](docs/plugins.md)
|
||||||
|
- **Schema Reference**: [Schema Documentation](docs/schema.md)
|
||||||
|
- **Issues**: [GitHub Issues](https://github.com/soconnor0919/robot-plugins/issues)
|
||||||
|
- **Email**: support@hristudio.com
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This repository is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
This repository is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
Individual plugins may have different licenses - please check each plugin's documentation.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- ROBOTIS for TurtleBot3 platform support
|
||||||
|
- SoftBank Robotics for NAO platform documentation
|
||||||
|
- ROS2 community for standardized messaging
|
||||||
|
- HRIStudio research community for feedback and testing
|
||||||
BIN
assets/nao-humanoid/back.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/nao-humanoid/front.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/nao-humanoid/logo.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/nao-humanoid/main.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/nao-humanoid/side.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/nao-humanoid/thumb.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
139
assets/style.css
@@ -793,13 +793,18 @@ img {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plugin-details-header {
|
.plugin-details-header {
|
||||||
padding: 1.5rem;
|
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plugin-details-header-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.plugin-details-icon {
|
.plugin-details-icon {
|
||||||
width: 4rem;
|
width: 5rem;
|
||||||
height: 4rem;
|
height: 5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: calc(var(--radius) - 0.25rem);
|
border-radius: calc(var(--radius) - 0.25rem);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -811,7 +816,24 @@ img {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
padding: 0.5rem;
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.plugin-details-header-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-details-icon {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-details-header .flex-wrap {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Plugin Images */
|
/* Plugin Images */
|
||||||
@@ -1077,4 +1099,113 @@ code {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image Zoom Modal */
|
||||||
|
.zoom-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: hsl(var(--background) / 0.8);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-modal[data-state="open"] {
|
||||||
|
display: flex;
|
||||||
|
animation: modal-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-modal-content {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-modal-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-modal-close:hover {
|
||||||
|
background: hsl(var(--accent) / 0.1);
|
||||||
|
color: hsl(var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update image gallery styles */
|
||||||
|
.image-gallery {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-item {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
cursor: zoom-in;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-item:hover {
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
assets/turtlebot3-waffle/front.png
Normal file
|
After Width: | Height: | Size: 555 KiB |
BIN
assets/turtlebot3-waffle/logo.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/turtlebot3-waffle/main.jpg
Normal file
|
After Width: | Height: | Size: 621 KiB |
BIN
assets/turtlebot3-waffle/side.png
Normal file
|
After Width: | Height: | Size: 579 KiB |
BIN
assets/turtlebot3-waffle/thumb.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/turtlebot3-waffle/top.png
Normal file
|
After Width: | Height: | Size: 808 KiB |
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- .:/usr/share/nginx/html:ro
|
||||||
|
environment:
|
||||||
|
- NGINX_HOST=localhost
|
||||||
|
- NGINX_PORT=80
|
||||||
|
command: >
|
||||||
|
/bin/sh -c "nginx -g 'daemon off;'"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
340
docs/plugins.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# HRIStudio Robot Plugins
|
||||||
|
|
||||||
|
This document explains how robot plugins work in HRIStudio and provides guidance for plugin developers.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
HRIStudio uses a plugin-based architecture to support different robot platforms. Each plugin defines:
|
||||||
|
|
||||||
|
- Robot capabilities and specifications
|
||||||
|
- Available actions for experiment design
|
||||||
|
- ROS2 integration details
|
||||||
|
- Communication protocols
|
||||||
|
|
||||||
|
## Plugin Structure
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **Robot Definition**: Basic information about the robot platform
|
||||||
|
2. **Action Library**: Operations that can be performed during experiments
|
||||||
|
3. **ROS2 Integration**: Message types, topics, and communication setup
|
||||||
|
4. **Assets**: Images, models, and visual resources
|
||||||
|
|
||||||
|
### Plugin Lifecycle
|
||||||
|
|
||||||
|
1. **Repository Registration**: Plugin repositories are added to HRIStudio
|
||||||
|
2. **Plugin Discovery**: Available plugins are fetched from repositories
|
||||||
|
3. **Study Installation**: Plugins are installed for specific studies
|
||||||
|
4. **Experiment Integration**: Actions become available in the experiment designer
|
||||||
|
5. **Trial Execution**: Actions are executed during live trials
|
||||||
|
|
||||||
|
## Action System
|
||||||
|
|
||||||
|
### Action Types
|
||||||
|
|
||||||
|
Actions are the building blocks of experiments. HRIStudio supports four main categories:
|
||||||
|
|
||||||
|
#### Movement Actions
|
||||||
|
- Robot locomotion and positioning
|
||||||
|
- Navigation and path planning
|
||||||
|
- Velocity control and stopping
|
||||||
|
|
||||||
|
#### Interaction Actions
|
||||||
|
- Speech synthesis and audio playback
|
||||||
|
- Gesture and animation control
|
||||||
|
- Display and lighting effects
|
||||||
|
|
||||||
|
#### Sensor Actions
|
||||||
|
- Data collection from sensors
|
||||||
|
- Environmental monitoring
|
||||||
|
- State queries and feedback
|
||||||
|
|
||||||
|
#### Logic Actions
|
||||||
|
- Conditional operations
|
||||||
|
- Loops and iteration
|
||||||
|
- Variable manipulation
|
||||||
|
|
||||||
|
### Parameter Schema
|
||||||
|
|
||||||
|
Each action defines parameters using JSON Schema format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"speed": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 0.5,
|
||||||
|
"description": "Movement speed as fraction of maximum"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["speed"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ROS2 Integration
|
||||||
|
|
||||||
|
Actions can integrate with ROS2 through:
|
||||||
|
|
||||||
|
- **Topics**: Publishing messages for robot control
|
||||||
|
- **Services**: Synchronous request/response operations
|
||||||
|
- **Actions**: Long-running operations with feedback
|
||||||
|
|
||||||
|
## Plugin Development
|
||||||
|
|
||||||
|
### Basic Plugin Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"robotId": "my-robot",
|
||||||
|
"name": "My Robot",
|
||||||
|
"description": "Custom robot for research",
|
||||||
|
"platform": "ROS2",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"pluginApiVersion": "1.0",
|
||||||
|
"hriStudioVersion": ">=0.1.0",
|
||||||
|
"trustLevel": "community",
|
||||||
|
"category": "mobile-robot",
|
||||||
|
|
||||||
|
"manufacturer": {
|
||||||
|
"name": "Research Lab",
|
||||||
|
"website": "https://example.com"
|
||||||
|
},
|
||||||
|
|
||||||
|
"assets": {
|
||||||
|
"thumbnailUrl": "assets/my-robot/thumb.png"
|
||||||
|
},
|
||||||
|
|
||||||
|
"actions": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Actions
|
||||||
|
|
||||||
|
Each action needs:
|
||||||
|
|
||||||
|
1. **Unique ID**: Snake_case identifier
|
||||||
|
2. **Clear Name**: Human-readable title
|
||||||
|
3. **Category**: One of the four main types
|
||||||
|
4. **Parameters**: JSON Schema definition
|
||||||
|
5. **ROS2 Config**: Communication details
|
||||||
|
|
||||||
|
Example action:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "move_forward",
|
||||||
|
"name": "Move Forward",
|
||||||
|
"description": "Move the robot forward by a specified distance",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "arrow-up",
|
||||||
|
"timeout": 30000,
|
||||||
|
"retryable": true,
|
||||||
|
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"distance": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 5.0,
|
||||||
|
"default": 1.0,
|
||||||
|
"description": "Distance to move in meters"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["distance"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/Twist",
|
||||||
|
"topic": "/cmd_vel",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "transformToTwist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Asset Management
|
||||||
|
|
||||||
|
Plugins should include visual assets:
|
||||||
|
|
||||||
|
- **Thumbnail**: 200x150px preview image
|
||||||
|
- **Main Image**: High-resolution robot photo
|
||||||
|
- **Angle Views**: Front, side, top perspectives
|
||||||
|
- **Logo**: Manufacturer or robot series logo
|
||||||
|
|
||||||
|
Assets are served relative to the repository URL.
|
||||||
|
|
||||||
|
### Testing Plugins
|
||||||
|
|
||||||
|
Before publishing, test your plugin:
|
||||||
|
|
||||||
|
1. Validate JSON syntax and schema
|
||||||
|
2. Verify all asset URLs are accessible
|
||||||
|
3. Test ROS2 message formats
|
||||||
|
4. Check parameter validation
|
||||||
|
5. Ensure timeout values are reasonable
|
||||||
|
|
||||||
|
## Integration with HRIStudio
|
||||||
|
|
||||||
|
### Repository Management
|
||||||
|
|
||||||
|
Administrators can add plugin repositories in HRIStudio:
|
||||||
|
|
||||||
|
1. Navigate to Admin > Plugin Repositories
|
||||||
|
2. Add repository URL
|
||||||
|
3. Set trust level and enable sync
|
||||||
|
4. Plugins become available for installation
|
||||||
|
|
||||||
|
### Study-Level Installation
|
||||||
|
|
||||||
|
Researchers install plugins for specific studies:
|
||||||
|
|
||||||
|
1. Go to Study > Plugins
|
||||||
|
2. Browse available plugins
|
||||||
|
3. Install required plugins
|
||||||
|
4. Configure plugin settings
|
||||||
|
|
||||||
|
### Experiment Design
|
||||||
|
|
||||||
|
Installed plugin actions appear in the experiment designer:
|
||||||
|
|
||||||
|
1. Drag actions from the block library
|
||||||
|
2. Configure parameters in the properties panel
|
||||||
|
3. Connect actions to create experiment flow
|
||||||
|
4. Test and validate the experiment protocol
|
||||||
|
|
||||||
|
### Trial Execution
|
||||||
|
|
||||||
|
During trials, HRIStudio:
|
||||||
|
|
||||||
|
1. Establishes ROS2 connections
|
||||||
|
2. Validates action parameters
|
||||||
|
3. Sends commands to robots
|
||||||
|
4. Monitors execution status
|
||||||
|
5. Logs all events for analysis
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Plugin Design
|
||||||
|
|
||||||
|
- Use clear, descriptive action names
|
||||||
|
- Provide comprehensive parameter validation
|
||||||
|
- Include helpful descriptions for all parameters
|
||||||
|
- Choose appropriate timeout values
|
||||||
|
- Make actions atomic and focused
|
||||||
|
|
||||||
|
### ROS2 Integration
|
||||||
|
|
||||||
|
- Follow ROS2 naming conventions
|
||||||
|
- Use appropriate QoS settings
|
||||||
|
- Handle connection failures gracefully
|
||||||
|
- Implement proper error reporting
|
||||||
|
- Document message format requirements
|
||||||
|
|
||||||
|
### Asset Management
|
||||||
|
|
||||||
|
- Use consistent image dimensions
|
||||||
|
- Optimize file sizes for web delivery
|
||||||
|
- Provide high-quality thumbnails
|
||||||
|
- Include multiple viewing angles
|
||||||
|
- Ensure assets load quickly
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Document all action behaviors
|
||||||
|
- Provide usage examples
|
||||||
|
- Explain parameter effects
|
||||||
|
- Include troubleshooting tips
|
||||||
|
- Maintain version compatibility notes
|
||||||
|
|
||||||
|
## Repository Hosting
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
repository/
|
||||||
|
├── repository.json # Repository metadata
|
||||||
|
├── index.html # Web interface
|
||||||
|
├── plugins/
|
||||||
|
│ ├── index.json # Plugin list
|
||||||
|
│ └── robot-name.json # Individual plugins
|
||||||
|
└── assets/
|
||||||
|
├── repository-icon.png
|
||||||
|
└── robot-name/
|
||||||
|
├── thumb.png
|
||||||
|
├── main.jpg
|
||||||
|
└── angles/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hosting Requirements
|
||||||
|
|
||||||
|
- HTTPS enabled
|
||||||
|
- CORS headers configured
|
||||||
|
- Static file serving
|
||||||
|
- Reliable uptime
|
||||||
|
- Fast response times
|
||||||
|
|
||||||
|
### Version Management
|
||||||
|
|
||||||
|
- Use semantic versioning
|
||||||
|
- Maintain backward compatibility
|
||||||
|
- Document breaking changes
|
||||||
|
- Provide migration guides
|
||||||
|
- Archive old versions
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Trust Levels
|
||||||
|
|
||||||
|
- **Official**: Signed and verified plugins
|
||||||
|
- **Verified**: Community plugins that passed review
|
||||||
|
- **Community**: User-contributed, use with caution
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
- All plugins are validated against schema
|
||||||
|
- Parameters are sanitized before execution
|
||||||
|
- ROS2 messages are type-checked
|
||||||
|
- Network communications are monitored
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
- Plugins run with limited permissions
|
||||||
|
- Robot access is study-scoped
|
||||||
|
- Actions can be disabled by administrators
|
||||||
|
- Audit logs track all plugin activities
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Plugin Not Loading**: Check JSON syntax and schema compliance
|
||||||
|
2. **Assets Not Found**: Verify asset URLs and file paths
|
||||||
|
3. **ROS2 Connection Failed**: Check topic names and message types
|
||||||
|
4. **Action Timeout**: Increase timeout or check robot connectivity
|
||||||
|
5. **Parameter Validation**: Ensure all required parameters are provided
|
||||||
|
|
||||||
|
### Debug Tools
|
||||||
|
|
||||||
|
- Use browser developer tools for network issues
|
||||||
|
- Check HRIStudio logs for plugin errors
|
||||||
|
- Validate JSON files with online tools
|
||||||
|
- Test ROS2 connections independently
|
||||||
|
- Monitor robot topics and services
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To contribute to the official plugin repository:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a new plugin file
|
||||||
|
3. Add assets and documentation
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
For questions or support, contact the HRIStudio development team.
|
||||||
290
docs/schema.md
@@ -1,227 +1,243 @@
|
|||||||
# Plugin Schema Documentation
|
# HRIStudio Plugin Schema Documentation
|
||||||
|
|
||||||
This document describes the schema for HRIStudio robot plugins.
|
This document describes the schema for HRIStudio robot plugins, including both repository metadata and individual plugin definitions.
|
||||||
|
|
||||||
## Repository Metadata
|
## Repository Metadata (`repository.json`)
|
||||||
|
|
||||||
The repository itself is defined by a `repository.json` file with the following structure:
|
The repository metadata file defines the plugin repository and its capabilities:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": string,
|
"id": "string (required) - Unique repository identifier",
|
||||||
"name": string,
|
"name": "string (required) - Display name",
|
||||||
"description": string?,
|
"description": "string (optional) - Repository description",
|
||||||
|
"apiVersion": "string (required) - Repository API version",
|
||||||
|
"pluginApiVersion": "string (required) - Plugin API version supported",
|
||||||
"urls": {
|
"urls": {
|
||||||
"repository": string (URL),
|
"repository": "string (URL, required) - Repository base URL",
|
||||||
"git": string (URL)?
|
"git": "string (URL, optional) - Git repository URL"
|
||||||
},
|
},
|
||||||
"official": boolean,
|
"official": "boolean (required) - Whether this is an official repository",
|
||||||
|
"trust": "string (enum: official|verified|community, required)",
|
||||||
"author": {
|
"author": {
|
||||||
"name": string,
|
"name": "string (required)",
|
||||||
"email": string (email)?,
|
"email": "string (email, optional)",
|
||||||
"url": string (URL)?,
|
"url": "string (URL, optional)",
|
||||||
"organization": string?
|
"organization": "string (optional)"
|
||||||
},
|
},
|
||||||
"maintainers": [
|
"maintainers": [
|
||||||
{
|
{
|
||||||
"name": string,
|
"name": "string (required)",
|
||||||
"email": string (email)?,
|
"email": "string (email, optional)",
|
||||||
"url": string (URL)?
|
"url": "string (URL, optional)"
|
||||||
}
|
}
|
||||||
]?,
|
],
|
||||||
"homepage": string (URL)?,
|
"homepage": "string (URL, optional)",
|
||||||
"license": string,
|
"license": "string (required) - License identifier",
|
||||||
"defaultBranch": string,
|
"defaultBranch": "string (required) - Default Git branch",
|
||||||
"lastUpdated": string (ISO date),
|
"lastUpdated": "string (ISO date, required)",
|
||||||
"trust": "official" | "verified" | "community",
|
"categories": [
|
||||||
"assets": {
|
{
|
||||||
"icon": string?,
|
"id": "string (required) - Category identifier",
|
||||||
"logo": string?,
|
"name": "string (required) - Display name",
|
||||||
"banner": string?
|
"description": "string (required) - Category description"
|
||||||
},
|
}
|
||||||
|
],
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"hristudio": {
|
"hristudio": {
|
||||||
"min": string,
|
"min": "string (semver, required) - Minimum HRIStudio version",
|
||||||
"recommended": string?
|
"recommended": "string (semver, optional) - Recommended version"
|
||||||
},
|
},
|
||||||
"ros2": {
|
"ros2": {
|
||||||
"distributions": string[],
|
"distributions": "string[] (optional) - Supported ROS2 distributions",
|
||||||
"recommended": string?
|
"recommended": "string (optional) - Recommended distribution"
|
||||||
}?
|
}
|
||||||
},
|
},
|
||||||
"tags": string[],
|
"assets": {
|
||||||
|
"icon": "string (path, optional) - Repository icon",
|
||||||
|
"logo": "string (path, optional) - Repository logo",
|
||||||
|
"banner": "string (path, optional) - Repository banner"
|
||||||
|
},
|
||||||
|
"tags": "string[] (required) - Repository tags",
|
||||||
"stats": {
|
"stats": {
|
||||||
"plugins": number
|
"plugins": "number (required) - Number of plugins"
|
||||||
}?
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Plugin Schema
|
## Plugin Schema
|
||||||
|
|
||||||
Each plugin is defined in a JSON file with the following top-level structure:
|
Each plugin is defined in a JSON file with HRIStudio-specific extensions:
|
||||||
|
|
||||||
|
### Core Properties
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"robotId": string,
|
"robotId": "string (required) - Unique robot identifier",
|
||||||
"name": string,
|
"name": "string (required) - Display name",
|
||||||
"description": string?,
|
"description": "string (optional) - Robot description",
|
||||||
"platform": string,
|
"platform": "string (required) - Robot platform (e.g., 'ROS2')",
|
||||||
"version": string,
|
"version": "string (required) - Plugin version (semver)",
|
||||||
"manufacturer": object,
|
"pluginApiVersion": "string (required) - Plugin API version",
|
||||||
"documentation": object,
|
"hriStudioVersion": "string (required) - Minimum HRIStudio version",
|
||||||
"assets": object,
|
"trustLevel": "string (enum: official|verified|community, required)",
|
||||||
"specs": object,
|
"category": "string (required) - Plugin category identifier"
|
||||||
"ros2Config": object,
|
|
||||||
"actions": array
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Properties
|
### Manufacturer Information
|
||||||
|
|
||||||
### Required Properties
|
|
||||||
|
|
||||||
- `robotId`: Unique identifier for the robot (e.g., "turtlebot3-burger")
|
|
||||||
- `name`: Display name of the robot
|
|
||||||
- `platform`: Robot platform/framework (e.g., "ROS2")
|
|
||||||
- `version`: Plugin version (semver format)
|
|
||||||
|
|
||||||
### Optional Properties
|
|
||||||
|
|
||||||
- `description`: Detailed description of the robot
|
|
||||||
|
|
||||||
## Manufacturer Information
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"manufacturer": {
|
"manufacturer": {
|
||||||
"name": string,
|
"name": "string (required)",
|
||||||
"website": string (URL)?,
|
"website": "string (URL, optional)",
|
||||||
"support": string (URL)?
|
"support": "string (URL, optional)"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation Links
|
### Documentation Links
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"mainUrl": string (URL),
|
"mainUrl": "string (URL, required)",
|
||||||
"apiReference": string (URL)?,
|
"apiReference": "string (URL, optional)",
|
||||||
"wikiUrl": string (URL)?,
|
"wikiUrl": "string (URL, optional)",
|
||||||
"videoUrl": string (URL)?
|
"videoUrl": "string (URL, optional)"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Assets Configuration
|
### Assets Configuration
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"assets": {
|
"assets": {
|
||||||
"thumbnailUrl": string,
|
"thumbnailUrl": "string (path, required)",
|
||||||
"logo": string?,
|
|
||||||
"images": {
|
"images": {
|
||||||
"main": string,
|
"main": "string (path, required)",
|
||||||
"angles": {
|
"angles": {
|
||||||
"front": string?,
|
"front": "string (path, optional)",
|
||||||
"side": string?,
|
"side": "string (path, optional)",
|
||||||
"top": string?
|
"top": "string (path, optional)"
|
||||||
}
|
},
|
||||||
|
"logo": "string (path, optional)"
|
||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"format": "URDF" | "glTF" | "other",
|
"format": "string (enum: URDF|glTF|STL, optional)",
|
||||||
"url": string (URL)
|
"url": "string (URL, optional)"
|
||||||
}?
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Robot Specifications
|
### Robot Specifications
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"specs": {
|
"specs": {
|
||||||
"dimensions": {
|
"dimensions": {
|
||||||
"length": number,
|
"length": "number (meters, required)",
|
||||||
"width": number,
|
"width": "number (meters, required)",
|
||||||
"height": number,
|
"height": "number (meters, required)",
|
||||||
"weight": number
|
"weight": "number (kg, required)"
|
||||||
},
|
},
|
||||||
"capabilities": string[],
|
"capabilities": "string[] (required) - List of robot capabilities",
|
||||||
"maxSpeed": number,
|
"maxSpeed": "number (m/s, optional)",
|
||||||
"batteryLife": number
|
"batteryLife": "number (hours, optional)"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## ROS2 Configuration
|
### ROS2 Configuration
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"ros2Config": {
|
"ros2Config": {
|
||||||
"namespace": string,
|
"namespace": "string (required) - Default ROS2 namespace",
|
||||||
"nodePrefix": string,
|
"nodePrefix": "string (required) - Node name prefix",
|
||||||
"defaultTopics": {
|
"defaultTopics": {
|
||||||
[key: string]: string
|
"[topicName]": "string (topic path) - Default topic mappings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Actions
|
## Action Definitions
|
||||||
|
|
||||||
Each action in the `actions` array follows this structure:
|
Actions define the operations that can be performed with the robot. Each action follows this HRIStudio-specific schema:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"actionId": string,
|
"id": "string (required) - Unique action identifier (snake_case)",
|
||||||
"type": "move" | "speak" | "wait" | "input" | "gesture" | "record" | "condition" | "loop",
|
"name": "string (required) - Display name for UI",
|
||||||
"title": string,
|
"description": "string (optional) - Action description",
|
||||||
"description": string,
|
"category": "string (enum: movement|interaction|sensors|logic, required)",
|
||||||
"icon": string?,
|
"icon": "string (optional) - Lucide icon name",
|
||||||
"parameters": {
|
"timeout": "number (milliseconds, optional) - Default timeout",
|
||||||
"type": "object",
|
"retryable": "boolean (optional) - Whether action can be retried on failure",
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object (required)",
|
||||||
"properties": {
|
"properties": {
|
||||||
[key: string]: {
|
"[paramName]": {
|
||||||
"type": string,
|
"type": "string (JSON Schema type, required)",
|
||||||
"title": string,
|
"minimum": "number (optional) - For numeric types",
|
||||||
"description": string?,
|
"maximum": "number (optional) - For numeric types",
|
||||||
"default": any?,
|
"default": "any (optional) - Default value",
|
||||||
"minimum": number?,
|
"description": "string (required) - Parameter description",
|
||||||
"maximum": number?,
|
"enum": "string[] (optional) - For enum types"
|
||||||
"enum": string[]?,
|
|
||||||
"unit": string?
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": string[]
|
"required": "string[] (required) - List of required parameters"
|
||||||
},
|
},
|
||||||
"ros2": {
|
"ros2": {
|
||||||
"messageType": string,
|
"messageType": "string (required) - ROS2 message type",
|
||||||
"topic": string?,
|
"topic": "string (optional) - Topic name for publishers",
|
||||||
"service": string?,
|
"service": "string (optional) - Service name for service calls",
|
||||||
"action": string?,
|
"action": "string (optional) - Action name for action calls",
|
||||||
"payloadMapping": {
|
"payloadMapping": {
|
||||||
"type": "direct" | "transform",
|
"type": "string (enum: transform|static, required)",
|
||||||
"map": object?,
|
"transformFn": "string (optional) - Transform function name",
|
||||||
"transformFn": string?
|
"payload": "object (optional) - Static payload for static type"
|
||||||
},
|
},
|
||||||
"qos": {
|
"qos": {
|
||||||
"reliability": "reliable" | "best_effort",
|
"reliability": "string (enum: reliable|best_effort, optional)",
|
||||||
"durability": "volatile" | "transient_local",
|
"durability": "string (enum: volatile|transient_local, optional)",
|
||||||
"history": "keep_last" | "keep_all",
|
"history": "string (enum: keep_last|keep_all, optional)",
|
||||||
"depth": number?
|
"depth": "number (optional) - Queue depth for keep_last"
|
||||||
}?
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## QoS Settings
|
## Action Categories
|
||||||
|
|
||||||
When specifying ROS2 QoS settings:
|
HRIStudio organizes actions into these standard categories:
|
||||||
|
|
||||||
- `reliability`: Message delivery guarantee
|
- **movement**: Robot locomotion and positioning
|
||||||
- `reliable`: Guaranteed delivery
|
- **interaction**: Communication and social behaviors
|
||||||
- `best_effort`: Fast but may drop messages
|
- **sensors**: Data collection and environmental sensing
|
||||||
|
- **logic**: Control flow and decision making
|
||||||
|
|
||||||
- `durability`: Message persistence
|
## Trust Levels
|
||||||
- `volatile`: Only delivered to active subscribers
|
|
||||||
- `transient_local`: Stored for late-joining subscribers
|
|
||||||
|
|
||||||
- `history`: Message queue behavior
|
Plugins are classified by trust level:
|
||||||
- `keep_last`: Store up to N messages (specify with depth)
|
|
||||||
- `keep_all`: Store all messages
|
|
||||||
|
|
||||||
## Example
|
- **official**: Maintained by HRIStudio team or robot manufacturer
|
||||||
|
- **verified**: Third-party plugins that have been reviewed and tested
|
||||||
|
- **community**: User-contributed plugins without formal verification
|
||||||
|
|
||||||
See the TurtleBot3 Burger plugin for a complete example implementation.
|
## Validation Requirements
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
All plugins must include:
|
||||||
|
- Core properties (robotId, name, platform, version)
|
||||||
|
- HRIStudio metadata (pluginApiVersion, hriStudioVersion, trustLevel, category)
|
||||||
|
- At least one action definition
|
||||||
|
- Valid manufacturer information
|
||||||
|
- Asset thumbnailUrl
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- `robotId`: lowercase with hyphens (e.g., "turtlebot3-burger")
|
||||||
|
- `action.id`: snake_case (e.g., "move_velocity")
|
||||||
|
- `category`: predefined enum values only
|
||||||
|
|
||||||
|
### Version Requirements
|
||||||
|
- Plugin version must follow semantic versioning (semver)
|
||||||
|
- HRIStudio version must use semver range syntax (e.g., ">=0.1.0")
|
||||||
|
|
||||||
|
## Example Implementation
|
||||||
|
|
||||||
|
See `plugins/turtlebot3-burger.json` for a complete reference implementation demonstrating all schema features and best practices.
|
||||||
269
index.html
@@ -196,120 +196,100 @@
|
|||||||
<!-- Plugin Details -->
|
<!-- Plugin Details -->
|
||||||
<div class="plugin-details">
|
<div class="plugin-details">
|
||||||
<div id="pluginDetails" class="hidden">
|
<div id="pluginDetails" class="hidden">
|
||||||
|
<!-- Header -->
|
||||||
<div class="plugin-details-header">
|
<div class="plugin-details-header">
|
||||||
<div class="mb-4 flex items-start justify-between">
|
<div class="plugin-details-header-content">
|
||||||
<div class="flex items-center gap-4">
|
<div class="plugin-details-icon">
|
||||||
<div class="plugin-details-icon">
|
<img id="detailsIcon" alt="" class="plugin-icon">
|
||||||
<img id="detailsIcon" alt="" class="plugin-icon">
|
</div>
|
||||||
</div>
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex-1 min-w-0">
|
<h3 id="detailsTitle" class="text-xl font-semibold">Plugin Title</h3>
|
||||||
<h3 id="detailsTitle" class="text-xl font-semibold"></h3>
|
<p id="detailsDescription" class="mt-1 text-muted-foreground">Plugin description goes here.</p>
|
||||||
<p id="detailsDescription" class="mt-1 text-muted-foreground"></p>
|
<div class="flex flex-wrap items-center gap-4 mt-4 text-sm">
|
||||||
<div class="mt-4 flex items-center gap-2">
|
<div class="flex items-center gap-1.5">
|
||||||
<span id="detailsSpeed" class="badge badge-secondary"></span>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
|
||||||
<span id="detailsBattery" class="badge badge-secondary"></span>
|
<path d="m5 8 6 6"></path>
|
||||||
<span id="detailsWeight" class="badge badge-secondary"></span>
|
<path d="m4 14 2-2-2-2"></path>
|
||||||
|
<path d="M2 14h4"></path>
|
||||||
|
<path d="M19 8v8"></path>
|
||||||
|
<path d="M22 8h-6"></path>
|
||||||
|
</svg>
|
||||||
|
<span id="detailsSpeed"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
|
||||||
|
<path d="M3 7v10c0 2 1 3 3 3h12"></path>
|
||||||
|
<path d="M6 10h14"></path>
|
||||||
|
<path d="M6 14h14"></path>
|
||||||
|
<path d="M3 3h18"></path>
|
||||||
|
</svg>
|
||||||
|
<span id="detailsBattery"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
|
||||||
|
<path d="M12 20v-8"></path>
|
||||||
|
<path d="M18 20V4"></path>
|
||||||
|
<path d="M6 20v-4"></path>
|
||||||
|
</svg>
|
||||||
|
<span id="detailsWeight"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<a id="detailsDocsButton" href="#" target="_blank" rel="noopener noreferrer" class="button button-outline">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
|
||||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
|
||||||
</svg>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-6">
|
<!-- Tabs -->
|
||||||
<div class="plugin-details-tabs">
|
<div class="plugin-details-tabs">
|
||||||
<div class="plugin-details-tabs-list" role="tablist">
|
<div class="plugin-details-tabs-list" role="tablist">
|
||||||
<button class="plugin-details-tab" role="tab" aria-selected="true" data-state="active" data-plugin-tab="plugin-overview">Overview</button>
|
<button class="plugin-details-tab" role="tab" aria-selected="true" data-state="active" data-plugin-tab="overview">Overview</button>
|
||||||
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="plugin-specs">Specifications</button>
|
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="specifications">Specifications</button>
|
||||||
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="plugin-actions">Actions</button>
|
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="actions">Actions</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="plugin-details-tab-content" data-state="active" role="tabpanel" data-plugin-tab="plugin-overview">
|
<!-- Overview Tab -->
|
||||||
<div id="detailsImages" class="relative mb-6">
|
<div class="plugin-details-tab-content" data-state="active" role="tabpanel" data-plugin-tab="overview">
|
||||||
<!-- Images will be loaded here -->
|
<div class="content-section">
|
||||||
</div>
|
<h3>Robot Images</h3>
|
||||||
<div class="card-secondary">
|
<div class="image-gallery" id="detailsImages"></div>
|
||||||
<h4>Documentation</h4>
|
</div>
|
||||||
<div class="grid gap-2 text-sm">
|
<div class="content-section">
|
||||||
<a id="detailsMainDocs" href="#" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline">
|
<h3>Documentation</h3>
|
||||||
User Manual
|
<div class="grid gap-2 text-sm">
|
||||||
</a>
|
<a
|
||||||
<a id="detailsApiDocs" href="#" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline hidden">
|
id="detailsMainDocs"
|
||||||
API Reference
|
href="#"
|
||||||
</a>
|
target="_blank"
|
||||||
</div>
|
rel="noopener noreferrer"
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
User Manual
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
id="detailsApiDocs"
|
||||||
|
href="#"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-primary hover:underline hidden"
|
||||||
|
>
|
||||||
|
API Reference
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="plugin-specs">
|
<!-- Specifications Tab -->
|
||||||
<div class="space-y-6">
|
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="specifications">
|
||||||
<div class="card-secondary">
|
<div class="content-section">
|
||||||
<h4>Physical Specifications</h4>
|
<h3>Robot Images</h3>
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="image-gallery" id="specsImageGallery"></div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M19 21l2-2v-6"/><path d="M12 3v18"/><path d="m5 21-2-2v-6"/>
|
|
||||||
<path d="M3 7V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v2"/>
|
|
||||||
<path d="M3 17v2a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-2"/>
|
|
||||||
</svg>
|
|
||||||
<span id="detailsDimensions" class="text-sm"></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="m5 8 6 6"/><path d="m4 14 6 6 10-10-6-6-10 10z"/>
|
|
||||||
</svg>
|
|
||||||
<span id="detailsSpeedFull" class="text-sm"></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>
|
|
||||||
</svg>
|
|
||||||
<span id="detailsBatteryFull" class="text-sm"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-secondary">
|
|
||||||
<h4>Capabilities</h4>
|
|
||||||
<div id="detailsCapabilities" class="flex flex-wrap gap-2">
|
|
||||||
<!-- Capabilities will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-secondary">
|
|
||||||
<h4>ROS 2 Configuration</h4>
|
|
||||||
<div class="grid gap-3 text-sm">
|
|
||||||
<div>
|
|
||||||
<span class="text-muted-foreground">Namespace: </span>
|
|
||||||
<code id="detailsNamespace" class="rounded bg-muted px-1.5 py-0.5"></code>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-muted-foreground">Node Prefix: </span>
|
|
||||||
<code id="detailsNodePrefix" class="rounded bg-muted px-1.5 py-0.5"></code>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<span class="text-muted-foreground">Default Topics:</span>
|
|
||||||
<div id="detailsTopics" class="pl-4">
|
|
||||||
<!-- Topics will be loaded here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="plugin-actions">
|
<!-- Actions Tab -->
|
||||||
<div id="detailsActions" class="space-y-4">
|
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="actions">
|
||||||
<!-- Actions will be loaded here -->
|
<div class="content-section">
|
||||||
</div>
|
<h3>Actions Tab</h3>
|
||||||
|
<p>Actions content will go here.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,6 +317,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Zoom Modal -->
|
||||||
|
<div class="zoom-modal" id="imageZoomModal" aria-modal="true" role="dialog">
|
||||||
|
<div class="zoom-modal-content">
|
||||||
|
<img id="zoomImage" class="zoom-modal-image" alt="Zoomed image">
|
||||||
|
<button class="zoom-modal-close" id="closeZoomModal" aria-label="Close modal">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 6 6 18"></path>
|
||||||
|
<path d="m6 6 12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Separate tab management for root and plugin details
|
// Separate tab management for root and plugin details
|
||||||
document.querySelectorAll('.tab').forEach(tab => {
|
document.querySelectorAll('.tab').forEach(tab => {
|
||||||
@@ -552,8 +545,11 @@
|
|||||||
actionsContainer.appendChild(div);
|
actionsContainer.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update specifications tab with image gallery
|
||||||
|
updateSpecsImageGallery(plugin);
|
||||||
|
|
||||||
// Trigger click on the Overview tab to show it by default
|
// Trigger click on the Overview tab to show it by default
|
||||||
document.querySelector('.plugin-details-tabs-list .plugin-details-tab[data-plugin-tab="plugin-overview"]').click();
|
document.querySelector('.plugin-details-tabs-list .plugin-details-tab[data-plugin-tab="overview"]').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
pluginList.appendChild(card);
|
pluginList.appendChild(card);
|
||||||
@@ -656,6 +652,75 @@
|
|||||||
|
|
||||||
// Load data when page loads
|
// Load data when page loads
|
||||||
loadRepositoryData();
|
loadRepositoryData();
|
||||||
|
|
||||||
|
// Image zoom functionality
|
||||||
|
const modal = document.getElementById('imageZoomModal');
|
||||||
|
const zoomImage = document.getElementById('zoomImage');
|
||||||
|
const closeButton = document.getElementById('closeZoomModal');
|
||||||
|
|
||||||
|
function openImageModal(imageUrl, altText) {
|
||||||
|
zoomImage.src = imageUrl;
|
||||||
|
zoomImage.alt = altText;
|
||||||
|
modal.setAttribute('data-state', 'open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeImageModal() {
|
||||||
|
modal.removeAttribute('data-state');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside the image
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
closeImageModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal with escape key
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && modal.hasAttribute('data-state')) {
|
||||||
|
closeImageModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButton.addEventListener('click', closeImageModal);
|
||||||
|
|
||||||
|
// Update the click handler to include the image gallery
|
||||||
|
function updateSpecsImageGallery(plugin) {
|
||||||
|
const gallery = document.getElementById('specsImageGallery');
|
||||||
|
gallery.innerHTML = '';
|
||||||
|
|
||||||
|
// Add main image
|
||||||
|
const mainImageItem = document.createElement('div');
|
||||||
|
mainImageItem.className = 'image-gallery-item';
|
||||||
|
mainImageItem.innerHTML = `
|
||||||
|
<img src="${plugin.assets.images.main}" alt="${plugin.name} main view">
|
||||||
|
<div class="image-gallery-label">Main View</div>
|
||||||
|
`;
|
||||||
|
mainImageItem.addEventListener('click', () => {
|
||||||
|
openImageModal(plugin.assets.images.main, `${plugin.name} main view`);
|
||||||
|
});
|
||||||
|
gallery.appendChild(mainImageItem);
|
||||||
|
|
||||||
|
// Add angle images
|
||||||
|
if (plugin.assets.images.angles) {
|
||||||
|
Object.entries(plugin.assets.images.angles)
|
||||||
|
.filter(([_, url]) => url)
|
||||||
|
.forEach(([angle, url]) => {
|
||||||
|
const angleImageItem = document.createElement('div');
|
||||||
|
angleImageItem.className = 'image-gallery-item';
|
||||||
|
angleImageItem.innerHTML = `
|
||||||
|
<img src="${url}" alt="${plugin.name} ${angle} view">
|
||||||
|
<div class="image-gallery-label">${angle} View</div>
|
||||||
|
`;
|
||||||
|
angleImageItem.addEventListener('click', () => {
|
||||||
|
openImageModal(url, `${plugin.name} ${angle} view`);
|
||||||
|
});
|
||||||
|
gallery.appendChild(angleImageItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
62
package.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "hristudio-robot-plugins",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Official HRIStudio robot plugins repository",
|
||||||
|
"main": "index.html",
|
||||||
|
"scripts": {
|
||||||
|
"validate": "node scripts/validate-plugin.js validate-all",
|
||||||
|
"update-index": "node scripts/validate-plugin.js update-index",
|
||||||
|
"serve": "python3 -m http.server 8080",
|
||||||
|
"test": "./validate.sh test",
|
||||||
|
"build": "./validate.sh build",
|
||||||
|
"lint": "eslint scripts/*.js",
|
||||||
|
"format": "prettier --write '**/*.{json,md,js}'",
|
||||||
|
"check-format": "prettier --check '**/*.{json,md,js}'",
|
||||||
|
"validate-json": "find . -name '*.json' -not -path './node_modules/*' | xargs -I {} sh -c 'echo \"Validating {}\" && node -e \"JSON.parse(require(\\\"fs\\\").readFileSync(\\\"{}\\\", \\\"utf8\\\"))\"'",
|
||||||
|
"optimize": "find . -name '*.json' -not -path './node_modules/*' | xargs -I {} node -e 'const fs=require(\"fs\");const data=JSON.parse(fs.readFileSync(\"{}\",\"utf8\"));fs.writeFileSync(\"{}\",JSON.stringify(data,null,2));console.log(\"Optimized {}\");'",
|
||||||
|
"stats": "node -e 'const fs=require(\"fs\");const index=JSON.parse(fs.readFileSync(\"plugins/index.json\",\"utf8\"));console.log(`Repository contains ${index.length} plugins`);index.forEach(f=>{const p=JSON.parse(fs.readFileSync(`plugins/${f}`,\"utf8\"));console.log(`- ${p.name} (${p.robotId}) - ${p.actions.length} actions`)});'"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/soconnor0919/robot-plugins.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"robotics",
|
||||||
|
"ros2",
|
||||||
|
"hri",
|
||||||
|
"wizard-of-oz",
|
||||||
|
"research",
|
||||||
|
"plugins"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "HRIStudio Team",
|
||||||
|
"email": "support@hristudio.com",
|
||||||
|
"url": "https://hristudio.com"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://repo.hristudio.com",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/soconnor0919/robot-plugins/issues"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ajv": "^8.12.0",
|
||||||
|
"ajv-cli": "^5.0.0",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"prettier": "^3.1.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"repository.json",
|
||||||
|
"index.html",
|
||||||
|
"plugins/",
|
||||||
|
"assets/",
|
||||||
|
"docs/",
|
||||||
|
"scripts/",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
44
plugins/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Robot Plugins
|
||||||
|
|
||||||
|
This directory contains individual robot plugin definitions for the HRIStudio platform.
|
||||||
|
|
||||||
|
## Available Plugins
|
||||||
|
|
||||||
|
### Mobile Robots
|
||||||
|
|
||||||
|
- **turtlebot3-burger.json** - Compact educational robot platform
|
||||||
|
- **turtlebot3-waffle.json** - Extended TurtleBot3 with camera and additional sensors
|
||||||
|
|
||||||
|
### Humanoid Robots
|
||||||
|
|
||||||
|
- **nao-humanoid.json** - NAO humanoid robot for social interaction research
|
||||||
|
|
||||||
|
## Plugin Structure
|
||||||
|
|
||||||
|
Each plugin file defines:
|
||||||
|
|
||||||
|
- Robot specifications and capabilities
|
||||||
|
- Available actions for experiment design
|
||||||
|
- Communication protocol configuration
|
||||||
|
- Asset references for UI display
|
||||||
|
|
||||||
|
## Adding New Plugins
|
||||||
|
|
||||||
|
1. Create a new JSON file following the schema
|
||||||
|
2. Add robot assets to the `assets/` directory
|
||||||
|
3. Update `index.json` to include the new plugin
|
||||||
|
4. Test the plugin definition for validity
|
||||||
|
|
||||||
|
## Schema Validation
|
||||||
|
|
||||||
|
All plugins must conform to the HRIStudio plugin schema. See `../docs/schema.md` for complete documentation.
|
||||||
|
|
||||||
|
## Asset Requirements
|
||||||
|
|
||||||
|
Each plugin should include:
|
||||||
|
- Thumbnail image (200x150px)
|
||||||
|
- Main robot image
|
||||||
|
- Multiple angle views
|
||||||
|
- Manufacturer logo (optional)
|
||||||
|
|
||||||
|
Assets are served relative to the repository root URL.
|
||||||
@@ -1,3 +1 @@
|
|||||||
[
|
["turtlebot3-burger.json", "turtlebot3-waffle.json", "nao-humanoid.json"]
|
||||||
"turtlebot3-burger.json"
|
|
||||||
]
|
|
||||||
|
|||||||
315
plugins/nao-humanoid.json
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
{
|
||||||
|
"robotId": "nao-humanoid",
|
||||||
|
"name": "NAO Humanoid Robot",
|
||||||
|
"description": "Autonomous, programmable humanoid robot designed for education, research, and human-robot interaction studies",
|
||||||
|
"platform": "NAOqi",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"pluginApiVersion": "1.0",
|
||||||
|
"hriStudioVersion": ">=0.1.0",
|
||||||
|
"trustLevel": "verified",
|
||||||
|
"category": "humanoid-robot",
|
||||||
|
|
||||||
|
"manufacturer": {
|
||||||
|
"name": "SoftBank Robotics",
|
||||||
|
"website": "https://www.softbankrobotics.com/",
|
||||||
|
"support": "https://developer.softbankrobotics.com/"
|
||||||
|
},
|
||||||
|
|
||||||
|
"documentation": {
|
||||||
|
"mainUrl": "https://developer.softbankrobotics.com/nao6/nao-documentation",
|
||||||
|
"apiReference": "https://developer.softbankrobotics.com/nao6/naoqi-developer-guide",
|
||||||
|
"wikiUrl": "https://en.wikipedia.org/wiki/Nao_(robot)",
|
||||||
|
"videoUrl": "https://www.youtube.com/watch?v=nNbj2G3GmAg"
|
||||||
|
},
|
||||||
|
|
||||||
|
"assets": {
|
||||||
|
"thumbnailUrl": "assets/nao-humanoid/thumb.png",
|
||||||
|
"images": {
|
||||||
|
"main": "assets/nao-humanoid/main.jpg",
|
||||||
|
"angles": {
|
||||||
|
"front": "assets/nao-humanoid/front.png",
|
||||||
|
"side": "assets/nao-humanoid/side.png",
|
||||||
|
"back": "assets/nao-humanoid/back.png"
|
||||||
|
},
|
||||||
|
"logo": "assets/nao-humanoid/logo.png"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"format": "URDF",
|
||||||
|
"url": "https://github.com/ros-naoqi/nao_robot/raw/master/nao_description/urdf/nao.urdf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"specs": {
|
||||||
|
"dimensions": {
|
||||||
|
"length": 0.275,
|
||||||
|
"width": 0.311,
|
||||||
|
"height": 0.574,
|
||||||
|
"weight": 5.4
|
||||||
|
},
|
||||||
|
"capabilities": ["bipedal_walking", "speech_synthesis", "speech_recognition", "computer_vision", "gestures", "led_control"],
|
||||||
|
"maxSpeed": 0.55,
|
||||||
|
"batteryLife": 1.5
|
||||||
|
},
|
||||||
|
|
||||||
|
"naoqiConfig": {
|
||||||
|
"defaultIP": "nao.local",
|
||||||
|
"defaultPort": 9559,
|
||||||
|
"modules": ["ALMotion", "ALTextToSpeech", "ALSpeechRecognition", "ALLeds", "ALAnimationPlayer", "ALBehaviorManager"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "say_text",
|
||||||
|
"name": "Say Text",
|
||||||
|
"description": "Make the robot speak using text-to-speech",
|
||||||
|
"category": "interaction",
|
||||||
|
"icon": "volume-2",
|
||||||
|
"timeout": 15000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Hello, I am NAO!",
|
||||||
|
"description": "Text to speak"
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 0.7,
|
||||||
|
"description": "Speech volume (0.1 to 1.0)"
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 50,
|
||||||
|
"maximum": 400,
|
||||||
|
"default": 100,
|
||||||
|
"description": "Speech speed in words per minute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["text"]
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALTextToSpeech",
|
||||||
|
"method": "say",
|
||||||
|
"parameters": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "walk_to_position",
|
||||||
|
"name": "Walk to Position",
|
||||||
|
"description": "Walk to a specific position relative to current location",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "footprints",
|
||||||
|
"timeout": 30000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -2.0,
|
||||||
|
"maximum": 2.0,
|
||||||
|
"default": 0.5,
|
||||||
|
"description": "Forward distance in meters"
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -1.0,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 0.0,
|
||||||
|
"description": "Sideways distance in meters (left is positive)"
|
||||||
|
},
|
||||||
|
"theta": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -3.14159,
|
||||||
|
"maximum": 3.14159,
|
||||||
|
"default": 0.0,
|
||||||
|
"description": "Turn angle in radians"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["x", "y", "theta"]
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALMotion",
|
||||||
|
"method": "walkTo",
|
||||||
|
"parameters": ["x", "y", "theta"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "play_animation",
|
||||||
|
"name": "Play Animation",
|
||||||
|
"description": "Play a predefined animation or gesture",
|
||||||
|
"category": "interaction",
|
||||||
|
"icon": "zap",
|
||||||
|
"timeout": 20000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"animation": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["Hello", "Goodbye", "Excited", "Thinking", "Clap", "Dance", "BowShort", "WipeForehead"],
|
||||||
|
"default": "Hello",
|
||||||
|
"description": "Animation to play"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["animation"]
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALAnimationPlayer",
|
||||||
|
"method": "run",
|
||||||
|
"parameters": ["animations/Stand/Gestures/{animation}"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set_led_color",
|
||||||
|
"name": "Set LED Color",
|
||||||
|
"description": "Change the color of NAO's eye LEDs",
|
||||||
|
"category": "interaction",
|
||||||
|
"icon": "lightbulb",
|
||||||
|
"timeout": 5000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["red", "green", "blue", "yellow", "magenta", "cyan", "white", "orange", "pink"],
|
||||||
|
"default": "blue",
|
||||||
|
"description": "LED color"
|
||||||
|
},
|
||||||
|
"intensity": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 1.0,
|
||||||
|
"description": "LED brightness (0.0 to 1.0)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["color"]
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALLeds",
|
||||||
|
"method": "fadeRGB",
|
||||||
|
"parameters": ["FaceLeds", "color", "intensity", 1.0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sit_down",
|
||||||
|
"name": "Sit Down",
|
||||||
|
"description": "Make the robot sit down",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "minus-square",
|
||||||
|
"timeout": 10000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALMotion",
|
||||||
|
"method": "rest",
|
||||||
|
"parameters": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "stand_up",
|
||||||
|
"name": "Stand Up",
|
||||||
|
"description": "Make the robot stand up from sitting position",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "plus-square",
|
||||||
|
"timeout": 10000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALMotion",
|
||||||
|
"method": "wakeUp",
|
||||||
|
"parameters": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "listen_for_speech",
|
||||||
|
"name": "Listen for Speech",
|
||||||
|
"description": "Listen for specific words or phrases",
|
||||||
|
"category": "sensors",
|
||||||
|
"icon": "mic",
|
||||||
|
"timeout": 30000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vocabulary": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": ["yes", "no", "hello", "goodbye"],
|
||||||
|
"description": "Words to listen for"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.3,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 0.5,
|
||||||
|
"description": "Minimum confidence threshold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["vocabulary"]
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALSpeechRecognition",
|
||||||
|
"method": "setVocabulary",
|
||||||
|
"parameters": ["vocabulary", "confidence"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_head",
|
||||||
|
"name": "Turn Head",
|
||||||
|
"description": "Turn the robot's head to look in a specific direction",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "rotate-ccw",
|
||||||
|
"timeout": 5000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"yaw": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -2.0857,
|
||||||
|
"maximum": 2.0857,
|
||||||
|
"default": 0.0,
|
||||||
|
"description": "Head yaw angle in radians (left-right)"
|
||||||
|
},
|
||||||
|
"pitch": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -0.6720,
|
||||||
|
"maximum": 0.5149,
|
||||||
|
"default": 0.0,
|
||||||
|
"description": "Head pitch angle in radians (up-down)"
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 0.3,
|
||||||
|
"description": "Movement speed fraction"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["yaw", "pitch"]
|
||||||
|
},
|
||||||
|
"naoqi": {
|
||||||
|
"module": "ALMotion",
|
||||||
|
"method": "setAngles",
|
||||||
|
"parameters": [["HeadYaw", "HeadPitch"], ["yaw", "pitch"], "speed"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,7 +4,11 @@
|
|||||||
"description": "A compact, affordable, programmable, ROS2-based mobile robot for education and research",
|
"description": "A compact, affordable, programmable, ROS2-based mobile robot for education and research",
|
||||||
"platform": "ROS2",
|
"platform": "ROS2",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
"pluginApiVersion": "1.0",
|
||||||
|
"hriStudioVersion": ">=0.1.0",
|
||||||
|
"trustLevel": "official",
|
||||||
|
"category": "mobile-robot",
|
||||||
|
|
||||||
"manufacturer": {
|
"manufacturer": {
|
||||||
"name": "ROBOTIS",
|
"name": "ROBOTIS",
|
||||||
"website": "https://www.robotis.com/",
|
"website": "https://www.robotis.com/",
|
||||||
@@ -42,12 +46,7 @@
|
|||||||
"height": 0.192,
|
"height": 0.192,
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": ["differential_drive", "lidar", "imu", "odometry"],
|
||||||
"differential_drive",
|
|
||||||
"lidar",
|
|
||||||
"imu",
|
|
||||||
"odometry"
|
|
||||||
],
|
|
||||||
"maxSpeed": 0.22,
|
"maxSpeed": 0.22,
|
||||||
"batteryLife": 2.5
|
"batteryLife": 2.5
|
||||||
},
|
},
|
||||||
@@ -66,31 +65,29 @@
|
|||||||
|
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"actionId": "move-velocity",
|
"id": "move_velocity",
|
||||||
"type": "move",
|
"name": "Set Velocity",
|
||||||
"title": "Set Velocity",
|
|
||||||
"description": "Control the robot's linear and angular velocity",
|
"description": "Control the robot's linear and angular velocity",
|
||||||
|
"category": "movement",
|
||||||
"icon": "navigation",
|
"icon": "navigation",
|
||||||
"parameters": {
|
"timeout": 30000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"linear": {
|
"linear": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"title": "Linear Velocity",
|
|
||||||
"description": "Forward/backward velocity",
|
|
||||||
"default": 0,
|
|
||||||
"minimum": -0.22,
|
"minimum": -0.22,
|
||||||
"maximum": 0.22,
|
"maximum": 0.22,
|
||||||
"unit": "m/s"
|
"default": 0,
|
||||||
|
"description": "Forward/backward velocity in m/s"
|
||||||
},
|
},
|
||||||
"angular": {
|
"angular": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"title": "Angular Velocity",
|
|
||||||
"description": "Rotational velocity",
|
|
||||||
"default": 0,
|
|
||||||
"minimum": -2.84,
|
"minimum": -2.84,
|
||||||
"maximum": 2.84,
|
"maximum": 2.84,
|
||||||
"unit": "rad/s"
|
"default": 0,
|
||||||
|
"description": "Rotational velocity in rad/s"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["linear", "angular"]
|
"required": ["linear", "angular"]
|
||||||
@@ -111,34 +108,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"actionId": "move-to-pose",
|
"id": "move_to_pose",
|
||||||
"type": "move",
|
"name": "Move to Position",
|
||||||
"title": "Move to Position",
|
|
||||||
"description": "Navigate to a specific position on the map",
|
"description": "Navigate to a specific position on the map",
|
||||||
|
"category": "movement",
|
||||||
"icon": "target",
|
"icon": "target",
|
||||||
"parameters": {
|
"timeout": 60000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"x": {
|
"x": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"title": "X Position",
|
|
||||||
"description": "X coordinate in meters",
|
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"unit": "m"
|
"description": "X coordinate in meters"
|
||||||
},
|
},
|
||||||
"y": {
|
"y": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"title": "Y Position",
|
|
||||||
"description": "Y coordinate in meters",
|
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"unit": "m"
|
"description": "Y coordinate in meters"
|
||||||
},
|
},
|
||||||
"theta": {
|
"theta": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"title": "Orientation",
|
|
||||||
"description": "Final orientation",
|
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"unit": "rad"
|
"description": "Final orientation in radians"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["x", "y", "theta"]
|
"required": ["x", "y", "theta"]
|
||||||
@@ -151,6 +144,37 @@
|
|||||||
"transformFn": "transformToPoseStamped"
|
"transformFn": "transformToPoseStamped"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "stop_robot",
|
||||||
|
"name": "Stop Robot",
|
||||||
|
"description": "Immediately stop all robot movement",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "square",
|
||||||
|
"timeout": 5000,
|
||||||
|
"retryable": false,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/Twist",
|
||||||
|
"topic": "/cmd_vel",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "static",
|
||||||
|
"payload": {
|
||||||
|
"linear": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
||||||
|
"angular": { "x": 0.0, "y": 0.0, "z": 0.0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qos": {
|
||||||
|
"reliability": "reliable",
|
||||||
|
"durability": "volatile",
|
||||||
|
"history": "keep_last",
|
||||||
|
"depth": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
367
plugins/turtlebot3-waffle.json
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
{
|
||||||
|
"robotId": "turtlebot3-waffle",
|
||||||
|
"name": "TurtleBot3 Waffle",
|
||||||
|
"description": "Extended TurtleBot3 platform with additional sensors and computing power for advanced research applications",
|
||||||
|
"platform": "ROS2",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"pluginApiVersion": "1.0",
|
||||||
|
"hriStudioVersion": ">=0.1.0",
|
||||||
|
"trustLevel": "official",
|
||||||
|
"category": "mobile-robot",
|
||||||
|
|
||||||
|
"manufacturer": {
|
||||||
|
"name": "ROBOTIS",
|
||||||
|
"website": "https://www.robotis.com/",
|
||||||
|
"support": "https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/"
|
||||||
|
},
|
||||||
|
|
||||||
|
"documentation": {
|
||||||
|
"mainUrl": "https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/",
|
||||||
|
"apiReference": "https://emanual.robotis.com/docs/en/platform/turtlebot3/ros2_manipulation/",
|
||||||
|
"wikiUrl": "https://wiki.ros.org/turtlebot3",
|
||||||
|
"videoUrl": "https://www.youtube.com/watch?v=rVM994ZhsEM"
|
||||||
|
},
|
||||||
|
|
||||||
|
"assets": {
|
||||||
|
"thumbnailUrl": "assets/turtlebot3-waffle/thumb.png",
|
||||||
|
"images": {
|
||||||
|
"main": "assets/turtlebot3-waffle/main.jpg",
|
||||||
|
"angles": {
|
||||||
|
"front": "assets/turtlebot3-waffle/front.png",
|
||||||
|
"side": "assets/turtlebot3-waffle/side.png",
|
||||||
|
"top": "assets/turtlebot3-waffle/top.png"
|
||||||
|
},
|
||||||
|
"logo": "assets/turtlebot3-waffle/logo.png"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"format": "URDF",
|
||||||
|
"url": "https://raw.githubusercontent.com/ROBOTIS-GIT/turtlebot3/master/turtlebot3_description/urdf/turtlebot3_waffle.urdf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"specs": {
|
||||||
|
"dimensions": {
|
||||||
|
"length": 0.281,
|
||||||
|
"width": 0.306,
|
||||||
|
"height": 0.141,
|
||||||
|
"weight": 1.8
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"differential_drive",
|
||||||
|
"lidar",
|
||||||
|
"imu",
|
||||||
|
"odometry",
|
||||||
|
"camera",
|
||||||
|
"manipulation"
|
||||||
|
],
|
||||||
|
"maxSpeed": 0.26,
|
||||||
|
"batteryLife": 2.8
|
||||||
|
},
|
||||||
|
|
||||||
|
"ros2Config": {
|
||||||
|
"namespace": "turtlebot3",
|
||||||
|
"nodePrefix": "hri_studio",
|
||||||
|
"defaultTopics": {
|
||||||
|
"cmd_vel": "/cmd_vel",
|
||||||
|
"odom": "/odom",
|
||||||
|
"scan": "/scan",
|
||||||
|
"imu": "/imu",
|
||||||
|
"joint_states": "/joint_states",
|
||||||
|
"camera": "/camera/image_raw",
|
||||||
|
"camera_info": "/camera/camera_info"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "move_velocity",
|
||||||
|
"name": "Set Velocity",
|
||||||
|
"description": "Control the robot's linear and angular velocity",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "navigation",
|
||||||
|
"timeout": 30000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"linear": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -0.26,
|
||||||
|
"maximum": 0.26,
|
||||||
|
"default": 0,
|
||||||
|
"description": "Forward/backward velocity in m/s"
|
||||||
|
},
|
||||||
|
"angular": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -1.82,
|
||||||
|
"maximum": 1.82,
|
||||||
|
"default": 0,
|
||||||
|
"description": "Rotational velocity in rad/s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["linear", "angular"]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/Twist",
|
||||||
|
"topic": "/cmd_vel",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "transformToTwist"
|
||||||
|
},
|
||||||
|
"qos": {
|
||||||
|
"reliability": "reliable",
|
||||||
|
"durability": "volatile",
|
||||||
|
"history": "keep_last",
|
||||||
|
"depth": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "move_to_pose",
|
||||||
|
"name": "Navigate to Position",
|
||||||
|
"description": "Navigate to a specific position on the map using autonomous navigation",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "target",
|
||||||
|
"timeout": 120000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"description": "X coordinate in meters"
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Y coordinate in meters"
|
||||||
|
},
|
||||||
|
"theta": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Final orientation in radians"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["x", "y", "theta"]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/PoseStamped",
|
||||||
|
"action": "/navigate_to_pose",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "transformToPoseStamped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "capture_image",
|
||||||
|
"name": "Capture Image",
|
||||||
|
"description": "Capture an image from the robot's camera",
|
||||||
|
"category": "sensors",
|
||||||
|
"icon": "camera",
|
||||||
|
"timeout": 10000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "image_{timestamp}.jpg",
|
||||||
|
"description": "Filename for the captured image"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100,
|
||||||
|
"default": 85,
|
||||||
|
"description": "JPEG quality (1-100)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["filename"]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "sensor_msgs/msg/Image",
|
||||||
|
"topic": "/camera/image_raw",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "captureAndSaveImage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scan_environment",
|
||||||
|
"name": "Scan Environment",
|
||||||
|
"description": "Perform a 360-degree scan of the environment using LIDAR",
|
||||||
|
"category": "sensors",
|
||||||
|
"icon": "radar",
|
||||||
|
"timeout": 15000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"duration": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1.0,
|
||||||
|
"maximum": 10.0,
|
||||||
|
"default": 3.0,
|
||||||
|
"description": "Scan duration in seconds"
|
||||||
|
},
|
||||||
|
"save_data": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Save scan data to file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["duration"]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "sensor_msgs/msg/LaserScan",
|
||||||
|
"topic": "/scan",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "collectLaserScan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rotate_in_place",
|
||||||
|
"name": "Rotate in Place",
|
||||||
|
"description": "Rotate the robot by a specific angle",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "rotate-cw",
|
||||||
|
"timeout": 30000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"angle": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -6.28,
|
||||||
|
"maximum": 6.28,
|
||||||
|
"default": 1.57,
|
||||||
|
"description": "Rotation angle in radians (positive = counterclockwise)"
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 0.5,
|
||||||
|
"description": "Rotation speed as fraction of maximum"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["angle"]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/Twist",
|
||||||
|
"topic": "/cmd_vel",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "transformToRotation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "follow_wall",
|
||||||
|
"name": "Follow Wall",
|
||||||
|
"description": "Follow a wall using LIDAR sensor feedback",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "move-3d",
|
||||||
|
"timeout": 60000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"side": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["left", "right"],
|
||||||
|
"default": "right",
|
||||||
|
"description": "Which side wall to follow"
|
||||||
|
},
|
||||||
|
"distance": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.2,
|
||||||
|
"maximum": 1.0,
|
||||||
|
"default": 0.3,
|
||||||
|
"description": "Desired distance from wall in meters"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 5.0,
|
||||||
|
"maximum": 120.0,
|
||||||
|
"default": 30.0,
|
||||||
|
"description": "Duration to follow wall in seconds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["side", "distance", "duration"]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/Twist",
|
||||||
|
"topic": "/cmd_vel",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "wallFollowing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "emergency_stop",
|
||||||
|
"name": "Emergency Stop",
|
||||||
|
"description": "Immediately stop all robot movement and disable motors",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "octagon",
|
||||||
|
"timeout": 2000,
|
||||||
|
"retryable": false,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "geometry_msgs/msg/Twist",
|
||||||
|
"topic": "/cmd_vel",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "static",
|
||||||
|
"payload": {
|
||||||
|
"linear": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
||||||
|
"angular": { "x": 0.0, "y": 0.0, "z": 0.0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qos": {
|
||||||
|
"reliability": "reliable",
|
||||||
|
"durability": "volatile",
|
||||||
|
"history": "keep_last",
|
||||||
|
"depth": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "get_robot_state",
|
||||||
|
"name": "Get Robot State",
|
||||||
|
"description": "Retrieve current robot position, orientation, and status",
|
||||||
|
"category": "sensors",
|
||||||
|
"icon": "info",
|
||||||
|
"timeout": 5000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"include_sensor_data": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Include latest sensor readings in response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "nav_msgs/msg/Odometry",
|
||||||
|
"topic": "/odom",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "extractRobotState"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
},
|
},
|
||||||
"official": true,
|
"official": true,
|
||||||
"trust": "official",
|
"trust": "official",
|
||||||
|
"apiVersion": "1.0",
|
||||||
|
"pluginApiVersion": "1.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "HRIStudio Team",
|
"name": "HRIStudio Team",
|
||||||
"email": "support@hristudio.com",
|
"email": "support@hristudio.com",
|
||||||
@@ -24,6 +26,28 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"defaultBranch": "main",
|
"defaultBranch": "main",
|
||||||
"lastUpdated": "2025-02-13T00:00:00Z",
|
"lastUpdated": "2025-02-13T00:00:00Z",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": "mobile-robots",
|
||||||
|
"name": "Mobile Robots",
|
||||||
|
"description": "Wheeled and tracked mobile platforms"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "manipulators",
|
||||||
|
"name": "Manipulators",
|
||||||
|
"description": "Robotic arms and end effectors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "humanoids",
|
||||||
|
"name": "Humanoid Robots",
|
||||||
|
"description": "Human-like robots for social interaction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "drones",
|
||||||
|
"name": "Aerial Vehicles",
|
||||||
|
"description": "Quadcopters and fixed-wing UAVs"
|
||||||
|
}
|
||||||
|
],
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"hristudio": {
|
"hristudio": {
|
||||||
"min": "0.1.0",
|
"min": "0.1.0",
|
||||||
@@ -39,13 +63,8 @@
|
|||||||
"logo": "assets/repository-logo.png",
|
"logo": "assets/repository-logo.png",
|
||||||
"banner": "assets/repository-banner.png"
|
"banner": "assets/repository-banner.png"
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["official", "mobile-robots", "ros2", "turtlebot"],
|
||||||
"official",
|
|
||||||
"mobile-robots",
|
|
||||||
"ros2",
|
|
||||||
"turtlebot"
|
|
||||||
],
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"plugins": 1
|
"plugins": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
383
scripts/validate-plugin.js
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Color output helpers
|
||||||
|
const colors = {
|
||||||
|
red: '\x1b[31m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
reset: '\x1b[0m'
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(message, color = 'reset') {
|
||||||
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message) {
|
||||||
|
log(`❌ ${message}`, 'red');
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(message) {
|
||||||
|
log(`✅ ${message}`, 'green');
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn(message) {
|
||||||
|
log(`⚠️ ${message}`, 'yellow');
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(message) {
|
||||||
|
log(`ℹ️ ${message}`, 'blue');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin schema validation
|
||||||
|
function validatePlugin(pluginPath) {
|
||||||
|
if (!fs.existsSync(pluginPath)) {
|
||||||
|
throw new Error(`Plugin file not found: ${pluginPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let plugin;
|
||||||
|
try {
|
||||||
|
plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid JSON syntax: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
// Required fields validation
|
||||||
|
const requiredFields = [
|
||||||
|
'robotId',
|
||||||
|
'name',
|
||||||
|
'platform',
|
||||||
|
'version',
|
||||||
|
'pluginApiVersion',
|
||||||
|
'hriStudioVersion',
|
||||||
|
'trustLevel',
|
||||||
|
'category'
|
||||||
|
];
|
||||||
|
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
if (!plugin[field]) {
|
||||||
|
errors.push(`Missing required field: ${field}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Field format validation
|
||||||
|
if (plugin.robotId && !/^[a-z0-9-]+$/.test(plugin.robotId)) {
|
||||||
|
errors.push('robotId must be lowercase with hyphens only');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.version && !/^\d+\.\d+\.\d+/.test(plugin.version)) {
|
||||||
|
errors.push('version must follow semantic versioning (e.g., 1.0.0)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.trustLevel && !['official', 'verified', 'community'].includes(plugin.trustLevel)) {
|
||||||
|
errors.push(`Invalid trustLevel: ${plugin.trustLevel}. Must be: official, verified, or community`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category validation
|
||||||
|
const validCategories = [
|
||||||
|
'mobile-robot',
|
||||||
|
'humanoid-robot',
|
||||||
|
'manipulator',
|
||||||
|
'drone',
|
||||||
|
'sensor-platform',
|
||||||
|
'simulation'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (plugin.category && !validCategories.includes(plugin.category)) {
|
||||||
|
errors.push(`Invalid category: ${plugin.category}. Valid categories: ${validCategories.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions validation
|
||||||
|
if (!plugin.actions || !Array.isArray(plugin.actions)) {
|
||||||
|
errors.push('Plugin must have an actions array');
|
||||||
|
} else if (plugin.actions.length === 0) {
|
||||||
|
warnings.push('Plugin has no actions defined');
|
||||||
|
} else {
|
||||||
|
plugin.actions.forEach((action, index) => {
|
||||||
|
const actionErrors = validateAction(action, index);
|
||||||
|
errors.push(...actionErrors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets validation
|
||||||
|
if (plugin.assets) {
|
||||||
|
if (!plugin.assets.thumbnailUrl) {
|
||||||
|
errors.push('assets.thumbnailUrl is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset paths exist
|
||||||
|
const assetChecks = [
|
||||||
|
['thumbnailUrl', plugin.assets.thumbnailUrl],
|
||||||
|
['main image', plugin.assets.images?.main],
|
||||||
|
['logo', plugin.assets.images?.logo]
|
||||||
|
];
|
||||||
|
|
||||||
|
if (plugin.assets.images?.angles) {
|
||||||
|
Object.entries(plugin.assets.images.angles).forEach(([angle, assetPath]) => {
|
||||||
|
assetChecks.push([`${angle} angle`, assetPath]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
assetChecks.forEach(([description, assetPath]) => {
|
||||||
|
if (assetPath && assetPath.startsWith('assets/')) {
|
||||||
|
const fullPath = path.resolve(path.dirname(pluginPath), '..', assetPath);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
warnings.push(`Asset not found: ${description} (${assetPath})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
errors.push('Plugin must have assets definition');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manufacturer validation
|
||||||
|
if (!plugin.manufacturer?.name) {
|
||||||
|
warnings.push('manufacturer.name is recommended');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { errors, warnings, plugin };
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAction(action, index) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
// Required action fields
|
||||||
|
const requiredFields = ['id', 'name', 'category', 'parameterSchema'];
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
if (!action[field]) {
|
||||||
|
errors.push(`Action ${index}: missing required field '${field}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Action ID format
|
||||||
|
if (action.id && !/^[a-z_]+$/.test(action.id)) {
|
||||||
|
errors.push(`Action ${index}: id must be snake_case (lowercase with underscores)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action category validation
|
||||||
|
const validActionCategories = ['movement', 'interaction', 'sensors', 'logic'];
|
||||||
|
if (action.category && !validActionCategories.includes(action.category)) {
|
||||||
|
errors.push(`Action ${index}: invalid category '${action.category}'. Valid: ${validActionCategories.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter schema validation
|
||||||
|
if (action.parameterSchema) {
|
||||||
|
if (action.parameterSchema.type !== 'object') {
|
||||||
|
errors.push(`Action ${index}: parameterSchema.type must be 'object'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!action.parameterSchema.properties) {
|
||||||
|
errors.push(`Action ${index}: parameterSchema must have properties`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(action.parameterSchema.required)) {
|
||||||
|
errors.push(`Action ${index}: parameterSchema.required must be an array`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Communication protocol validation
|
||||||
|
const hasRos2 = action.ros2;
|
||||||
|
const hasNaoqi = action.naoqi;
|
||||||
|
const hasRestApi = action.restApi;
|
||||||
|
|
||||||
|
if (!hasRos2 && !hasNaoqi && !hasRestApi) {
|
||||||
|
errors.push(`Action ${index}: must have at least one communication protocol (ros2, naoqi, or restApi)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository validation
|
||||||
|
function validateRepository() {
|
||||||
|
const repoPath = path.resolve('repository.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(repoPath)) {
|
||||||
|
throw new Error('repository.json not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let repo;
|
||||||
|
try {
|
||||||
|
repo = JSON.parse(fs.readFileSync(repoPath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Invalid repository.json: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
// Required repository fields
|
||||||
|
const requiredFields = ['id', 'name', 'apiVersion', 'pluginApiVersion', 'trust'];
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
if (!repo[field]) {
|
||||||
|
errors.push(`Missing required repository field: ${field}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate plugin count
|
||||||
|
const indexPath = path.resolve('plugins/index.json');
|
||||||
|
if (fs.existsSync(indexPath)) {
|
||||||
|
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
||||||
|
const actualCount = index.length;
|
||||||
|
const reportedCount = repo.stats?.plugins || 0;
|
||||||
|
|
||||||
|
if (actualCount !== reportedCount) {
|
||||||
|
errors.push(`Plugin count mismatch: reported ${reportedCount}, actual ${actualCount}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { errors, warnings, repo };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update plugin index
|
||||||
|
function updateIndex() {
|
||||||
|
const pluginsDir = path.resolve('plugins');
|
||||||
|
const indexPath = path.join(pluginsDir, 'index.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(pluginsDir)) {
|
||||||
|
throw new Error('plugins directory not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginFiles = fs.readdirSync(pluginsDir)
|
||||||
|
.filter(file => file.endsWith('.json') && file !== 'index.json')
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
fs.writeFileSync(indexPath, JSON.stringify(pluginFiles, null, 2));
|
||||||
|
success(`Updated index.json with ${pluginFiles.length} plugins`);
|
||||||
|
|
||||||
|
// Update repository stats
|
||||||
|
const repoPath = path.resolve('repository.json');
|
||||||
|
if (fs.existsSync(repoPath)) {
|
||||||
|
const repo = JSON.parse(fs.readFileSync(repoPath, 'utf8'));
|
||||||
|
repo.stats = repo.stats || {};
|
||||||
|
repo.stats.plugins = pluginFiles.length;
|
||||||
|
fs.writeFileSync(repoPath, JSON.stringify(repo, null, 2));
|
||||||
|
success(`Updated repository stats: ${pluginFiles.length} plugins`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main CLI
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const command = args[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case 'validate':
|
||||||
|
const pluginPath = args[1];
|
||||||
|
if (!pluginPath) {
|
||||||
|
error('Usage: validate <plugin-file>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(`Validating plugin: ${pluginPath}`);
|
||||||
|
const { errors, warnings } = validatePlugin(pluginPath);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
error('Validation failed:');
|
||||||
|
errors.forEach(err => console.log(` - ${err}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
warn('Warnings:');
|
||||||
|
warnings.forEach(warn => console.log(` - ${warn}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length === 0) {
|
||||||
|
success('Plugin validation passed!');
|
||||||
|
if (warnings.length === 0) {
|
||||||
|
success('No warnings found');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'validate-all':
|
||||||
|
info('Validating all plugins...');
|
||||||
|
|
||||||
|
// Validate repository
|
||||||
|
const repoResult = validateRepository();
|
||||||
|
if (repoResult.errors.length > 0) {
|
||||||
|
error('Repository validation failed:');
|
||||||
|
repoResult.errors.forEach(err => console.log(` - ${err}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all plugins
|
||||||
|
const indexPath = path.resolve('plugins/index.json');
|
||||||
|
if (!fs.existsSync(indexPath)) {
|
||||||
|
error('plugins/index.json not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
||||||
|
let allValid = true;
|
||||||
|
|
||||||
|
for (const pluginFile of index) {
|
||||||
|
const pluginPath = path.resolve('plugins', pluginFile);
|
||||||
|
try {
|
||||||
|
const { errors } = validatePlugin(pluginPath);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
error(`${pluginFile}: ${errors.length} errors`);
|
||||||
|
errors.forEach(err => console.log(` - ${err}`));
|
||||||
|
allValid = false;
|
||||||
|
} else {
|
||||||
|
success(`${pluginFile}: valid`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error(`${pluginFile}: ${e.message}`);
|
||||||
|
allValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allValid) {
|
||||||
|
success('All plugins are valid!');
|
||||||
|
} else {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update-index':
|
||||||
|
info('Updating plugin index...');
|
||||||
|
updateIndex();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'help':
|
||||||
|
default:
|
||||||
|
console.log(`
|
||||||
|
HRIStudio Plugin Validator
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
validate <plugin-file> Validate a single plugin file
|
||||||
|
validate-all Validate all plugins and repository
|
||||||
|
update-index Update plugins/index.json with all plugin files
|
||||||
|
help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./scripts/validate-plugin.js validate plugins/my-robot.json
|
||||||
|
./scripts/validate-plugin.js validate-all
|
||||||
|
./scripts/validate-plugin.js update-index
|
||||||
|
`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error(`Error: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validatePlugin,
|
||||||
|
validateRepository,
|
||||||
|
updateIndex
|
||||||
|
};
|
||||||
397
validate.sh
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# HRIStudio Plugin Repository Validation Script
|
||||||
|
# This script provides convenient commands for plugin development and validation
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if Node.js is available
|
||||||
|
check_node() {
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
error "Node.js is required but not installed."
|
||||||
|
error "Please install Node.js from https://nodejs.org/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate JSON syntax
|
||||||
|
validate_json() {
|
||||||
|
local file="$1"
|
||||||
|
if ! node -e "JSON.parse(require('fs').readFileSync('$file', 'utf8'))" 2>/dev/null; then
|
||||||
|
error "Invalid JSON syntax in $file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate all JSON files
|
||||||
|
validate_json_files() {
|
||||||
|
log "Validating JSON syntax..."
|
||||||
|
|
||||||
|
local valid=true
|
||||||
|
|
||||||
|
for file in repository.json plugins/*.json; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
if validate_json "$file"; then
|
||||||
|
success "✓ $file"
|
||||||
|
else
|
||||||
|
error "✗ $file"
|
||||||
|
valid=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$valid" = true ]; then
|
||||||
|
success "All JSON files have valid syntax"
|
||||||
|
else
|
||||||
|
error "Some JSON files have syntax errors"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate plugins using the Node.js script
|
||||||
|
validate_plugins() {
|
||||||
|
log "Validating plugins..."
|
||||||
|
check_node
|
||||||
|
|
||||||
|
if [ ! -f "scripts/validate-plugin.js" ]; then
|
||||||
|
error "Plugin validator script not found: scripts/validate-plugin.js"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
node scripts/validate-plugin.js validate-all
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update plugin index
|
||||||
|
update_index() {
|
||||||
|
log "Updating plugin index..."
|
||||||
|
check_node
|
||||||
|
|
||||||
|
if [ ! -f "scripts/validate-plugin.js" ]; then
|
||||||
|
error "Plugin validator script not found: scripts/validate-plugin.js"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
node scripts/validate-plugin.js update-index
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start local development server
|
||||||
|
serve() {
|
||||||
|
local port="${1:-8080}"
|
||||||
|
log "Starting development server on port $port..."
|
||||||
|
|
||||||
|
if command -v python3 &> /dev/null; then
|
||||||
|
log "Using Python 3 HTTP server"
|
||||||
|
python3 -m http.server "$port"
|
||||||
|
elif command -v python &> /dev/null; then
|
||||||
|
log "Using Python HTTP server"
|
||||||
|
python -m SimpleHTTPServer "$port"
|
||||||
|
elif command -v node &> /dev/null; then
|
||||||
|
log "Using Node.js http-server (install with: npm install -g http-server)"
|
||||||
|
if command -v http-server &> /dev/null; then
|
||||||
|
http-server -p "$port" -c-1
|
||||||
|
else
|
||||||
|
error "http-server not found. Install with: npm install -g http-server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
error "No suitable HTTP server found. Please install Python or Node.js with http-server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test the repository
|
||||||
|
test_repo() {
|
||||||
|
log "Running repository tests..."
|
||||||
|
|
||||||
|
# Validate JSON syntax
|
||||||
|
validate_json_files || return 1
|
||||||
|
|
||||||
|
# Validate plugins
|
||||||
|
validate_plugins || return 1
|
||||||
|
|
||||||
|
# Check assets exist
|
||||||
|
log "Checking asset files..."
|
||||||
|
local missing_assets=false
|
||||||
|
|
||||||
|
for plugin_file in plugins/*.json; do
|
||||||
|
if [ "$plugin_file" = "plugins/index.json" ] || [ "$plugin_file" = "plugins/*.json" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local plugin_name=$(basename "$plugin_file" .json)
|
||||||
|
log "Checking assets for $plugin_name..."
|
||||||
|
|
||||||
|
# Extract asset paths and check they exist
|
||||||
|
if [ -f "$plugin_file" ]; then
|
||||||
|
local thumbnail=$(node -e "
|
||||||
|
const plugin = JSON.parse(require('fs').readFileSync('$plugin_file', 'utf8'));
|
||||||
|
console.log(plugin.assets?.thumbnailUrl || '');
|
||||||
|
")
|
||||||
|
|
||||||
|
if [ -n "$thumbnail" ] && [ ! -f "$thumbnail" ]; then
|
||||||
|
warn "Missing thumbnail: $thumbnail"
|
||||||
|
missing_assets=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$missing_assets" = true ]; then
|
||||||
|
warn "Some assets are missing - this may cause display issues"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test web interface
|
||||||
|
log "Testing web interface..."
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
# Start server in background
|
||||||
|
python3 -m http.server 8000 &>/dev/null &
|
||||||
|
local server_pid=$!
|
||||||
|
|
||||||
|
# Wait for server to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test endpoints
|
||||||
|
if curl -sf http://localhost:8000/index.html >/dev/null 2>&1; then
|
||||||
|
success "✓ Web interface accessible"
|
||||||
|
else
|
||||||
|
error "✗ Web interface not accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -sf http://localhost:8000/repository.json >/dev/null 2>&1; then
|
||||||
|
success "✓ Repository metadata accessible"
|
||||||
|
else
|
||||||
|
error "✗ Repository metadata not accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
kill $server_pid 2>/dev/null || true
|
||||||
|
else
|
||||||
|
warn "curl not available - skipping web interface test"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Repository validation completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
build() {
|
||||||
|
log "Building repository for production..."
|
||||||
|
|
||||||
|
# Validate everything first
|
||||||
|
test_repo || return 1
|
||||||
|
|
||||||
|
# Update index
|
||||||
|
update_index
|
||||||
|
|
||||||
|
# Optimize JSON files (remove extra whitespace)
|
||||||
|
log "Optimizing JSON files..."
|
||||||
|
for file in repository.json plugins/*.json; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const data = JSON.parse(fs.readFileSync('$file', 'utf8'));
|
||||||
|
fs.writeFileSync('$file', JSON.stringify(data));
|
||||||
|
"
|
||||||
|
success "✓ Optimized $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
success "Build completed - repository is ready for production"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a new plugin template
|
||||||
|
create_plugin() {
|
||||||
|
local robot_id="$1"
|
||||||
|
|
||||||
|
if [ -z "$robot_id" ]; then
|
||||||
|
error "Usage: $0 create <robot-id>"
|
||||||
|
error "Example: $0 create my-robot"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local plugin_file="plugins/${robot_id}.json"
|
||||||
|
local asset_dir="assets/${robot_id}"
|
||||||
|
|
||||||
|
if [ -f "$plugin_file" ]; then
|
||||||
|
error "Plugin already exists: $plugin_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Creating plugin template for $robot_id..."
|
||||||
|
|
||||||
|
# Create asset directory
|
||||||
|
mkdir -p "$asset_dir"
|
||||||
|
|
||||||
|
# Create plugin template
|
||||||
|
cat > "$plugin_file" << EOF
|
||||||
|
{
|
||||||
|
"robotId": "$robot_id",
|
||||||
|
"name": "Robot Name",
|
||||||
|
"description": "Description of the robot platform",
|
||||||
|
"platform": "ROS2",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"pluginApiVersion": "1.0",
|
||||||
|
"hriStudioVersion": ">=0.1.0",
|
||||||
|
"trustLevel": "community",
|
||||||
|
"category": "mobile-robot",
|
||||||
|
|
||||||
|
"manufacturer": {
|
||||||
|
"name": "Manufacturer Name",
|
||||||
|
"website": "https://example.com",
|
||||||
|
"support": "https://example.com/support"
|
||||||
|
},
|
||||||
|
|
||||||
|
"documentation": {
|
||||||
|
"mainUrl": "https://example.com/docs"
|
||||||
|
},
|
||||||
|
|
||||||
|
"assets": {
|
||||||
|
"thumbnailUrl": "$asset_dir/thumb.png",
|
||||||
|
"images": {
|
||||||
|
"main": "$asset_dir/main.jpg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"specs": {
|
||||||
|
"dimensions": {
|
||||||
|
"length": 0.5,
|
||||||
|
"width": 0.3,
|
||||||
|
"height": 0.2,
|
||||||
|
"weight": 5.0
|
||||||
|
},
|
||||||
|
"capabilities": ["example_capability"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "example_action",
|
||||||
|
"name": "Example Action",
|
||||||
|
"description": "An example action for demonstration",
|
||||||
|
"category": "movement",
|
||||||
|
"icon": "move",
|
||||||
|
"timeout": 30000,
|
||||||
|
"retryable": true,
|
||||||
|
"parameterSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"parameter": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "value",
|
||||||
|
"description": "Example parameter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["parameter"]
|
||||||
|
},
|
||||||
|
"ros2": {
|
||||||
|
"messageType": "std_msgs/msg/String",
|
||||||
|
"topic": "/example_topic",
|
||||||
|
"payloadMapping": {
|
||||||
|
"type": "transform",
|
||||||
|
"transformFn": "exampleTransform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
success "✓ Created plugin template: $plugin_file"
|
||||||
|
success "✓ Created asset directory: $asset_dir"
|
||||||
|
warn "Don't forget to:"
|
||||||
|
warn " 1. Edit the plugin details in $plugin_file"
|
||||||
|
warn " 2. Add robot images to $asset_dir/"
|
||||||
|
warn " 3. Run './validate.sh update-index' to include the plugin"
|
||||||
|
warn " 4. Run './validate.sh validate' to check your plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
HRIStudio Plugin Repository Development Tools
|
||||||
|
|
||||||
|
Usage: $0 <command> [arguments]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
validate Validate all plugins and repository metadata
|
||||||
|
update-index Update plugins/index.json with all plugin files
|
||||||
|
serve [port] Start local development server (default port: 8080)
|
||||||
|
test Run full test suite
|
||||||
|
build Build and optimize for production
|
||||||
|
create <robot-id> Create a new plugin template
|
||||||
|
help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$0 validate # Validate all plugins
|
||||||
|
$0 serve 3000 # Start server on port 3000
|
||||||
|
$0 create my-robot # Create new plugin template
|
||||||
|
$0 test # Run all tests
|
||||||
|
$0 build # Build for production
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Node.js (for plugin validation)
|
||||||
|
- Python 3 or Python 2 (for development server)
|
||||||
|
- curl (for testing, optional)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main command dispatcher
|
||||||
|
main() {
|
||||||
|
case "${1:-help}" in
|
||||||
|
validate)
|
||||||
|
validate_plugins
|
||||||
|
;;
|
||||||
|
update-index)
|
||||||
|
update_index
|
||||||
|
;;
|
||||||
|
serve)
|
||||||
|
serve "${2:-8080}"
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
test_repo
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
build
|
||||||
|
;;
|
||||||
|
create)
|
||||||
|
create_plugin "$2"
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unknown command: $1"
|
||||||
|
echo
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function with all arguments
|
||||||
|
main "$@"
|
||||||