mirror of
https://github.com/miyu0-bit/frameeye-helper.git
synced 2026-10-06 03:00:14 +02:00
first version
This commit is contained in:
commit
3d43bfe5ca
47 files changed
+25167
No files matched your search
Generated
+112
@@ -0,0 +1,112 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "fake-eye-server"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "fake-eye-server"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
description = "Pretends to be the Steam Frame's eye tracker (shared-memory layout v4) so frameeyeosc can run on a PC"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
@@ -0,0 +1,381 @@
|
||||
//! Pretends to be the Steam Frame's eye tracker, so the real frameeyeosc (and its Eye panel) can run on
|
||||
//! an ordinary Linux PC. It creates /dev/shm/eye-server.mmap with the private version-4 layout that
|
||||
//! frameeyeosc reads (see frameeyeosc's src/main.rs) and publishes made-up samples about 120 times a second:
|
||||
//! eyes that look around, blink and now and then wink.
|
||||
//!
|
||||
//! During the Eye panel's eye fit it plays along: it reads the capture requests the panel writes into
|
||||
//! config.json and looks at each dot (or closes its eyes), like a person following the fit would.
|
||||
//! Its "sensor" is deliberately a little off (reads low and a bit narrow vertically), so the fit has
|
||||
//! something to correct.
|
||||
//!
|
||||
//! fake-eye-server [--rate HZ] [--no-bias] [--config PATH]
|
||||
//! Stop with Ctrl+C; the shared-memory file is removed on exit.
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const PATH: &str = "/dev/shm/eye-server.mmap";
|
||||
const SHM_SIZE: usize = 0x4f21a;
|
||||
const VERSION: u32 = 4;
|
||||
const OFF_INITIALIZED: usize = 0x04;
|
||||
const OFF_MUTEX: usize = 0x08;
|
||||
const OFF_SEQUENCE: usize = 0x38;
|
||||
const OFF_REQUESTED: usize = 0x3c;
|
||||
const OFF_RECORD: usize = 0x152;
|
||||
// Inside the packed record
|
||||
const REC_STATE: usize = 0x00;
|
||||
const REC_FLAG: usize = 0x04;
|
||||
const REC_TIME: usize = 0x05;
|
||||
const REC_GAZE: usize = 0x0d;
|
||||
const REC_GAZE_COV: usize = 0x25;
|
||||
const REC_FIXATION: usize = 0x3d;
|
||||
const REC_PRE_GAZE: usize = 0x49;
|
||||
const REC_PRE_COV: usize = 0x61;
|
||||
const REC_OPENNESS: usize = 0x79;
|
||||
|
||||
const _: () = assert!(std::mem::size_of::<libc::pthread_mutex_t>() <= 0x30);
|
||||
|
||||
static STOP: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
extern "C" fn on_signal(_: libc::c_int) {
|
||||
STOP.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
struct Shm {
|
||||
base: *mut u8,
|
||||
}
|
||||
|
||||
impl Shm {
|
||||
fn create() -> io::Result<Self> {
|
||||
let _ = std::fs::remove_file(PATH);
|
||||
let file = OpenOptions::new().read(true).write(true).create_new(true).mode(0o666).open(PATH)?;
|
||||
file.set_len(SHM_SIZE as u64)?;
|
||||
use std::os::fd::AsRawFd;
|
||||
let base = unsafe {
|
||||
libc::mmap(ptr::null_mut(), SHM_SIZE, libc::PROT_READ | libc::PROT_WRITE, libc::MAP_SHARED, file.as_raw_fd(), 0)
|
||||
};
|
||||
if base == libc::MAP_FAILED {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let shm = Shm { base: base.cast() };
|
||||
unsafe {
|
||||
// A robust, process-shared mutex, like the real eye server's
|
||||
let mut attr: libc::pthread_mutexattr_t = std::mem::zeroed();
|
||||
libc::pthread_mutexattr_init(&mut attr);
|
||||
libc::pthread_mutexattr_setpshared(&mut attr, libc::PTHREAD_PROCESS_SHARED);
|
||||
libc::pthread_mutexattr_setrobust(&mut attr, libc::PTHREAD_MUTEX_ROBUST);
|
||||
libc::pthread_mutex_init(shm.mutex(), &attr);
|
||||
libc::pthread_mutexattr_destroy(&mut attr);
|
||||
shm.write_u32(0, VERSION);
|
||||
shm.write_u32(OFF_INITIALIZED, 1);
|
||||
}
|
||||
Ok(shm)
|
||||
}
|
||||
|
||||
fn mutex(&self) -> *mut libc::pthread_mutex_t {
|
||||
unsafe { self.base.add(OFF_MUTEX).cast() }
|
||||
}
|
||||
|
||||
unsafe fn write_u32(&self, offset: usize, value: u32) {
|
||||
unsafe { ptr::write_volatile(self.base.add(offset).cast::<u32>(), value) };
|
||||
}
|
||||
|
||||
unsafe fn write_bytes(&self, offset: usize, bytes: &[u8]) {
|
||||
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), self.base.add(offset), bytes.len()) };
|
||||
}
|
||||
|
||||
fn publish(&self, sample: &Sample) {
|
||||
let record = OFF_RECORD;
|
||||
let floats = |values: &[f32]| -> Vec<u8> { values.iter().flat_map(|value| value.to_le_bytes()).collect() };
|
||||
unsafe {
|
||||
let code = libc::pthread_mutex_lock(self.mutex());
|
||||
if code == libc::EOWNERDEAD {
|
||||
libc::pthread_mutex_consistent(self.mutex());
|
||||
}
|
||||
self.write_bytes(record + REC_STATE, &1u32.to_le_bytes());
|
||||
self.write_bytes(record + REC_FLAG, &[1]);
|
||||
self.write_bytes(record + REC_TIME, &sample.time.to_le_bytes());
|
||||
let gaze = [sample.left[0], sample.left[1], sample.left[2], sample.right[0], sample.right[1], sample.right[2]];
|
||||
self.write_bytes(record + REC_GAZE, &floats(&gaze));
|
||||
self.write_bytes(record + REC_GAZE_COV, &floats(&[0.005; 6]));
|
||||
self.write_bytes(record + REC_FIXATION, &floats(&sample.fixation));
|
||||
self.write_bytes(record + REC_PRE_GAZE, &floats(&gaze));
|
||||
self.write_bytes(record + REC_PRE_COV, &floats(&[0.005; 6]));
|
||||
self.write_bytes(record + REC_OPENNESS, &floats(&sample.openness));
|
||||
let sequence = self.base.add(OFF_SEQUENCE).cast::<u32>();
|
||||
ptr::write_volatile(sequence, ptr::read_volatile(sequence).wrapping_add(1));
|
||||
self.write_u32(OFF_REQUESTED, 0);
|
||||
libc::pthread_mutex_unlock(self.mutex());
|
||||
// Shared (not private) futex: frameeyeosc waits on this word from another process
|
||||
libc::syscall(libc::SYS_futex, sequence, libc::FUTEX_WAKE, i32::MAX, ptr::null::<libc::timespec>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Shm {
|
||||
fn drop(&mut self) {
|
||||
unsafe { libc::munmap(self.base.cast(), SHM_SIZE) };
|
||||
let _ = std::fs::remove_file(PATH);
|
||||
}
|
||||
}
|
||||
|
||||
struct Sample {
|
||||
time: f64,
|
||||
left: [f32; 3],
|
||||
right: [f32; 3],
|
||||
fixation: [f32; 3],
|
||||
openness: [f32; 2],
|
||||
}
|
||||
|
||||
/// A small deterministic pseudo-random generator (xorshift), so no extra crate is needed.
|
||||
struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
fn next(&mut self) -> f32 {
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
(self.0 >> 40) as f32 / (1u64 << 24) as f32
|
||||
}
|
||||
|
||||
fn noise(&mut self, size: f32) -> f32 {
|
||||
(self.next() + self.next() + self.next() - 1.5) * size
|
||||
}
|
||||
}
|
||||
|
||||
/// What the pretend person is doing.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Look {
|
||||
Wander,
|
||||
/// Following an eye-fit dot: degrees right, degrees up.
|
||||
Target(f32, f32),
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// Reads the eye-fit capture the panel asked for (config.json `gaze_capture`) and whether frameeyeosc
|
||||
/// finished it (status.json).
|
||||
struct FitWatcher {
|
||||
config: Option<PathBuf>,
|
||||
status: Option<PathBuf>,
|
||||
last_check: Instant,
|
||||
look: Look,
|
||||
/// Until when to keep holding the look after a capture finished (the dot is still up, gliding on).
|
||||
hold_until: Option<Instant>,
|
||||
active_id: i64,
|
||||
}
|
||||
|
||||
impl FitWatcher {
|
||||
fn new(config: Option<PathBuf>) -> Self {
|
||||
let config = config.or_else(|| {
|
||||
let base = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))?;
|
||||
Some(base.join("frameeyeosc").join("config.json"))
|
||||
});
|
||||
let status = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| Some(PathBuf::from(format!("/run/user/{}", unsafe { libc::getuid() }))))
|
||||
.map(|dir| dir.join("frameeyeosc").join("status.json"));
|
||||
Self {
|
||||
config,
|
||||
status,
|
||||
last_check: Instant::now() - Duration::from_secs(1),
|
||||
look: Look::Wander,
|
||||
hold_until: None,
|
||||
active_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_json(path: &Option<PathBuf>) -> Option<serde_json::Value> {
|
||||
let text = std::fs::read_to_string(path.as_ref()?).ok()?;
|
||||
serde_json::from_str(&text).ok()
|
||||
}
|
||||
|
||||
fn update(&mut self) -> Look {
|
||||
if self.last_check.elapsed() < Duration::from_millis(50) {
|
||||
return self.current();
|
||||
}
|
||||
self.last_check = Instant::now();
|
||||
let request = Self::read_json(&self.config).and_then(|config| config.get("gaze_capture").cloned());
|
||||
let id = request.as_ref().and_then(|request| request.get("id")).and_then(|id| id.as_i64()).unwrap_or(0);
|
||||
let target = request.as_ref().and_then(|request| request.get("target")).and_then(|t| t.as_str()).unwrap_or("");
|
||||
let done_id = Self::read_json(&self.status)
|
||||
.and_then(|status| status.get("gaze_capture").cloned())
|
||||
.filter(|capture| capture.get("state").and_then(|state| state.as_str()) == Some("done"))
|
||||
.and_then(|capture| capture.get("id").and_then(|id| id.as_i64()))
|
||||
.unwrap_or(0);
|
||||
if id != 0 && id != done_id {
|
||||
// A capture is running (or about to): look where it wants
|
||||
if id != self.active_id {
|
||||
self.active_id = id;
|
||||
eprintln!("[fake-eye-server] eye fit asks for \"{target}\" (capture {id}); looking there");
|
||||
}
|
||||
self.look = match target {
|
||||
"center" => Look::Target(0.0, 0.0),
|
||||
"up" => Look::Target(0.0, 15.0),
|
||||
"down" => Look::Target(0.0, -15.0),
|
||||
"left" => Look::Target(-20.0, 0.0),
|
||||
"right" => Look::Target(20.0, 0.0),
|
||||
"closed" => Look::Closed,
|
||||
_ => self.look,
|
||||
};
|
||||
self.hold_until = Some(Instant::now() + Duration::from_secs(3));
|
||||
} else if self.look == Look::Closed && id == done_id {
|
||||
// The eyes-shut step is over: open them again
|
||||
self.look = Look::Wander;
|
||||
self.hold_until = None;
|
||||
}
|
||||
self.current()
|
||||
}
|
||||
|
||||
fn current(&mut self) -> Look {
|
||||
match self.hold_until {
|
||||
Some(until) if Instant::now() > until => {
|
||||
self.hold_until = None;
|
||||
self.look = Look::Wander;
|
||||
Look::Wander
|
||||
}
|
||||
_ => self.look,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn direction(yaw_deg: f32, pitch_deg: f32) -> [f32; 3] {
|
||||
let (x, y) = (yaw_deg.to_radians().tan(), pitch_deg.to_radians().tan());
|
||||
let length = (x * x + y * y + 1.0).sqrt();
|
||||
[x / length, y / length, -1.0 / length]
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut rate = 120.0f64;
|
||||
let mut bias = true;
|
||||
let mut config = None;
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--rate" if i + 1 < args.len() => {
|
||||
rate = args[i + 1].parse().unwrap_or(rate).clamp(5.0, 500.0);
|
||||
i += 1;
|
||||
}
|
||||
"--no-bias" => bias = false,
|
||||
"--config" if i + 1 < args.len() => {
|
||||
config = Some(PathBuf::from(&args[i + 1]));
|
||||
i += 1;
|
||||
}
|
||||
"-h" | "--help" => {
|
||||
println!("fake-eye-server [--rate HZ] [--no-bias] [--config PATH]\nPretends to be the Steam Frame's eye tracker at {PATH}.");
|
||||
return;
|
||||
}
|
||||
other => {
|
||||
eprintln!("unknown option {other}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
unsafe {
|
||||
libc::signal(libc::SIGINT, on_signal as extern "C" fn(libc::c_int) as libc::sighandler_t);
|
||||
libc::signal(libc::SIGTERM, on_signal as extern "C" fn(libc::c_int) as libc::sighandler_t);
|
||||
}
|
||||
let shm = match Shm::create() {
|
||||
Ok(shm) => shm,
|
||||
Err(error) => {
|
||||
eprintln!("[fake-eye-server] can't create {PATH}: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
eprintln!("[fake-eye-server] publishing made-up eye samples at {rate:.0} Hz to {PATH} (Ctrl+C to stop)");
|
||||
|
||||
let mut rng = Rng(0x9e3779b97f4a7c15);
|
||||
let mut fit = FitWatcher::new(config);
|
||||
let start = Instant::now();
|
||||
let period = Duration::from_secs_f64(1.0 / rate);
|
||||
let mut next = Instant::now();
|
||||
// Wandering: fixations at random points, with quick saccades between them
|
||||
let (mut yaw, mut pitch) = (0.0f32, 0.0f32);
|
||||
let (mut goal_yaw, mut goal_pitch) = (0.0f32, 0.0f32);
|
||||
let mut next_saccade = 0.8f64;
|
||||
let mut next_blink = 2.5f64;
|
||||
let mut blink_start = -10.0f64;
|
||||
let mut wink_start = -10.0f64;
|
||||
let mut next_wink = 17.0f64;
|
||||
let (mut open_left, mut open_right) = (0.84f32, 0.84f32);
|
||||
|
||||
while !STOP.load(Ordering::SeqCst) {
|
||||
let t = start.elapsed().as_secs_f64();
|
||||
let look = fit.update();
|
||||
let (target_yaw, target_pitch, shut) = match look {
|
||||
Look::Wander => {
|
||||
if t > next_saccade {
|
||||
goal_yaw = rng.noise(34.0);
|
||||
goal_pitch = rng.noise(20.0) - 2.0;
|
||||
next_saccade = t + 0.6 + rng.next() as f64 * 1.6;
|
||||
}
|
||||
(goal_yaw, goal_pitch, false)
|
||||
}
|
||||
Look::Target(y, p) => (y, p, false),
|
||||
Look::Closed => (0.0, 0.0, true),
|
||||
};
|
||||
// Saccade: cover most of the way within ~50 ms
|
||||
let k = 1.0 - (-(1.0 / rate as f32) * 45.0).exp();
|
||||
yaw += (target_yaw - yaw) * k;
|
||||
pitch += (target_pitch - pitch) * k;
|
||||
|
||||
// Blinks (not while following a fit dot) and a wink now and then
|
||||
if look == Look::Wander && t > next_blink {
|
||||
blink_start = t;
|
||||
next_blink = t + 2.5 + rng.next() as f64 * 3.5;
|
||||
}
|
||||
if look == Look::Wander && t > next_wink {
|
||||
wink_start = t;
|
||||
next_wink = t + 15.0 + rng.next() as f64 * 10.0;
|
||||
}
|
||||
let blink = |since: f64| -> f32 {
|
||||
let s = (t - since) as f32;
|
||||
if s < 0.0 || s > 0.26 { 1.0 } else if s < 0.08 { 1.0 - s / 0.08 } else if s < 0.14 { 0.0 } else { (s - 0.14) / 0.12 }
|
||||
};
|
||||
let blinking = blink(blink_start);
|
||||
let winking = if (t - wink_start) < 0.6 { 0.0 } else { 1.0 };
|
||||
// The Frame reads eyes as less open when looking down
|
||||
let relaxed = 0.84 - 0.25 * (-pitch / 20.0).max(0.0);
|
||||
let (goal_left, goal_right) = if shut {
|
||||
(0.18, 0.19)
|
||||
} else {
|
||||
(0.2 + (relaxed - 0.2) * blinking, 0.2 + (relaxed * 1.02 - 0.2) * blinking * winking)
|
||||
};
|
||||
let lid_k = 1.0 - (-(1.0 / rate as f32) * 60.0).exp();
|
||||
open_left += (goal_left - open_left) * lid_k;
|
||||
open_right += (goal_right - open_right) * lid_k;
|
||||
|
||||
// What the "sensor" reports: a little noise, and (unless --no-bias) a bit low and narrow
|
||||
let (sensor_yaw, sensor_pitch) = if bias { (yaw * 0.95 + 1.0, pitch * 0.85 - 2.5) } else { (yaw, pitch) };
|
||||
let (n1, n2, n3, n4) = (rng.noise(0.5), rng.noise(0.5), rng.noise(0.5), rng.noise(0.5));
|
||||
// Each eye turns in a little toward a point about 2 m away
|
||||
let left = direction(sensor_yaw + 0.9 + n1, sensor_pitch + n2);
|
||||
let right = direction(sensor_yaw - 0.9 + n3, sensor_pitch + n4);
|
||||
let center = direction(sensor_yaw + (n1 + n3) / 2.0, sensor_pitch + (n2 + n4) / 2.0);
|
||||
let sample = Sample {
|
||||
time: 1000.0 + t,
|
||||
left,
|
||||
right,
|
||||
fixation: center.map(|c| c * 0.7),
|
||||
openness: [(open_left + rng.noise(0.01)).clamp(0.0, 1.2), (open_right + rng.noise(0.01)).clamp(0.0, 1.2)],
|
||||
};
|
||||
shm.publish(&sample);
|
||||
|
||||
next += period;
|
||||
let now = Instant::now();
|
||||
if next > now {
|
||||
std::thread::sleep(next - now);
|
||||
} else {
|
||||
next = now;
|
||||
}
|
||||
}
|
||||
eprintln!("[fake-eye-server] stopped");
|
||||
}
|
||||
Reference in new issue
Block a user