This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
Most software designed to help people disconnect from screens makes a contradictory mistake: it asks you to walk outside, pull out a glass rectangle, and look down at a screen to photograph leaves. It replaces desk-screen time with outdoor-screen time.
I wanted something different. I wanted to step onto an urban rooftop at midnight, walk away from terminal windows, and look up at the actual sky.
When you look at a smartphone in the dark, you encounter an immediate biological barrier. The human retina contains roughly 120 million rod photoreceptors that govern nocturnal night vision. These rods rely on a photopigment called rhodopsin. A single 500-millisecond glance at an illuminated smartphone screen bleaches over 80 percent of your retinal rhodopsin. Your pupils constrict from seven millimeters down to two. Rebuilding that dark adaptation requires 20 to 30 continuous minutes in total darkness.
Every mainstream astronomy app forces you to stare down at a digital simulation of stars rendered in glowing pixels. You stand under real celestial bodies while staring at a backlit slab.
I built Nakshatra (the Sanskrit word for constellation or star sector).
Nakshatra is an eyes-up astronomical radar that runs inside the mobile browser. It does not display a virtual sky map. Its display is kept at 0.000 nits (solid #000000 on OLED panels) with low-intensity deep-red telemetry ($\lambda > 650\text{ nm}$) that preserves retinal rhodopsin.
You hold the phone flat against your chest. A dual-oscillator acoustic radar hums in your earbuds, rising in pitch as your torso turns toward an invisible celestial body. When you align with the star, a chime sounds, and a whispered voice directs you to bring the phone to your chin. You tilt your head back, look directly into the night sky, and listen as local Google Gemma whispers the lore of the star into your ears.
Your eyes never leave the sky.
Figure 1: Nakshatra running in pitch darkness. On OLED panels, unused subpixels turn off completely (0.000 nits). Red elements use wavelengths above 650nm to protect rod photoreceptors.
npm start --prefix client (Runs at http://localhost:3000)
Figure 2: The two-phase posture: holding the phone flat at chest height to sweep the horizon, then raising it to the chin to gaze directly upward.
npm test --prefix client).
Building for an idealized browser simulator is straightforward. Deploying that same software on a physical smartphone standing on a cold concrete roof reveals immediate hardware hurdles.
Here is how the system is engineered from the ground up.
Holding a smartphone upward at arm's length for two minutes causes rapid muscle fatigue and tremors. Nakshatra decouples horizontal azimuth alignment from vertical altitude targeting into two physical postures:
graph LR
subgraph Phase_1 ["Phase 1: Chest-Level Sweep"]
direction TB
P1_Action["Phone flat in palm at chest (pitch <= 25°)"]
P1_Sound["Acoustic sonar hums 220Hz-440Hz"]
P1_Lock["Dwell within 6° for 1000ms locks heading"]
P1_Voice["Whisper: 'Bring phone to your chin'"]
P1_Action --> P1_Sound --> P1_Lock --> P1_Voice
end
subgraph Phase_2 ["Phase 2: Chin-Level Tilt"]
direction TB
P2_Action["Phone raised to chin (pitch > 25°)"]
P2_Cue["Whisper: 'Tilt your head up slightly'"]
P2_Lock["Pitch matches altitude within 3°"]
P2_Voice["Lock chime: 'Stop right there'"]
P2_Action --> P2_Cue --> P2_Lock --> P2_Voice
end
subgraph Phase_3 ["Phase 3: Dark Observation"]
direction TB
P3_Screen["Screen solid black (0.000 nits)"]
P3_Gemma["Local Gemma whispers celestial lore"]
P3_Screen --> P3_Gemma
end
P1_Voice --> P2_Action
P2_Voice --> P3_Screen
On mobile Chromium on mid-tier Android devices, calling event.acceleration returns {x: 0, y: 0, z: 0} without throwing an error. The API appears active, but reports zero motion. Any code relying on linear acceleration to track physical movement fails silently.
To detect when a user is walking versus standing still, I read event.accelerationIncludingGravity, calculate the raw Euclidean magnitude, and strip Earth's gravitational acceleration:
handleMotion(event) {
if (!event) return;
let dynamicMag = 0;
let hasLinearAcc = false;
if (event.acceleration && typeof event.acceleration.x === 'number' && event.acceleration.x !== null) {
const ax = Number(event.acceleration.x) || 0;
const ay = Number(event.acceleration.y) || 0;
const az = Number(event.acceleration.z) || 0;
const mag = Math.sqrt(ax * ax + ay * ay + az * az);
if (mag > 0.05) {
dynamicMag = mag;
hasLinearAcc = true;
}
}
if (!hasLinearAcc && event.accelerationIncludingGravity) {
const ax = Number(event.accelerationIncludingGravity.x) || 0;
const ay = Number(event.accelerationIncludingGravity.y) || 0;
const az = Number(event.accelerationIncludingGravity.z) || 0;
const rawMag = Math.sqrt(ax * ax + ay * ay + az * az);
dynamicMag = Math.abs(rawMag - 9.81);
}
const now = Date.now();
if (dynamicMag > 0.50 && (now - this._lastStepTime > 300)) {
this._lastStepTime = now;
this.stepsDetected += 1;
this.displacementMeters += 0.65;
}
}
This filter detects real footsteps and resets tracking if the user walks away from their observation point.
Smartphone magnetometers experience continuous jitter between eight and twelve degrees caused by rooftop rebar and sensor noise. If you instruct an application to guide a user toward an exact single-degree coordinate, the audio radar chatters constantly.
Furthermore, celestial targets between zero and fifteen degrees altitude are almost always obstructed by apartment buildings, power lines, or streetlights.
I resolved both issues in quadrant_selector.js:
selectTargetForHeading(currentHeadingAz, allVisibleTargets) {
if (!allVisibleTargets || allVisibleTargets.length === 0) {
return null;
}
const currentQuad = this.getQuadrant(currentHeadingAz);
if (
this.latchedTarget &&
this.activeQuadrant === currentQuad &&
!this.isTargetCooledDown(this.latchedTarget.name)
) {
return this.latchedTarget;
}
const inQuadrant = allVisibleTargets.filter((t) => {
if (typeof t.azimuth !== 'number' || typeof t.altitude !== 'number') {
return false;
}
if (t.altitude < this.minAltitudeDeg) {
return false;
}
return this.getQuadrant(t.azimuth) === currentQuad;
});
if (inQuadrant.length === 0) {
return null;
}
const uncooled = inQuadrant.filter((t) => !this.isTargetCooledDown(t.name));
const candidatePool = uncooled.length > 0 ? uncooled : inQuadrant;
const chosen = this._pickWeightedCandidate(candidatePool);
this.latchedTarget = chosen;
this.activeQuadrant = currentQuad;
return chosen;
}
My initial prototype used a strict 15-degree heading margin between the chest sweep and the chin tilt phase.
In outdoor testing, this caused nine out of ten attempts to abort. When a human raises their hands from chest level to their chin, the elbow hinge naturally rotates the wrist. This mechanical arc swings the internal phone compass by 25 to 35 degrees during transit.
I relaxed the heading abort tolerance to 45 degrees during the lift motion and implemented a 1.2-second post-lift gyroscopic stabilization dampener. The software now conforms to human anatomy instead of treating the user like an industrial robotic arm.
A core requirement of Nakshatra was to eliminate robotic coordinate readouts. Hearing a synthesized voice say "Azimuth 214 degrees, pitch 42 degrees, Right Ascension 18 hours" pulls the user out of contemplation and back into analytical tension.
graph TD
subgraph Tier_1 ["1. Mobile Sensor Hardening (50Hz)"]
S_Orient["DeviceOrientation (Compass & Pitch)"] --> S_Filter["Low-Pass Filter (alpha = 0.15)"]
S_Motion["DeviceMotion (Raw Accelerometer)"] --> S_Gravity["Gravity Decomposition Filter"]
S_Filter --> S_Fusion["Calibrated Pose & Motion Vectors"]
S_Gravity --> S_Fusion
end
subgraph Tier_2 ["2. Astronomical Ephemeris Engine"]
E_Catalog["80+ Celestial Body Catalog"] --> E_Math["Sidereal & Topocentric Math"]
S_Fusion --> E_Math
E_Math --> E_Quad["4-Quadrant Selector (18° Skyline Floor)"]
E_Quad --> E_Latch["Target Latching & 30-Minute Cooldown"]
end
subgraph Tier_3 ["3. Intelligence & Audio Synthesis"]
E_Latch --> A_Radar["Web Audio Radar (220Hz-440Hz)"]
E_Latch --> W_Gemma["On-Device Gemma 3 (WebGPU Worker)"]
W_Gemma -->|Memory limits| W_Lore["Deterministic Lore Database"]
W_Gemma --> A_Speech["Sub-15ms Whispered Speech Synthesizer"]
W_Lore --> A_Speech
A_Radar --> A_Speech
end
Nakshatra uses a dual-engine architecture to generate body-relative celestial phrasing:
gemma-3-270m-it-ONNX via Web Worker or Chrome's native Prompt API). All weight , token caching, and generation run off the main JavaScript thread, ensuring the audio oscillators never stutter.
All model outputs pass through a deterministic validation filter that checks for forbidden technical terms:
const FORBIDDEN_JARGON_REGEX = /\b(azimuth|altitude|right\s+ascension|declination|degree[s]?|arcminute[s]?|arcsecond[s]?|bearing|southeast|northeast|southwest|northwest|fist[s]?|clenched)\b/i;
Unit tests in an IDE pass easily. Real hardware outdoors fails in unexpected ways.
Testing under real skies caught three critical edge-case bugs:
sensors.js using shortest-angle modulo wrapping:
shortestAngleDiff(target, source) {
let diff = ((target - source + 180) % 360) - 180;
if (diff < -180) diff += 360;
return diff;
}
The 81 automated tests in npm test execute in 1.3 seconds via Node's native test runner, ensuring these mathematical edge cases remain guarded against regression.
Open Innovation Matter?
Open-source and open-weight AI models are vital for tools designed for the physical outdoors.
When you stand in a dark-sky preserve, on a mountain ridgeline, or on a remote trail, cellular towers do not reach you. A proprietary cloud application that depends on remote inference servers fails the moment connectivity drops.
Google Gemma models running locally on client hardware fundamentally change outdoor software: