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
|
||||
|
||||
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.json # Repository metadata and configuration
|
||||
index.html # Web interface for viewing repository information
|
||||
plugins/ # Directory containing all plugin files
|
||||
index.json # List of available plugins
|
||||
plugin1.json # Individual plugin definition
|
||||
plugin2.json # Individual plugin definition
|
||||
...
|
||||
assets/ # Optional directory for repository assets
|
||||
repository-icon.png # Repository icon
|
||||
repository-logo.png # Repository logo
|
||||
repository-banner.png # Repository banner
|
||||
robot-plugins/
|
||||
├── repository.json # Repository metadata
|
||||
├── index.html # Web interface
|
||||
├── plugins/ # Plugin definitions
|
||||
│ ├── index.json # Plugin list
|
||||
│ ├── turtlebot3-burger.json
|
||||
│ ├── turtlebot3-waffle.json
|
||||
│ └── nao-humanoid.json
|
||||
├── assets/ # Visual assets
|
||||
│ ├── repository-*.png # Repository branding
|
||||
│ ├── 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
|
||||
|
||||
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
|
||||
- Show repository statistics (plugin count)
|
||||
- List author information and compatibility details
|
||||
- Display repository tags and categories
|
||||
- Show repository assets (icon, banner, logo)
|
||||
|
||||
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).
|
||||
- Repository information and statistics
|
||||
- Plugin catalog with search and filtering
|
||||
- Individual plugin details and documentation
|
||||
- Asset preview and download links
|
||||
- Installation instructions for HRIStudio
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork or clone this repository
|
||||
2. Create your plugin branch
|
||||
3. Add your plugin JSON file to the `plugins` directory
|
||||
4. Update `plugins/index.json` to include your plugin
|
||||
5. Test your changes locally
|
||||
6. Submit your changes
|
||||
### Adding a Plugin
|
||||
|
||||
1. **Fork** this repository
|
||||
2. **Create** your plugin using the template
|
||||
3. **Add** comprehensive robot assets
|
||||
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
|
||||
|
||||
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 {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.plugin-details-header-content {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.plugin-details-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: calc(var(--radius) - 0.25rem);
|
||||
overflow: hidden;
|
||||
@@ -811,7 +816,24 @@ img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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 */
|
||||
@@ -1077,4 +1099,113 @@ code {
|
||||
width: 100%;
|
||||
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
|
||||
{
|
||||
"id": string,
|
||||
"name": string,
|
||||
"description": string?,
|
||||
"id": "string (required) - Unique repository identifier",
|
||||
"name": "string (required) - Display name",
|
||||
"description": "string (optional) - Repository description",
|
||||
"apiVersion": "string (required) - Repository API version",
|
||||
"pluginApiVersion": "string (required) - Plugin API version supported",
|
||||
"urls": {
|
||||
"repository": string (URL),
|
||||
"git": string (URL)?
|
||||
"repository": "string (URL, required) - Repository base 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": {
|
||||
"name": string,
|
||||
"email": string (email)?,
|
||||
"url": string (URL)?,
|
||||
"organization": string?
|
||||
"name": "string (required)",
|
||||
"email": "string (email, optional)",
|
||||
"url": "string (URL, optional)",
|
||||
"organization": "string (optional)"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": string,
|
||||
"email": string (email)?,
|
||||
"url": string (URL)?
|
||||
"name": "string (required)",
|
||||
"email": "string (email, optional)",
|
||||
"url": "string (URL, optional)"
|
||||
}
|
||||
]?,
|
||||
"homepage": string (URL)?,
|
||||
"license": string,
|
||||
"defaultBranch": string,
|
||||
"lastUpdated": string (ISO date),
|
||||
"trust": "official" | "verified" | "community",
|
||||
"assets": {
|
||||
"icon": string?,
|
||||
"logo": string?,
|
||||
"banner": string?
|
||||
},
|
||||
],
|
||||
"homepage": "string (URL, optional)",
|
||||
"license": "string (required) - License identifier",
|
||||
"defaultBranch": "string (required) - Default Git branch",
|
||||
"lastUpdated": "string (ISO date, required)",
|
||||
"categories": [
|
||||
{
|
||||
"id": "string (required) - Category identifier",
|
||||
"name": "string (required) - Display name",
|
||||
"description": "string (required) - Category description"
|
||||
}
|
||||
],
|
||||
"compatibility": {
|
||||
"hristudio": {
|
||||
"min": string,
|
||||
"recommended": string?
|
||||
"min": "string (semver, required) - Minimum HRIStudio version",
|
||||
"recommended": "string (semver, optional) - Recommended version"
|
||||
},
|
||||
"ros2": {
|
||||
"distributions": string[],
|
||||
"recommended": string?
|
||||
}?
|
||||
"distributions": "string[] (optional) - Supported ROS2 distributions",
|
||||
"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": {
|
||||
"plugins": number
|
||||
}?
|
||||
"plugins": "number (required) - Number of plugins"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
{
|
||||
"robotId": string,
|
||||
"name": string,
|
||||
"description": string?,
|
||||
"platform": string,
|
||||
"version": string,
|
||||
"manufacturer": object,
|
||||
"documentation": object,
|
||||
"assets": object,
|
||||
"specs": object,
|
||||
"ros2Config": object,
|
||||
"actions": array
|
||||
"robotId": "string (required) - Unique robot identifier",
|
||||
"name": "string (required) - Display name",
|
||||
"description": "string (optional) - Robot description",
|
||||
"platform": "string (required) - Robot platform (e.g., 'ROS2')",
|
||||
"version": "string (required) - Plugin version (semver)",
|
||||
"pluginApiVersion": "string (required) - Plugin API version",
|
||||
"hriStudioVersion": "string (required) - Minimum HRIStudio version",
|
||||
"trustLevel": "string (enum: official|verified|community, required)",
|
||||
"category": "string (required) - Plugin category identifier"
|
||||
}
|
||||
```
|
||||
|
||||
## Core Properties
|
||||
|
||||
### 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
|
||||
### Manufacturer Information
|
||||
|
||||
```json
|
||||
"manufacturer": {
|
||||
"name": string,
|
||||
"website": string (URL)?,
|
||||
"support": string (URL)?
|
||||
"name": "string (required)",
|
||||
"website": "string (URL, optional)",
|
||||
"support": "string (URL, optional)"
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation Links
|
||||
### Documentation Links
|
||||
|
||||
```json
|
||||
"documentation": {
|
||||
"mainUrl": string (URL),
|
||||
"apiReference": string (URL)?,
|
||||
"wikiUrl": string (URL)?,
|
||||
"videoUrl": string (URL)?
|
||||
"mainUrl": "string (URL, required)",
|
||||
"apiReference": "string (URL, optional)",
|
||||
"wikiUrl": "string (URL, optional)",
|
||||
"videoUrl": "string (URL, optional)"
|
||||
}
|
||||
```
|
||||
|
||||
## Assets Configuration
|
||||
### Assets Configuration
|
||||
|
||||
```json
|
||||
"assets": {
|
||||
"thumbnailUrl": string,
|
||||
"logo": string?,
|
||||
"thumbnailUrl": "string (path, required)",
|
||||
"images": {
|
||||
"main": string,
|
||||
"main": "string (path, required)",
|
||||
"angles": {
|
||||
"front": string?,
|
||||
"side": string?,
|
||||
"top": string?
|
||||
}
|
||||
"front": "string (path, optional)",
|
||||
"side": "string (path, optional)",
|
||||
"top": "string (path, optional)"
|
||||
},
|
||||
"logo": "string (path, optional)"
|
||||
},
|
||||
"model": {
|
||||
"format": "URDF" | "glTF" | "other",
|
||||
"url": string (URL)
|
||||
}?
|
||||
"format": "string (enum: URDF|glTF|STL, optional)",
|
||||
"url": "string (URL, optional)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Robot Specifications
|
||||
### Robot Specifications
|
||||
|
||||
```json
|
||||
"specs": {
|
||||
"dimensions": {
|
||||
"length": number,
|
||||
"width": number,
|
||||
"height": number,
|
||||
"weight": number
|
||||
"length": "number (meters, required)",
|
||||
"width": "number (meters, required)",
|
||||
"height": "number (meters, required)",
|
||||
"weight": "number (kg, required)"
|
||||
},
|
||||
"capabilities": string[],
|
||||
"maxSpeed": number,
|
||||
"batteryLife": number
|
||||
"capabilities": "string[] (required) - List of robot capabilities",
|
||||
"maxSpeed": "number (m/s, optional)",
|
||||
"batteryLife": "number (hours, optional)"
|
||||
}
|
||||
```
|
||||
|
||||
## ROS2 Configuration
|
||||
### ROS2 Configuration
|
||||
|
||||
```json
|
||||
"ros2Config": {
|
||||
"namespace": string,
|
||||
"nodePrefix": string,
|
||||
"namespace": "string (required) - Default ROS2 namespace",
|
||||
"nodePrefix": "string (required) - Node name prefix",
|
||||
"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
|
||||
{
|
||||
"actionId": string,
|
||||
"type": "move" | "speak" | "wait" | "input" | "gesture" | "record" | "condition" | "loop",
|
||||
"title": string,
|
||||
"description": string,
|
||||
"icon": string?,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"id": "string (required) - Unique action identifier (snake_case)",
|
||||
"name": "string (required) - Display name for UI",
|
||||
"description": "string (optional) - Action description",
|
||||
"category": "string (enum: movement|interaction|sensors|logic, required)",
|
||||
"icon": "string (optional) - Lucide icon name",
|
||||
"timeout": "number (milliseconds, optional) - Default timeout",
|
||||
"retryable": "boolean (optional) - Whether action can be retried on failure",
|
||||
"parameterSchema": {
|
||||
"type": "object (required)",
|
||||
"properties": {
|
||||
[key: string]: {
|
||||
"type": string,
|
||||
"title": string,
|
||||
"description": string?,
|
||||
"default": any?,
|
||||
"minimum": number?,
|
||||
"maximum": number?,
|
||||
"enum": string[]?,
|
||||
"unit": string?
|
||||
"[paramName]": {
|
||||
"type": "string (JSON Schema type, required)",
|
||||
"minimum": "number (optional) - For numeric types",
|
||||
"maximum": "number (optional) - For numeric types",
|
||||
"default": "any (optional) - Default value",
|
||||
"description": "string (required) - Parameter description",
|
||||
"enum": "string[] (optional) - For enum types"
|
||||
}
|
||||
},
|
||||
"required": string[]
|
||||
"required": "string[] (required) - List of required parameters"
|
||||
},
|
||||
"ros2": {
|
||||
"messageType": string,
|
||||
"topic": string?,
|
||||
"service": string?,
|
||||
"action": string?,
|
||||
"messageType": "string (required) - ROS2 message type",
|
||||
"topic": "string (optional) - Topic name for publishers",
|
||||
"service": "string (optional) - Service name for service calls",
|
||||
"action": "string (optional) - Action name for action calls",
|
||||
"payloadMapping": {
|
||||
"type": "direct" | "transform",
|
||||
"map": object?,
|
||||
"transformFn": string?
|
||||
"type": "string (enum: transform|static, required)",
|
||||
"transformFn": "string (optional) - Transform function name",
|
||||
"payload": "object (optional) - Static payload for static type"
|
||||
},
|
||||
"qos": {
|
||||
"reliability": "reliable" | "best_effort",
|
||||
"durability": "volatile" | "transient_local",
|
||||
"history": "keep_last" | "keep_all",
|
||||
"depth": number?
|
||||
}?
|
||||
"reliability": "string (enum: reliable|best_effort, optional)",
|
||||
"durability": "string (enum: volatile|transient_local, optional)",
|
||||
"history": "string (enum: keep_last|keep_all, optional)",
|
||||
"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
|
||||
- `reliable`: Guaranteed delivery
|
||||
- `best_effort`: Fast but may drop messages
|
||||
- **movement**: Robot locomotion and positioning
|
||||
- **interaction**: Communication and social behaviors
|
||||
- **sensors**: Data collection and environmental sensing
|
||||
- **logic**: Control flow and decision making
|
||||
|
||||
- `durability`: Message persistence
|
||||
- `volatile`: Only delivered to active subscribers
|
||||
- `transient_local`: Stored for late-joining subscribers
|
||||
## Trust Levels
|
||||
|
||||
- `history`: Message queue behavior
|
||||
- `keep_last`: Store up to N messages (specify with depth)
|
||||
- `keep_all`: Store all messages
|
||||
Plugins are classified by trust level:
|
||||
|
||||
## 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 -->
|
||||
<div class="plugin-details">
|
||||
<div id="pluginDetails" class="hidden">
|
||||
<!-- Header -->
|
||||
<div class="plugin-details-header">
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="plugin-details-icon">
|
||||
<img id="detailsIcon" alt="" class="plugin-icon">
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 id="detailsTitle" class="text-xl font-semibold"></h3>
|
||||
<p id="detailsDescription" class="mt-1 text-muted-foreground"></p>
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span id="detailsSpeed" class="badge badge-secondary"></span>
|
||||
<span id="detailsBattery" class="badge badge-secondary"></span>
|
||||
<span id="detailsWeight" class="badge badge-secondary"></span>
|
||||
<div class="plugin-details-header-content">
|
||||
<div class="plugin-details-icon">
|
||||
<img id="detailsIcon" alt="" class="plugin-icon">
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 id="detailsTitle" class="text-xl font-semibold">Plugin Title</h3>
|
||||
<p id="detailsDescription" class="mt-1 text-muted-foreground">Plugin description goes here.</p>
|
||||
<div class="flex flex-wrap items-center gap-4 mt-4 text-sm">
|
||||
<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="m5 8 6 6"></path>
|
||||
<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 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 class="p-6">
|
||||
<div class="plugin-details-tabs">
|
||||
<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="false" data-plugin-tab="plugin-specs">Specifications</button>
|
||||
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="plugin-actions">Actions</button>
|
||||
</div>
|
||||
<!-- Tabs -->
|
||||
<div class="plugin-details-tabs">
|
||||
<div class="plugin-details-tabs-list" role="tablist">
|
||||
<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="specifications">Specifications</button>
|
||||
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="actions">Actions</button>
|
||||
</div>
|
||||
|
||||
<div class="plugin-details-tab-content" data-state="active" role="tabpanel" data-plugin-tab="plugin-overview">
|
||||
<div id="detailsImages" class="relative mb-6">
|
||||
<!-- Images will be loaded here -->
|
||||
</div>
|
||||
<div class="card-secondary">
|
||||
<h4>Documentation</h4>
|
||||
<div class="grid gap-2 text-sm">
|
||||
<a id="detailsMainDocs" href="#" target="_blank" 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>
|
||||
<!-- Overview Tab -->
|
||||
<div class="plugin-details-tab-content" data-state="active" role="tabpanel" data-plugin-tab="overview">
|
||||
<div class="content-section">
|
||||
<h3>Robot Images</h3>
|
||||
<div class="image-gallery" id="detailsImages"></div>
|
||||
</div>
|
||||
<div class="content-section">
|
||||
<h3>Documentation</h3>
|
||||
<div class="grid gap-2 text-sm">
|
||||
<a
|
||||
id="detailsMainDocs"
|
||||
href="#"
|
||||
target="_blank"
|
||||
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 class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="plugin-specs">
|
||||
<div class="space-y-6">
|
||||
<div class="card-secondary">
|
||||
<h4>Physical Specifications</h4>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<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>
|
||||
<!-- Specifications Tab -->
|
||||
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="specifications">
|
||||
<div class="content-section">
|
||||
<h3>Robot Images</h3>
|
||||
<div class="image-gallery" id="specsImageGallery"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="plugin-actions">
|
||||
<div id="detailsActions" class="space-y-4">
|
||||
<!-- Actions will be loaded here -->
|
||||
</div>
|
||||
<!-- Actions Tab -->
|
||||
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="actions">
|
||||
<div class="content-section">
|
||||
<h3>Actions Tab</h3>
|
||||
<p>Actions content will go here.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,6 +317,19 @@
|
||||
</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>
|
||||
// Separate tab management for root and plugin details
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
@@ -552,8 +545,11 @@
|
||||
actionsContainer.appendChild(div);
|
||||
});
|
||||
|
||||
// Update specifications tab with image gallery
|
||||
updateSpecsImageGallery(plugin);
|
||||
|
||||
// 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);
|
||||
@@ -656,6 +652,75 @@
|
||||
|
||||
// Load data when page loads
|
||||
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>
|
||||
</body>
|
||||
</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-burger.json", "turtlebot3-waffle.json", "nao-humanoid.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",
|
||||
"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/",
|
||||
@@ -42,12 +46,7 @@
|
||||
"height": 0.192,
|
||||
"weight": 1.0
|
||||
},
|
||||
"capabilities": [
|
||||
"differential_drive",
|
||||
"lidar",
|
||||
"imu",
|
||||
"odometry"
|
||||
],
|
||||
"capabilities": ["differential_drive", "lidar", "imu", "odometry"],
|
||||
"maxSpeed": 0.22,
|
||||
"batteryLife": 2.5
|
||||
},
|
||||
@@ -66,31 +65,29 @@
|
||||
|
||||
"actions": [
|
||||
{
|
||||
"actionId": "move-velocity",
|
||||
"type": "move",
|
||||
"title": "Set Velocity",
|
||||
"id": "move_velocity",
|
||||
"name": "Set Velocity",
|
||||
"description": "Control the robot's linear and angular velocity",
|
||||
"category": "movement",
|
||||
"icon": "navigation",
|
||||
"parameters": {
|
||||
"timeout": 30000,
|
||||
"retryable": true,
|
||||
"parameterSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"linear": {
|
||||
"type": "number",
|
||||
"title": "Linear Velocity",
|
||||
"description": "Forward/backward velocity",
|
||||
"default": 0,
|
||||
"minimum": -0.22,
|
||||
"maximum": 0.22,
|
||||
"unit": "m/s"
|
||||
"default": 0,
|
||||
"description": "Forward/backward velocity in m/s"
|
||||
},
|
||||
"angular": {
|
||||
"type": "number",
|
||||
"title": "Angular Velocity",
|
||||
"description": "Rotational velocity",
|
||||
"default": 0,
|
||||
"minimum": -2.84,
|
||||
"maximum": 2.84,
|
||||
"unit": "rad/s"
|
||||
"default": 0,
|
||||
"description": "Rotational velocity in rad/s"
|
||||
}
|
||||
},
|
||||
"required": ["linear", "angular"]
|
||||
@@ -111,34 +108,30 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"actionId": "move-to-pose",
|
||||
"type": "move",
|
||||
"title": "Move to Position",
|
||||
"id": "move_to_pose",
|
||||
"name": "Move to Position",
|
||||
"description": "Navigate to a specific position on the map",
|
||||
"category": "movement",
|
||||
"icon": "target",
|
||||
"parameters": {
|
||||
"timeout": 60000,
|
||||
"retryable": true,
|
||||
"parameterSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"title": "X Position",
|
||||
"description": "X coordinate in meters",
|
||||
"default": 0,
|
||||
"unit": "m"
|
||||
"description": "X coordinate in meters"
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"title": "Y Position",
|
||||
"description": "Y coordinate in meters",
|
||||
"default": 0,
|
||||
"unit": "m"
|
||||
"description": "Y coordinate in meters"
|
||||
},
|
||||
"theta": {
|
||||
"type": "number",
|
||||
"title": "Orientation",
|
||||
"description": "Final orientation",
|
||||
"default": 0,
|
||||
"unit": "rad"
|
||||
"description": "Final orientation in radians"
|
||||
}
|
||||
},
|
||||
"required": ["x", "y", "theta"]
|
||||
@@ -151,6 +144,37 @@
|
||||
"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,
|
||||
"trust": "official",
|
||||
"apiVersion": "1.0",
|
||||
"pluginApiVersion": "1.0",
|
||||
"author": {
|
||||
"name": "HRIStudio Team",
|
||||
"email": "support@hristudio.com",
|
||||
@@ -24,6 +26,28 @@
|
||||
"license": "MIT",
|
||||
"defaultBranch": "main",
|
||||
"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": {
|
||||
"hristudio": {
|
||||
"min": "0.1.0",
|
||||
@@ -39,13 +63,8 @@
|
||||
"logo": "assets/repository-logo.png",
|
||||
"banner": "assets/repository-banner.png"
|
||||
},
|
||||
"tags": [
|
||||
"official",
|
||||
"mobile-robots",
|
||||
"ros2",
|
||||
"turtlebot"
|
||||
],
|
||||
"tags": ["official", "mobile-robots", "ros2", "turtlebot"],
|
||||
"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 "$@"
|
||||