Release Vulpes OS 2.7-preview.22

This commit is contained in:
OverSu 2026-10-22 20:42:26 +02:00
commit a138f97877
294 changed files with 515786 additions and 187 deletions

View file

@ -39,6 +39,9 @@ class Vibration:
raise ValueError('INVALID_VIBRATION')
self.steps = list(enumerate(pattern))
self.deadline = None
trigger = VIBRATOR/'trigger'
if trigger.exists():
trigger.write_text('none')
(VIBRATOR/'brightness').write_text('0')
self.advance(time.monotonic())
@ -60,11 +63,27 @@ class Vibration:
vibration = Vibration()
class WakeRecovery:
def __init__(self):
self.deadline = None
def arm(self, now):
self.deadline = now + 1.5
def advance(self, now):
if self.deadline is not None and now >= self.deadline:
self.deadline = None
if int((BACKLIGHT/'brightness').read_text()) == 0:
control({'operation':'screen', 'enabled':True, 'level':0.5})
wake_recovery = WakeRecovery()
def control(p):
if p.get('operation') == 'screen':
enabled, level = p.get('enabled'), p.get('level')
if type(enabled) is not bool or type(level) not in (int, float) or not 0 <= level <= 1:
raise ValueError('INVALID_SCREEN')
wake_recovery.deadline = None
maximum = int((BACKLIGHT/'max_brightness').read_text())
(BACKLIGHT/'brightness').write_text(str(max(1, round(maximum * level)) if enabled else 0))
elif p.get('operation') == 'vibrate':
@ -125,15 +144,16 @@ def serve():
_, _, kind, code, value = INPUT.unpack_from(data, offset)
event = key_event(kind, code, value)
if event:
# Always allow a physical press to recover a black display,
# even if Gaia has stopped responding.
# Let Gaia restore the lock screen before lighting it.
# Recover independently only if its screen command never arrives.
if code == 116 and value == 1 and int((BACKLIGHT/'brightness').read_text()) == 0:
control({'operation':'screen', 'enabled':True, 'level':0.5})
wake_recovery.arm(time.monotonic())
state['sequence'] += 1
state['events'].append({'id':state['sequence'], 'type':event})
state['events'] = state['events'][-64:]
if vibration.deadline is not None:
vibration.advance(time.monotonic())
wake_recovery.advance(time.monotonic())
publish()
def client():

View file

@ -127,6 +127,7 @@ class Camera:
def metadata(self):
return {'ready': True, 'width': self.width, 'height': self.height,
'orientation': self.orientation, 'format': 'nv21',
'flashModes': [m for m in self.parameters.get('flash-mode-values', 'off').split(',') if m in ('off','auto','on','torch')],
'pictureSizes': [{'width':w,'height':h} for w,h in self.picture_sizes],
'focusModes': self.parameters.get('focus-mode-values', 'fixed').split(','),
'maxFocusAreas': min(1,int(self.parameters.get('max-num-focus-areas', 0))),
@ -135,6 +136,16 @@ class Camera:
def control(self, command):
P, I = C.c_void_p, C.c_int
action = command.get('action')
if action == 'flash':
mode = command.get('mode')
modes = ('off', 'auto', 'on', 'torch')
if mode not in modes or mode not in self.metadata()['flashModes']:
raise ValueError('CAMERA_FLASH_UNAVAILABLE')
function(self.lib, 'android_camera_set_flash_mode', [P,I])(self.camera,modes.index(mode))
actual = self.read_parameters().get('flash-mode', 'off')
if actual != mode:
raise RuntimeError('CAMERA_FLASH_READBACK_FAILED')
return {'mode':actual}
if action == 'zoom':
ratio = command.get('zoom')
if not isinstance(ratio, (int,float)) or not self.zoom_ratios[0] <= ratio <= self.zoom_ratios[-1]:

View file

@ -0,0 +1,59 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Preserve wall time across boots when Qualcomm's RTC has no Unix epoch."""
import json
from pathlib import Path
import time
STATE = Path('/var/lib/tundra/clock/last-time.json')
RTC = Path('/sys/class/rtc/rtc0/since_epoch')
def rtc_seconds():
try:
return int(RTC.read_text().strip())
except (OSError, ValueError):
return None
def valid_epoch(value):
return type(value) in (int, float) and 946684800 <= value <= 4102444800
def restore():
try:
saved = json.loads(STATE.read_text())
epoch, previous = saved['epoch'], saved.get('rtc')
if not valid_epoch(epoch):
return False
current = rtc_seconds()
# A reset/replaced RTC is not evidence of time elapsed while powered off.
if type(previous) is int and current is not None and 0 <= current - previous <= 366*86400:
epoch += current - previous
if valid_epoch(epoch) and time.time() < epoch:
time.clock_settime(time.CLOCK_REALTIME, epoch)
return True
except (OSError, ValueError, KeyError, TypeError):
pass
return False
def save():
epoch = time.time()
if not valid_epoch(epoch):
return
STATE.parent.mkdir(parents=True, exist_ok=True)
temporary = STATE.with_suffix('.tmp')
temporary.write_text(json.dumps({'epoch': epoch, 'rtc': rtc_seconds()}) + '\n')
temporary.chmod(0o600)
temporary.replace(STATE)
if __name__ == '__main__':
import sys
if sys.argv[1:] == ['--restore']:
restore()
elif sys.argv[1:] == ['--save']:
save()
else:
raise SystemExit('Use --restore or --save')

View file

@ -8,6 +8,11 @@ from pathlib import Path
CLOCK_CONFIG = Path("/var/lib/tundra/clock/settings.json")
def checkpoint_clock():
import runpy
runpy.run_path(str(Path(__file__).with_name('clock-state.py')))['save']()
def clock_automatic():
try:
return json.loads(CLOCK_CONFIG.read_text())["automatic"] is True
@ -43,6 +48,7 @@ def sync_network_clock():
if abs(time.time() - seconds) > 2:
# oFono timestamps use CLOCK_MONOTONIC, not wall time or boot time.
time.clock_settime(time.CLOCK_REALTIME, seconds)
checkpoint_clock()
@ -159,6 +165,8 @@ def device_request(value):
result = subprocess.run(['/usr/bin/timedatectl', *args], capture_output=True, text=True, timeout=10)
if result.returncode:
raise RuntimeError('CLOCK_REQUEST_FAILED')
if action == 'setTime':
checkpoint_clock()
result = subprocess.run(['/usr/bin/timedatectl', 'show', '-p', 'Timezone', '-p', 'NTP', '-p', 'CanNTP'],
capture_output=True, text=True, timeout=5)
if result.returncode:

View file

@ -37,6 +37,38 @@ def prepare(root, network=None, hardware=None, diagnostics=False):
ExecStartPre=/usr/bin/fc-cache -f /usr/share/fonts/opentype/vulpes
''')
write('etc/os-release','NAME="Tundra"\nID=tundra\nPRETTY_NAME="Tundra Sargo development trial"\n')
(root/'etc/tundra').mkdir(parents=True, exist_ok=True)
shutil.copy2(Path(__file__).with_name('clock-state.py'), root/'etc/tundra/clock-state.py')
write('etc/systemd/system/tundra-clock-restore.service', """[Unit]
Description=Restore Tundra clock from persistent state and RTC elapsed time
DefaultDependencies=no
After=local-fs.target
Before=time-set.target tundra-display.service tundra-radio.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /etc/tundra/clock-state.py --restore
RemainAfterExit=yes
ExecStop=/usr/bin/python3 /etc/tundra/clock-state.py --save
StateDirectory=tundra/clock
""")
write('etc/systemd/system/tundra-clock-save.service', """[Unit]
Description=Checkpoint Tundra clock
After=tundra-clock-restore.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /etc/tundra/clock-state.py --save
StateDirectory=tundra/clock
""")
write('etc/systemd/system/tundra-clock-save.timer', """[Unit]
Description=Checkpoint Tundra clock once per minute
[Timer]
OnBootSec=60
OnUnitActiveSec=60
""")
wants = root/'etc/systemd/system/tundra.target.wants'
wants.mkdir(parents=True, exist_ok=True)
for unit in ('tundra-clock-restore.service', 'tundra-clock-save.timer'):
(wants/unit).symlink_to('../'+unit)
write('etc/hostname','vulpes-sargo\n')
write('etc/shells','/bin/sh\n/bin/bash\n/usr/bin/bash\n')
write('etc/pam.d/tundra','auth required pam_permit.so\naccount required pam_permit.so\nsession required pam_systemd.so\n')
@ -177,6 +209,7 @@ NoNewPrivileges=yes
write('etc/NetworkManager/conf.d/80-vulpes-hotspot.conf',Path(__file__).with_name('hotspot-firewall.conf').read_text())
for name in ('iptables','ip6tables'):
alias=root/'usr/sbin'/name
alias.parent.mkdir(parents=True, exist_ok=True)
if not alias.exists() and not alias.is_symlink():alias.symlink_to(name+'-legacy')
write('etc/tundra/network-enabled', 'sargo\n')
display = root/'etc/systemd/system/tundra-display.service'