Release Vulpes OS 2.7-preview.22
This commit is contained in:
parent
200d1fb235
commit
a138f97877
294 changed files with 515786 additions and 187 deletions
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
59
tundra/runtime/clock-state.py
Normal file
59
tundra/runtime/clock-state.py
Normal 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')
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in a new issue