feat: Add localization template with grid-based sensor fusion
Implements comprehensive robot localization system as third template option. Teams can now start with professional positioning and navigation code. Template Features: - 12x12 field grid system (12-inch cells) - Multi-sensor fusion (encoders, IMU, vision) - Kalman-filter-style sensor combination - Fault-tolerant positioning (graceful degradation) - 21 files, ~1,500 lines, 3 passing tests Core Components: - GridCell/Pose2D/FieldGrid - Coordinate system - RobotLocalizer - Sensor fusion engine - OdometryTracker - Dead reckoning from encoders - ImuLocalizer - Heading from gyroscope - VisionLocalizer - Position from AprilTags Sensor Fusion Strategy: Priority 1: Vision (AprilTags) → ±2" accuracy, 100% confidence Priority 2: IMU + Odometry → ±4" accuracy, 70% confidence Priority 3: Odometry only → ±12" accuracy, 40% confidence System gracefully degrades when sensors fail, maintaining operation even with partial sensor availability. Hardware Abstraction: - Interfaces (Encoder, GyroSensor, VisionCamera) - Mock implementations for unit testing - Teams implement FTC wrappers for their hardware Documentation: - LOCALIZATION_GUIDE.md - System architecture and usage - GRID_SYSTEM.md - Field coordinate reference - README.md - Quick start and examples Usage: weevil new my-robot --template localization cd my-robot ./gradlew test # 3 tests pass in < 1 second This teaches professional robotics concepts (sensor fusion, fault tolerance, coordinate systems) not found in other FTC tools. Positions Nexus Workshops as teaching advanced autonomous programming. Updated src/templates/mod.rs to register localization template with proper metadata and feature descriptions. All tests passing (10/10 template tests).
This commit is contained in:
@@ -8,6 +8,7 @@ use colored::*;
|
|||||||
// Embed template directories at compile time
|
// Embed template directories at compile time
|
||||||
static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic");
|
static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic");
|
||||||
static TESTING_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/testing");
|
static TESTING_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/testing");
|
||||||
|
static LOCALIZATION_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/localization");
|
||||||
|
|
||||||
pub struct TemplateManager {
|
pub struct TemplateManager {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -39,13 +40,14 @@ impl TemplateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn template_exists(&self, name: &str) -> bool {
|
pub fn template_exists(&self, name: &str) -> bool {
|
||||||
matches!(name, "basic" | "testing")
|
matches!(name, "basic" | "testing" | "localization")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_templates(&self) -> Vec<String> {
|
pub fn list_templates(&self) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
" basic - Minimal FTC project (default)".to_string(),
|
" basic - Minimal FTC project (default)".to_string(),
|
||||||
" testing - Testing showcase with examples".to_string(),
|
" testing - Testing showcase with examples".to_string(),
|
||||||
|
" localization - Grid-based positioning with sensor fusion".to_string(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,14 @@ impl TemplateManager {
|
|||||||
test_count: 45,
|
test_count: 45,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
},
|
},
|
||||||
|
TemplateInfo {
|
||||||
|
name: "localization".to_string(),
|
||||||
|
description: "Grid-based robot localization with sensor fusion".to_string(),
|
||||||
|
file_count: 21,
|
||||||
|
line_count: 1500,
|
||||||
|
test_count: 3,
|
||||||
|
is_default: false,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +100,14 @@ impl TemplateManager {
|
|||||||
test_count: 45,
|
test_count: 45,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
},
|
},
|
||||||
|
"localization" => TemplateInfo {
|
||||||
|
name: "localization".to_string(),
|
||||||
|
description: "Grid-based robot localization system with multi-sensor fusion and fault tolerance.".to_string(),
|
||||||
|
file_count: 21,
|
||||||
|
line_count: 1500,
|
||||||
|
test_count: 3,
|
||||||
|
is_default: false,
|
||||||
|
},
|
||||||
_ => bail!("Unknown template: {}", name),
|
_ => bail!("Unknown template: {}", name),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,6 +127,16 @@ impl TemplateManager {
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if info.name == "localization" {
|
||||||
|
println!("{}", "Features:".bright_white().bold());
|
||||||
|
println!(" • 12x12 field grid system (12-inch cells)");
|
||||||
|
println!(" • Multi-sensor fusion (encoders + IMU + vision)");
|
||||||
|
println!(" • Fault-tolerant positioning (graceful degradation)");
|
||||||
|
println!(" • Kalman-filter-style sensor fusion");
|
||||||
|
println!(" • Professional robotics patterns");
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
println!("{}", "Files included:".bright_white().bold());
|
println!("{}", "Files included:".bright_white().bold());
|
||||||
println!(" {} files", info.file_count);
|
println!(" {} files", info.file_count);
|
||||||
println!(" ~{} lines of code", info.line_count);
|
println!(" ~{} lines of code", info.line_count);
|
||||||
@@ -132,6 +160,7 @@ impl TemplateManager {
|
|||||||
let template_dir = match template_name {
|
let template_dir = match template_name {
|
||||||
"basic" => &BASIC_TEMPLATE,
|
"basic" => &BASIC_TEMPLATE,
|
||||||
"testing" => &TESTING_TEMPLATE,
|
"testing" => &TESTING_TEMPLATE,
|
||||||
|
"localization" => &LOCALIZATION_TEMPLATE,
|
||||||
_ => bail!("Unknown template: {}", template_name),
|
_ => bail!("Unknown template: {}", template_name),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,6 +262,7 @@ mod tests {
|
|||||||
let mgr = TemplateManager::new().unwrap();
|
let mgr = TemplateManager::new().unwrap();
|
||||||
assert!(mgr.template_exists("basic"));
|
assert!(mgr.template_exists("basic"));
|
||||||
assert!(mgr.template_exists("testing"));
|
assert!(mgr.template_exists("testing"));
|
||||||
|
assert!(mgr.template_exists("localization"));
|
||||||
assert!(!mgr.template_exists("nonexistent"));
|
assert!(!mgr.template_exists("nonexistent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +270,19 @@ mod tests {
|
|||||||
fn test_list_templates() {
|
fn test_list_templates() {
|
||||||
let mgr = TemplateManager::new().unwrap();
|
let mgr = TemplateManager::new().unwrap();
|
||||||
let templates = mgr.list_templates();
|
let templates = mgr.list_templates();
|
||||||
assert_eq!(templates.len(), 2);
|
assert_eq!(templates.len(), 3);
|
||||||
|
assert!(templates[0].contains("basic"));
|
||||||
|
assert!(templates[1].contains("testing"));
|
||||||
|
assert!(templates[2].contains("localization"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_template_info_all() {
|
||||||
|
let mgr = TemplateManager::new().unwrap();
|
||||||
|
let infos = mgr.get_template_info_all().unwrap();
|
||||||
|
assert_eq!(infos.len(), 3);
|
||||||
|
assert_eq!(infos[0].name, "basic");
|
||||||
|
assert_eq!(infos[1].name, "testing");
|
||||||
|
assert_eq!(infos[2].name, "localization");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ Grid-based robot localization with sensor fusion and fault tolerance.
|
|||||||
- **Grid System** - 12x12 field grid (12" cells)
|
- **Grid System** - 12x12 field grid (12" cells)
|
||||||
- **Sensor Fusion** - Combine encoders, IMU, vision
|
- **Sensor Fusion** - Combine encoders, IMU, vision
|
||||||
- **Fault Tolerance** - Graceful sensor failure handling
|
- **Fault Tolerance** - Graceful sensor failure handling
|
||||||
- **20+ Tests** - All passing
|
- **3 Tests** - All passing
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
9 . . . . . . . . . . . .
|
9 . . . . . . . . . . . .
|
||||||
8 . . . . . . . . . . . .
|
8 . . . . . . . . . . . .
|
||||||
7 . . . . . . . . . . . .
|
7 . . . . . . . . . . . .
|
||||||
6 . . . . . . X . . . . .
|
6 . . . . . X . . . . . .
|
||||||
5 . . . . . . . . . . . .
|
5 . . . . . . . . . . . .
|
||||||
4 . . . . . . . . . . . .
|
4 . . . . . . . . . . . .
|
||||||
3 . . . . . . . . . . . .
|
3 . . . . . . . . . . . .
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
rootProject.name = '{{PROJECT_NAME}}'
|
|
||||||
17
templates/localization/settings.gradle.kts.template
Normal file
17
templates/localization/settings.gradle.kts.template
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "{{PROJECT_NAME}}"
|
||||||
@@ -4,4 +4,5 @@ public interface GyroSensor {
|
|||||||
double getHeading();
|
double getHeading();
|
||||||
boolean isConnected();
|
boolean isConnected();
|
||||||
void calibrate();
|
void calibrate();
|
||||||
|
boolean isCalibrated();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,17 @@ public class ImuLocalizer {
|
|||||||
|
|
||||||
public void calibrate(double initialHeading) {
|
public void calibrate(double initialHeading) {
|
||||||
if (gyro.isConnected()) {
|
if (gyro.isConnected()) {
|
||||||
|
gyro.calibrate();
|
||||||
this.headingOffset = initialHeading - gyro.getHeading();
|
this.headingOffset = initialHeading - gyro.getHeading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Double getHeading() {
|
public Double getHeading() {
|
||||||
if (!gyro.isConnected()) return null;
|
if (!gyro.isConnected() || !gyro.isCalibrated()) return null;
|
||||||
return Pose2D.normalizeAngle(gyro.getHeading() + headingOffset);
|
return Pose2D.normalizeAngle(gyro.getHeading() + headingOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWorking() { return gyro.isConnected(); }
|
public boolean isWorking() {
|
||||||
|
return gyro.isConnected() && gyro.isCalibrated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package robot.hardware;
|
|||||||
public class MockGyroSensor implements GyroSensor {
|
public class MockGyroSensor implements GyroSensor {
|
||||||
private double heading = 0;
|
private double heading = 0;
|
||||||
private boolean connected = true;
|
private boolean connected = true;
|
||||||
|
private boolean calibrated = true;
|
||||||
|
|
||||||
public double getHeading() { return heading; }
|
public double getHeading() { return heading; }
|
||||||
public boolean isConnected() { return connected; }
|
public boolean isConnected() { return connected; }
|
||||||
public void calibrate() { }
|
public void calibrate() { calibrated = true; }
|
||||||
|
public boolean isCalibrated() { return calibrated; }
|
||||||
|
|
||||||
public void setHeading(double h) { heading = h; }
|
public void setHeading(double h) { heading = h; }
|
||||||
public void setConnected(boolean c) { connected = c; }
|
public void setConnected(boolean c) { connected = c; }
|
||||||
|
public void setCalibrated(boolean c) { calibrated = c; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,15 @@ class SensorFusionTest {
|
|||||||
MockGyroSensor gyro = new MockGyroSensor();
|
MockGyroSensor gyro = new MockGyroSensor();
|
||||||
MockVisionCamera camera = new MockVisionCamera();
|
MockVisionCamera camera = new MockVisionCamera();
|
||||||
|
|
||||||
|
// Set a pose so vision is actually "working" (not just connected)
|
||||||
|
camera.setPose(new Pose2D(12, 12, 0));
|
||||||
|
|
||||||
OdometryTracker odometry = new OdometryTracker(left, right);
|
OdometryTracker odometry = new OdometryTracker(left, right);
|
||||||
ImuLocalizer imu = new ImuLocalizer(gyro);
|
ImuLocalizer imu = new ImuLocalizer(gyro);
|
||||||
VisionLocalizer vision = new VisionLocalizer(camera);
|
VisionLocalizer vision = new VisionLocalizer(camera);
|
||||||
|
|
||||||
RobotLocalizer localizer = new RobotLocalizer(odometry, imu, vision);
|
RobotLocalizer localizer = new RobotLocalizer(odometry, imu, vision);
|
||||||
|
localizer.update(); // Need to update to actually use vision
|
||||||
|
|
||||||
assertEquals(1.0, localizer.getConfidence(), 0.01);
|
assertEquals(1.0, localizer.getConfidence(), 0.01);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user