Observe scheduled Move hues through PSMoveAPI

This commit is contained in:
Meta Vens 2026-07-12 22:07:41 +01:00
commit 8dc7dcd172
4 changed files with 172 additions and 9 deletions

View file

@ -4255,13 +4255,29 @@ fn publish_move_controller_states(
}
fn active_move_marker_camera_sources(options: &Options) -> Vec<ActiveMoveMarkerCameraSource> {
let move_colors: Vec<(String, [u8; 3])> = serve_move_state_sources(options, true)
let mut move_roster = serve_move_state_sources(options, true)
.into_iter()
.map(|source| {
let (red, green, blue) = default_move_color_for_identity(&source.move_id);
(source.move_id, [red, green, blue])
.map(|source| source.move_id)
.collect::<Vec<_>>();
move_roster.sort();
move_roster.dedup();
let now_ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as i128;
let move_colors = move_roster
.iter()
.filter_map(|identity| {
scheduled_golden_move_color(
identity,
&move_roster,
0,
i128::from(options.move_hue_cycle_ms) * 1_000_000,
now_ns,
)
.map(|(color, _, _)| (identity.clone(), [color.0, color.1, color.2]))
})
.collect();
.collect::<Vec<_>>();
options
.move_marker_camera_sources
.iter()
@ -4278,6 +4294,7 @@ fn active_move_marker_camera_sources(options: &Options) -> Vec<ActiveMoveMarkerC
camera_index,
options.move_tracker_exposure_milli as f32 / 1000.0,
move_colors.clone(),
options.move_hue_cycle_ms,
))
})
} else {
@ -4384,6 +4401,7 @@ fn start_psmoveapi_tracker_worker(
camera_index: i32,
exposure: f32,
colors: Vec<(String, [u8; 3])>,
move_hue_cycle_ms: u64,
) -> mpsc::Receiver<Vec<muninn_psmoveapi_tracker::PsmoveApiObservation>> {
let (sender, receiver) = mpsc::sync_channel(1);
thread::spawn(move || {
@ -4401,9 +4419,26 @@ fn start_psmoveapi_tracker_worker(
let mut last_calibration_at = Instant::now();
loop {
if last_calibration_at.elapsed() >= Duration::from_secs(30) {
tracker.calibrate_connected();
tracker.observe_connected();
last_calibration_at = Instant::now();
}
let mut roster = colors.iter().map(|(identity, _)| identity.clone()).collect::<Vec<_>>();
roster.sort();
let now_ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as i128;
for identity in &roster {
if let Some((color, _, _)) = scheduled_golden_move_color(
identity,
&roster,
0,
i128::from(move_hue_cycle_ms) * 1_000_000,
now_ns,
) {
tracker.set_expected_color(identity, [color.0, color.1, color.2]);
}
}
let observations = tracker.update();
match sender.try_send(observations) {
Ok(()) | Err(mpsc::TrySendError::Full(_)) => {}

View file

@ -52,13 +52,20 @@ mod linux {
fn psmove_disconnect(controller: *mut c_void);
fn psmove_tracker_new_with_camera(camera: c_int) -> *mut c_void;
fn psmove_tracker_free(tracker: *mut c_void);
fn psmove_tracker_enable_with_color(
fn psmove_tracker_enable_color_observer(
tracker: *mut c_void,
controller: *mut c_void,
red: c_uchar,
green: c_uchar,
blue: c_uchar,
) -> c_int;
fn psmove_tracker_set_expected_color(
tracker: *mut c_void,
controller: *mut c_void,
red: c_uchar,
green: c_uchar,
blue: c_uchar,
) -> bool;
fn psmove_tracker_disable(tracker: *mut c_void, controller: *mut c_void);
fn psmove_tracker_set_exposure(tracker: *mut c_void, exposure: c_float);
fn psmove_tracker_get_exposure(tracker: *mut c_void) -> c_float;
@ -117,7 +124,7 @@ mod linux {
exposure,
},
};
result.calibrate_connected();
result.observe_connected();
result.camera_info = unsafe {
let info = psmove_tracker_get_camera_info(tracker);
if info.is_null() {
@ -139,6 +146,10 @@ mod linux {
}
pub fn calibrate_connected(&mut self) -> usize {
self.observe_connected()
}
pub fn observe_connected(&mut self) -> usize {
let count = unsafe { psmove_count_connected() }.max(0);
for index in 0..count {
let handle = unsafe { psmove_connect_by_id(index) };
@ -158,7 +169,7 @@ mod linux {
continue;
};
let status = unsafe {
psmove_tracker_enable_with_color(
psmove_tracker_enable_color_observer(
self.tracker, handle, color[0], color[1], color[2]
)
};
@ -172,6 +183,23 @@ mod linux {
self.controllers.len()
}
pub fn set_expected_color(&mut self, move_id: &str, color: [u8; 3]) -> bool {
let Some(controller) = self.controllers.iter().find(|controller| {
controller.move_id.eq_ignore_ascii_case(move_id)
}) else {
return false;
};
unsafe {
psmove_tracker_set_expected_color(
self.tracker,
controller.handle,
color[0],
color[1],
color[2],
)
}
}
pub fn camera_info(&self) -> &PsmoveApiCameraInfo {
&self.camera_info
}
@ -255,6 +283,10 @@ impl PsmoveApiTracker {
pub fn calibrate_connected(&mut self) -> usize { 0 }
pub fn observe_connected(&mut self) -> usize { 0 }
pub fn set_expected_color(&mut self, _move_id: &str, _color: [u8; 3]) -> bool { false }
pub fn tracked_controller_count(&self) -> usize { 0 }
pub fn update(&mut self) -> Vec<PsmoveApiObservation> {

View file

@ -0,0 +1,13 @@
# Muninn PSMoveAPI Observer Patch
Muninn owns PS Move LED output. PSMoveAPI owns camera exposure, HSV filtering,
morphology, contour extraction, and marker position estimation.
`expected-color-observer.patch` adds only the missing authority boundary:
- enable tracking without blinking or writing LEDs;
- update the expected camera hue without writing LEDs.
The expected hue comes from Muninn's timestamp-driven `gold(x)` schedule. This
patch must be applied to the pinned PSMoveAPI source with `git apply --recount`
before building the Nightwing tracker libraries.

View file

@ -0,0 +1,83 @@
diff --git a/include/psmove_tracker.h b/include/psmove_tracker.h
index 7de998b..d44db20 100644
--- a/include/psmove_tracker.h
+++ b/include/psmove_tracker.h
@@ -235,6 +235,25 @@ ADDCALL psmove_tracker_get_auto_update_leds(PSMoveTracker *tracker, PSMove *move
ADDAPI void
ADDCALL psmove_tracker_set_exposure(PSMoveTracker *tracker, float exposure);
+/**
+ * Enable reference blob tracking without writing controller LEDs.
+ * The caller owns LED output and supplies the expected fully-saturated RGB hue.
+ */
+ADDAPI enum PSMoveTracker_Status
+ADDCALL psmove_tracker_enable_color_observer(PSMoveTracker *tracker, PSMove *move,
+ unsigned char r, unsigned char g, unsigned char b);
+
+/**
+ * Change the expected RGB hue for an observer-enabled controller without
+ * writing controller LEDs or running blinking calibration.
+ */
+ADDAPI bool
+ADDCALL psmove_tracker_set_expected_color(PSMoveTracker *tracker, PSMove *move,
+ unsigned char r, unsigned char g, unsigned char b);
+
/**
* \brief Get exposure as fraction (0.0 -> 1.0)
**/
diff --git a/src/tracker/psmove_tracker.cpp b/src/tracker/psmove_tracker.cpp
index 23e33ed..621b9ea 100644
--- a/src/tracker/psmove_tracker.cpp
+++ b/src/tracker/psmove_tracker.cpp
@@ -547,6 +547,49 @@ psmove_tracker_get_exposure(PSMoveTracker *tracker)
return tracker->settings.camera_exposure;
}
+static void
+psmove_tracker_assign_observed_color(TrackedController *tc,
+ unsigned char r, unsigned char g, unsigned char b)
+{
+ struct PSMove_RGBValue rgb = { r, g, b };
+ CvScalar hsv = th_rgb2hsv(cvScalar(r, g, b, 255.0));
+ tc->color = rgb;
+ tc->auto_update_leds = false;
+ tc->eColorHSV = tc->eFColorHSV = hsv;
+ tc->assignedHSV = hsv;
+}
+
+enum PSMoveTracker_Status
+psmove_tracker_enable_color_observer(PSMoveTracker *tracker, PSMove *move,
+ unsigned char r, unsigned char g, unsigned char b)
+{
+ psmove_return_val_if_fail(tracker != NULL, Tracker_CALIBRATION_ERROR);
+ psmove_return_val_if_fail(move != NULL, Tracker_CALIBRATION_ERROR);
+
+ TrackedController *tc = psmove_tracker_find_controller(tracker, move);
+ if (tc == NULL) {
+ tc = psmove_tracker_find_controller(tracker, NULL);
+ }
+ if (tc == NULL) {
+ return Tracker_CALIBRATION_ERROR;
+ }
+ tc->move = move;
+ psmove_tracker_assign_observed_color(tc, r, g, b);
+ return Tracker_CALIBRATED;
+}
+
+bool
+psmove_tracker_set_expected_color(PSMoveTracker *tracker, PSMove *move,
+ unsigned char r, unsigned char g, unsigned char b)
+{
+ psmove_return_val_if_fail(tracker != NULL, false);
+ psmove_return_val_if_fail(move != NULL, false);
+ TrackedController *tc = psmove_tracker_find_controller(tracker, move);
+ if (tc == NULL) {
+ return false;
+ }
+ psmove_tracker_assign_observed_color(tc, r, g, b);
+ return true;
+}
+
void
psmove_tracker_reset_color_calibration(PSMoveTracker *tracker)
{