Update for new HRIStudio build

This commit is contained in:
2025-08-07 01:29:00 -04:00
parent 3acdccf9a7
commit 0e835f2ee3
29 changed files with 2978 additions and 362 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.git
.github
.gitignore
.dockerignore
docker-compose.yml
README.md
LICENSE
docs/

247
.github/workflows/validate.yml vendored Normal file
View 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
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

17
docker-compose.yml Normal file
View 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
View 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.

View File

@@ -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.

View File

@@ -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
View 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
View 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.

View File

@@ -1,3 +1 @@
[
"turtlebot3-burger.json"
]
["turtlebot3-burger.json", "turtlebot3-waffle.json", "nao-humanoid.json"]

315
plugins/nao-humanoid.json Normal file
View 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"]
}
}
]
}

View File

@@ -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
}
}
}
]
}
}

View 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"
}
}
}
]
}

View File

@@ -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
View 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
View 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 "$@"