test-card/src/routes/(pages)/sensors/+page.svelte

545 lines
16 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import { m } from '$lib/paraglide/messages';
// Geolocation
let geoWatchId: number | null = $state(null);
let geolocation: GeolocationPosition | null = $state(null);
let geoError: string | null = $state(null);
let geoSupported = $state<boolean>(false);
// DeviceMotion / DeviceOrientation (useful fallbacks on iOS/Safari)
type Motion = { ax?: number; ay?: number; az?: number; gx?: number; gy?: number; gz?: number };
let deviceMotion: Motion | null = $state(null);
let deviceOrientation: { alpha?: number; beta?: number; gamma?: number } | null = $state(null);
let motionSupported = $state(false);
let orientationSupported = $state(false);
// iOS Safari permission flow for motion/orientation
let motionPermissionAvailable = $state(false);
let orientationPermissionAvailable = $state(false);
let motionPermission: 'unknown' | 'granted' | 'denied' = $state('unknown');
let orientationPermission: 'unknown' | 'granted' | 'denied' = $state('unknown');
// Generic Sensor API (subject to browser/flag support)
type SensorHandle = {
instance?: any;
supported: boolean;
active: boolean;
error?: string | null;
data: Record<string, number | string | undefined>;
};
let accelerometer: SensorHandle = $state({ supported: false, active: false, data: {} });
let gyroscope: SensorHandle = $state({ supported: false, active: false, data: {} });
let magnetometer: SensorHandle = $state({ supported: false, active: false, data: {} });
let ambientLight: SensorHandle = $state({ supported: false, active: false, data: {} });
let barometer: SensorHandle = $state({ supported: false, active: false, data: {} });
const w = browser ? (window as any) : undefined;
function detectSupport() {
geoSupported = browser && 'geolocation' in navigator;
motionSupported = browser && 'DeviceMotionEvent' in (window as any);
orientationSupported = browser && 'DeviceOrientationEvent' in (window as any);
accelerometer.supported = Boolean(w?.Accelerometer);
gyroscope.supported = Boolean(w?.Gyroscope);
magnetometer.supported = Boolean(w?.Magnetometer);
ambientLight.supported = Boolean(w?.AmbientLightSensor);
barometer.supported = Boolean(w?.Barometer);
}
onMount(() => {
detectSupport();
// Check for iOS-style permission request APIs
motionPermissionAvailable =
browser &&
typeof (window as any).DeviceMotionEvent !== 'undefined' &&
typeof (DeviceMotionEvent as any).requestPermission === 'function';
orientationPermissionAvailable =
browser &&
typeof (window as any).DeviceOrientationEvent !== 'undefined' &&
typeof (DeviceOrientationEvent as any).requestPermission === 'function';
if (orientationSupported) {
const handler = (e: DeviceOrientationEvent) => {
deviceOrientation = {
alpha: e.alpha ?? undefined,
beta: e.beta ?? undefined,
gamma: e.gamma ?? undefined
};
};
window.addEventListener('deviceorientation', handler);
onDestroy(() => window.removeEventListener('deviceorientation', handler));
}
if (motionSupported) {
const handler = (e: DeviceMotionEvent) => {
deviceMotion = {
ax: e.acceleration?.x ?? undefined,
ay: e.acceleration?.y ?? undefined,
az: e.acceleration?.z ?? undefined,
gx: e.rotationRate?.alpha ?? undefined,
gy: e.rotationRate?.beta ?? undefined,
gz: e.rotationRate?.gamma ?? undefined
};
};
window.addEventListener('devicemotion', handler);
onDestroy(() => window.removeEventListener('devicemotion', handler));
}
});
onDestroy(() => {
stopGeolocation();
stopSensor(accelerometer);
stopSensor(gyroscope);
stopSensor(magnetometer);
stopSensor(ambientLight);
stopSensor(barometer);
});
// (Permissions are requested implicitly when starting sensors where applicable)
// Geolocation controls
async function startGeolocation() {
if (!geoSupported) return;
try {
geoError = null;
geolocation = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000
});
});
geoWatchId = navigator.geolocation.watchPosition(
(pos) => (geolocation = pos),
(err) => (geoError = err?.message || String(err)),
{ enableHighAccuracy: true }
);
} catch (e: any) {
geoError = e?.message ?? String(e);
}
}
function stopGeolocation() {
if (geoWatchId != null) {
navigator.geolocation.clearWatch(geoWatchId);
geoWatchId = null;
}
}
// Generic Sensor helpers
function startSensor(handle: SensorHandle, ctorName: string, options: any = { frequency: 60 }) {
try {
handle.error = null;
if (!w?.[ctorName]) {
handle.supported = false;
return;
}
handle.instance = new w[ctorName](options);
handle.instance.addEventListener('reading', () => {
// Populate based on sensor type
if (ctorName === 'Accelerometer') {
handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
} else if (ctorName === 'Gyroscope') {
handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
} else if (ctorName === 'Magnetometer') {
handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
} else if (ctorName === 'AmbientLightSensor') {
handle.data = { illuminance: handle.instance.illuminance };
} else if (ctorName === 'Barometer') {
handle.data = {
pressure: handle.instance.pressure,
temperature: handle.instance?.temperature
};
}
});
handle.instance.addEventListener('error', (event: any) => {
handle.error = event?.error?.message || String(event);
});
handle.instance.start();
handle.active = true;
} catch (e: any) {
handle.error = e?.message ?? String(e);
handle.active = false;
}
}
function stopSensor(handle: SensorHandle) {
try {
handle.instance?.stop?.();
} catch {}
handle.active = false;
}
// UI helpers
function toFixed(n: number | undefined, digits = 2) {
return typeof n === 'number' && Number.isFinite(n) ? n.toFixed(digits) : '—';
}
async function copyJSON(data: unknown) {
try {
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
alert(m.sensors_copied());
} catch {}
}
async function requestMotionOrientation() {
try {
if (motionPermissionAvailable) {
const res = await (DeviceMotionEvent as any).requestPermission();
motionPermission = res === 'granted' ? 'granted' : 'denied';
}
if (orientationPermissionAvailable) {
const res2 = await (DeviceOrientationEvent as any).requestPermission();
orientationPermission = res2 === 'granted' ? 'granted' : 'denied';
}
} catch (_) {
if (motionPermissionAvailable && motionPermission === 'unknown') motionPermission = 'denied';
if (orientationPermissionAvailable && orientationPermission === 'unknown')
orientationPermission = 'denied';
}
}
// Kick off light permission checks lazily in UI; starting sensors will request permissions where needed.
</script>
<h2><i class="ti ti-cpu-2"></i> {m.sensors_title()}</h2>
<div class="sections">
{#if motionPermissionAvailable || orientationPermissionAvailable}
<section>
<h3>{m.sensors_permissions()}</h3>
<div class="row">
<button onclick={requestMotionOrientation}
><i class="ti ti-key"></i> {m.sensors_enableMotionOrientation()}</button
>
</div>
<ul class="kv">
{#if motionPermissionAvailable}
<li>
<span class="key">{m.sensors_motion()}</span>
<span>
{#if motionPermission === 'granted'}{m.sensors_status_granted()}
{:else if motionPermission === 'denied'}{m.sensors_status_denied()}
{:else}{m.sensors_status_unknown()}{/if}
</span>
</li>
{/if}
{#if orientationPermissionAvailable}
<li>
<span class="key">{m.sensors_orientation()}</span>
<span>
{#if orientationPermission === 'granted'}{m.sensors_status_granted()}
{:else if orientationPermission === 'denied'}{m.sensors_status_denied()}
{:else}{m.sensors_status_unknown()}{/if}
</span>
</li>
{/if}
</ul>
</section>
{/if}
<section>
<h3>{m.sensors_geolocation()}</h3>
{#if geoSupported}
<div class="row">
<button onclick={startGeolocation} disabled={geoWatchId !== null}>
<i class="ti ti-player-play"></i>
{m.sensors_start()}
</button>
<button onclick={stopGeolocation} disabled={geoWatchId === null}>
<i class="ti ti-player-stop"></i>
{m.sensors_stop()}
</button>
</div>
{#if geoError}
<div class="error">{geoError}</div>
{/if}
{#if geolocation}
<ul class="kv">
<li><span class="key">lat</span><span>{geolocation.coords.latitude}</span></li>
<li><span class="key">lon</span><span>{geolocation.coords.longitude}</span></li>
<li>
<span class="key">{m.sensors_accuracy()}</span><span
>{toFixed(geolocation.coords.accuracy)}</span
>
</li>
<li>
<span class="key">{m.sensors_altitude()}</span><span
>{geolocation.coords.altitude ?? '—'}</span
>
</li>
<li>
<span class="key">{m.sensors_heading()}</span><span
>{toFixed(geolocation.coords.heading ?? undefined)}</span
>
</li>
<li>
<span class="key">{m.sensors_speed()}</span><span
>{toFixed(geolocation.coords.speed ?? undefined)}</span
>
</li>
<li>
<span class="key">{m.sensors_timestamp()}</span><span
>{new Date(geolocation.timestamp).toLocaleString()}</span
>
</li>
</ul>
<div class="row">
<button onclick={() => copyJSON(geolocation)}
><i class="ti ti-copy"></i> {m.sensors_copy()}</button
>
</div>
{/if}
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_deviceMotion()}</h3>
{#if motionSupported}
<ul class="kv">
<li><span class="key">ax</span><span>{toFixed(deviceMotion?.ax)}</span></li>
<li><span class="key">ay</span><span>{toFixed(deviceMotion?.ay)}</span></li>
<li><span class="key">az</span><span>{toFixed(deviceMotion?.az)}</span></li>
<li><span class="key">α</span><span>{toFixed(deviceMotion?.gx)}</span></li>
<li><span class="key">β</span><span>{toFixed(deviceMotion?.gy)}</span></li>
<li><span class="key">γ</span><span>{toFixed(deviceMotion?.gz)}</span></li>
</ul>
<div class="row">
<button onclick={() => copyJSON(deviceMotion)}
><i class="ti ti-copy"></i> {m.sensors_copy()}</button
>
</div>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_deviceOrientation()}</h3>
{#if orientationSupported}
<ul class="kv">
<li><span class="key">alpha</span><span>{toFixed(deviceOrientation?.alpha)}</span></li>
<li><span class="key">beta</span><span>{toFixed(deviceOrientation?.beta)}</span></li>
<li><span class="key">gamma</span><span>{toFixed(deviceOrientation?.gamma)}</span></li>
</ul>
<div class="row">
<button onclick={() => copyJSON(deviceOrientation)}
><i class="ti ti-copy"></i> {m.sensors_copy()}</button
>
</div>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_accelerometer()}</h3>
{#if accelerometer.supported}
<div class="row">
{#if !accelerometer.active}
<button onclick={() => startSensor(accelerometer, 'Accelerometer')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(accelerometer)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if accelerometer.error}
<div class="error">{accelerometer.error}</div>
{/if}
<ul class="kv">
<li><span class="key">x</span><span>{toFixed(accelerometer.data.x as number)}</span></li>
<li><span class="key">y</span><span>{toFixed(accelerometer.data.y as number)}</span></li>
<li><span class="key">z</span><span>{toFixed(accelerometer.data.z as number)}</span></li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_gyroscope()}</h3>
{#if gyroscope.supported}
<div class="row">
{#if !gyroscope.active}
<button onclick={() => startSensor(gyroscope, 'Gyroscope')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(gyroscope)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if gyroscope.error}
<div class="error">{gyroscope.error}</div>
{/if}
<ul class="kv">
<li><span class="key">x</span><span>{toFixed(gyroscope.data.x as number)}</span></li>
<li><span class="key">y</span><span>{toFixed(gyroscope.data.y as number)}</span></li>
<li><span class="key">z</span><span>{toFixed(gyroscope.data.z as number)}</span></li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_magnetometer()}</h3>
{#if magnetometer.supported}
<div class="row">
{#if !magnetometer.active}
<button onclick={() => startSensor(magnetometer, 'Magnetometer', { frequency: 10 })}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(magnetometer)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if magnetometer.error}
<div class="error">{magnetometer.error}</div>
{/if}
<ul class="kv">
<li><span class="key">x</span><span>{toFixed(magnetometer.data.x as number)}</span></li>
<li><span class="key">y</span><span>{toFixed(magnetometer.data.y as number)}</span></li>
<li><span class="key">z</span><span>{toFixed(magnetometer.data.z as number)}</span></li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_ambientLight()}</h3>
{#if ambientLight.supported}
<div class="row">
{#if !ambientLight.active}
<button onclick={() => startSensor(ambientLight, 'AmbientLightSensor')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(ambientLight)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if ambientLight.error}
<div class="error">{ambientLight.error}</div>
{/if}
<ul class="kv">
<li>
<span class="key">{m.sensors_illuminance()}</span><span
>{toFixed(ambientLight.data.illuminance as number)}</span
>
</li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_barometer()}</h3>
{#if barometer.supported}
<div class="row">
{#if !barometer.active}
<button onclick={() => startSensor(barometer, 'Barometer')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(barometer)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if barometer.error}
<div class="error">{barometer.error}</div>
{/if}
<ul class="kv">
<li>
<span class="key">{m.sensors_pressure()}</span><span
>{toFixed(barometer.data.pressure as number)}</span
>
</li>
<li>
<span class="key">{m.sensors_temperature()}</span><span
>{barometer.data.temperature ?? '—'}</span
>
</li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
</div>
<style>
.sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
section {
border: 1px solid currentColor;
border-radius: 0.5rem;
padding: 0.75rem;
}
h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
}
.row {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.kv {
list-style: none;
padding: 0;
margin: 0.5rem 0 0 0;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 0.75rem;
align-items: baseline;
}
.kv .key {
opacity: 0.8;
margin-right: 0.25em;
}
.error {
color: #ff6b6b;
margin: 0.25rem 0;
}
.subdued {
opacity: 0.7;
}
button {
background: none;
color: inherit;
border: 1px solid currentColor;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
</style>