plane image fallback + seismic calibrations
This commit is contained in:
@@ -49,21 +49,28 @@ const description = computed(() => props.plane.desc || [props.plane.manufacturer
|
|||||||
const operator = computed(() => props.plane.operator || props.plane.owner || '')
|
const operator = computed(() => props.plane.operator || props.plane.owner || '')
|
||||||
|
|
||||||
// ── Aircraft photo ────────────────────────────────────────────────────────────
|
// ── Aircraft photo ────────────────────────────────────────────────────────────
|
||||||
const photoUrl = ref<string | null>(null)
|
const photoUrl = ref<string | null>(null)
|
||||||
|
const isSil = ref(false)
|
||||||
const photoLoading = ref(false)
|
const photoLoading = ref(false)
|
||||||
|
|
||||||
async function fetchPhoto(icao: string) {
|
async function fetchPhoto(icao: string, type: string) {
|
||||||
photoUrl.value = null
|
photoUrl.value = null
|
||||||
|
isSil.value = false
|
||||||
if (!icao) return
|
if (!icao) return
|
||||||
photoLoading.value = true
|
photoLoading.value = true
|
||||||
const res = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao.toLowerCase()}`)
|
const res = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao.toLowerCase()}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const photo = data?.photos?.[0]
|
const photo = data?.photos?.[0]
|
||||||
if (photo) photoUrl.value = photo.thumbnail_large?.src ?? photo.thumbnail?.src ?? null
|
if (photo) {
|
||||||
|
photoUrl.value = photo.thumbnail_large?.src ?? photo.thumbnail?.src ?? null
|
||||||
|
} else {
|
||||||
|
photoUrl.value = `https://globe.adsbexchange.com/aircraft_sil/${type}.png`
|
||||||
|
isSil.value = true
|
||||||
|
}
|
||||||
photoLoading.value = false
|
photoLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.plane.icao || props.plane.hex, (icao) => fetchPhoto(icao), { immediate: true })
|
watch(() => props.plane, (plane) => fetchPhoto(plane.icao || plane.hex, plane.aircraft), { immediate: true })
|
||||||
|
|
||||||
// ── Drag ──────────────────────────────────────────────────────────────────────
|
// ── Drag ──────────────────────────────────────────────────────────────────────
|
||||||
const pos = ref({ ...props.position })
|
const pos = ref({ ...props.position })
|
||||||
@@ -234,7 +241,7 @@ const navball = computed(() => {
|
|||||||
|
|
||||||
<!-- Photo -->
|
<!-- Photo -->
|
||||||
<div v-if="photoUrl" class="ap-photo-wrap">
|
<div v-if="photoUrl" class="ap-photo-wrap">
|
||||||
<img class="ap-photo" :src="photoUrl" :alt="callsign" />
|
<img class="ap-photo" :class="{ 'ap-photo--sil': isSil }" :src="photoUrl" :alt="callsign" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
@@ -335,8 +342,8 @@ const navball = computed(() => {
|
|||||||
|
|
||||||
/* ── Photo ── */
|
/* ── Photo ── */
|
||||||
.ap-photo-wrap {
|
.ap-photo-wrap {
|
||||||
width: 100%;
|
max-width: 360px;
|
||||||
height: 160px;
|
max-height: 160px;
|
||||||
background: #111;
|
background: #111;
|
||||||
border-bottom: 1px solid rgba(200,200,220,0.15);
|
border-bottom: 1px solid rgba(200,200,220,0.15);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -344,13 +351,8 @@ const navball = computed(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.ap-photo {
|
.ap-photo { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
width: 100%;
|
.ap-photo--sil { object-fit: contain; box-sizing: border-box; image-rendering: auto; }
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ap-body {
|
.ap-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|||||||
@@ -151,42 +151,89 @@ def read_bme(c, sensor, alt_m=None):
|
|||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
||||||
# ── MPU6050 ───────────────────────────────────────────────────────────────────
|
# ── MPU6050 ───────────────────────────────────────────────────────────────────
|
||||||
|
# ── MPU6050 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
MPU_ADDR = 0x68
|
MPU_ADDR = 0x68
|
||||||
_mpu_peak = None
|
_mpu_peak = None
|
||||||
_mpu_lock = threading.Lock()
|
_mpu_lock = threading.Lock()
|
||||||
_mpu_offsets = None
|
_mpu_grav = None # slow-tracked gravity vector [x, y, z]
|
||||||
|
GRAV_ALPHA = 0.05 # EMA rate while still
|
||||||
|
STILL_SPREAD = 0.03 # g, max peak-to-peak per axis to count as "still"
|
||||||
|
_still_buf = deque(maxlen=50) # ~0.5s @ 100Hz
|
||||||
|
|
||||||
|
def read_accel():
|
||||||
|
d = try_read(lambda: bus.read_i2c_block_data(MPU_ADDR, 0x3B, 6))
|
||||||
|
if not d: return None
|
||||||
|
return [
|
||||||
|
s16((d[0] << 8) | d[1]) / 16384.0,
|
||||||
|
s16((d[2] << 8) | d[3]) / 16384.0,
|
||||||
|
s16((d[4] << 8) | d[5]) / 16384.0,
|
||||||
|
]
|
||||||
|
|
||||||
|
def rotation_to_z(g):
|
||||||
|
"""Rodrigues rotation mapping gravity vector g -> +Z. Returns (R, |g|)."""
|
||||||
|
mag = math.sqrt(sum(v * v for v in g))
|
||||||
|
u = [v / mag for v in g]
|
||||||
|
kx, ky = u[1], -u[0] # axis = u × z
|
||||||
|
s = math.sqrt(kx * kx + ky * ky) # sin(theta)
|
||||||
|
c = u[2] # cos(theta)
|
||||||
|
if s < 1e-8:
|
||||||
|
return [[1, 0, 0], [0, c, 0], [0, 0, c]], mag
|
||||||
|
kx, ky = kx / s, ky / s
|
||||||
|
v = 1 - c
|
||||||
|
return [
|
||||||
|
[c + kx * kx * v, kx * ky * v, ky * s],
|
||||||
|
[ky * kx * v, c + ky * ky * v, -kx * s],
|
||||||
|
[-ky * s, kx * s, c ],
|
||||||
|
], mag
|
||||||
|
|
||||||
|
def is_still(buf):
|
||||||
|
if len(buf) < buf.maxlen: return False
|
||||||
|
return all(
|
||||||
|
max(s[i] for s in buf) - min(s[i] for s in buf) < STILL_SPREAD
|
||||||
|
for i in range(3)
|
||||||
|
)
|
||||||
|
|
||||||
def calibrate_mpu(samples=200):
|
def calibrate_mpu(samples=200):
|
||||||
if not bus: return None
|
if not bus: return None
|
||||||
print(" Calibrating MPU6050, keep still...")
|
print(" Calibrating MPU6050, keep still...")
|
||||||
sums = [0.0] * 6
|
sums, n = [0.0, 0.0, 0.0], 0
|
||||||
n = 0
|
|
||||||
for _ in range(samples):
|
for _ in range(samples):
|
||||||
d = try_read(lambda: bus.read_i2c_block_data(MPU_ADDR, 0x3B, 14))
|
a = read_accel()
|
||||||
if d:
|
if a:
|
||||||
sums[0] += s16((d[0] << 8) | d[1]) / 16384.0
|
sums = [s + v for s, v in zip(sums, a)]
|
||||||
sums[1] += s16((d[2] << 8) | d[3]) / 16384.0
|
|
||||||
sums[2] += s16((d[4] << 8) | d[5]) / 16384.0
|
|
||||||
sums[3] += s16((d[8] << 8) | d[9]) / 131.0
|
|
||||||
sums[4] += s16((d[10] << 8) | d[11]) / 131.0
|
|
||||||
sums[5] += s16((d[12] << 8) | d[13]) / 131.0
|
|
||||||
n += 1
|
n += 1
|
||||||
time.sleep(0.005)
|
time.sleep(0.005)
|
||||||
if n == 0: return None
|
if n == 0: return None
|
||||||
return [sums[0]/n, sums[1]/n, sums[2]/n - 1.0, sums[3]/n, sums[4]/n, sums[5]/n]
|
|
||||||
|
g = [s / n for s in sums]
|
||||||
|
mag = math.sqrt(sum(v * v for v in g))
|
||||||
|
if not 0.5 < mag < 1.5:
|
||||||
|
print(f" ❌ MPU6050: bad gravity magnitude {mag:.2f}g")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f" ✅ MPU6050: gravity {mag:.3f}g on ({g[0]:.2f}, {g[1]:.2f}, {g[2]:.2f}) → mapped to Z")
|
||||||
|
return g
|
||||||
|
|
||||||
def mpu_loop():
|
def mpu_loop():
|
||||||
global _mpu_peak
|
global _mpu_peak, _mpu_grav
|
||||||
while True:
|
while True:
|
||||||
if bus and _mpu_offsets:
|
if bus and _mpu_grav:
|
||||||
d = try_read(lambda: bus.read_i2c_block_data(MPU_ADDR, 0x3B, 14))
|
a = read_accel()
|
||||||
if d:
|
if a:
|
||||||
o = _mpu_offsets
|
_still_buf.append(a)
|
||||||
ax = s16((d[0] << 8) | d[1]) / 16384.0 - o[0]
|
|
||||||
ay = s16((d[2] << 8) | d[3]) / 16384.0 - o[1]
|
R, g = rotation_to_z(_mpu_grav)
|
||||||
az = s16((d[4] << 8) | d[5]) / 16384.0 - o[2] - 1.0 # remove gravity
|
ax = R[0][0] * a[0] + R[0][1] * a[1] + R[0][2] * a[2]
|
||||||
|
ay = R[1][0] * a[0] + R[1][1] * a[1] + R[1][2] * a[2]
|
||||||
|
az = R[2][0] * a[0] + R[2][1] * a[1] + R[2][2] * a[2] - g
|
||||||
mag = math.sqrt(ax**2 + ay**2 + az**2)
|
mag = math.sqrt(ax**2 + ay**2 + az**2)
|
||||||
|
|
||||||
|
# Re-track gravity whenever the sensor is *still* —
|
||||||
|
# absorbs bias shifts of any size, blocks shakes
|
||||||
|
if is_still(_still_buf):
|
||||||
|
_mpu_grav = [p + (v - p) * GRAV_ALPHA for p, v in zip(_mpu_grav, a)]
|
||||||
|
|
||||||
with _mpu_lock:
|
with _mpu_lock:
|
||||||
if _mpu_peak is None or mag > _mpu_peak['seismic_magnitude']:
|
if _mpu_peak is None or mag > _mpu_peak['seismic_magnitude']:
|
||||||
_mpu_peak = {
|
_mpu_peak = {
|
||||||
@@ -484,9 +531,9 @@ if __name__ == '__main__':
|
|||||||
if bus:
|
if bus:
|
||||||
try_read(lambda: bus.write_byte_data(MPU_ADDR, 0x6B, 0))
|
try_read(lambda: bus.write_byte_data(MPU_ADDR, 0x6B, 0))
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
_mpu_offsets = try_init(calibrate_mpu, 'MPU6050')
|
_mpu_grav = try_init(calibrate_mpu, 'MPU6050')
|
||||||
else:
|
else:
|
||||||
_mpu_offsets = None
|
_mpu_grav = None
|
||||||
print(" ❌ MPU6050: no I2C bus")
|
print(" ❌ MPU6050: no I2C bus")
|
||||||
|
|
||||||
threading.Thread(target=mpu_loop, daemon=True).start()
|
threading.Thread(target=mpu_loop, daemon=True).start()
|
||||||
|
|||||||
Reference in New Issue
Block a user