VulpesOS/host/Tundra.sys.mjs
OverSu 1311bed5e9 Release Preview 20 shared desktop, Android and Sargo updates
Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
2026-10-13 00:19:45 +02:00

209 lines
8.7 KiB
JavaScript

// Native state is published by Tundra, never supplied by an application.
export async function tundraSnapshot() {
try {
const value = await IOUtils.readJSON('/run/tundra-hardware/status.json');
const age = Date.now() / 1000 - value.recordedAt;
if (value.schema !== 1 || !Number.isFinite(age) || age < -5 || age > 20)
throw Error('STALE_HARDWARE_STATE');
const battery = value.battery;
return {
battery:
battery &&
Number.isFinite(battery.level) &&
battery.level >= 0 &&
battery.level <= 1 &&
typeof battery.charging === 'boolean'
? battery
: null,
wifi: value.wifi,
recordedAt: value.recordedAt,
available: true,
};
} catch (_) {
return { battery: null, wifi: { available: false, state: 'unavailable' }, available: false };
}
}
export function tundraCapabilities(release) {
const radio = Services.env?.get('VULPES_TUNDRA_RADIO') === '1';
return {
platform: 'tundra',
platformName: 'Tundra',
model: 'Google Pixel 3a',
tundra: '0.1.0-dev',
adapter: { version: '0.1.0-dev', device: 'sargo' },
version: release.version,
gaia: release.gaia,
gecko: Services.appinfo.platformVersion,
camera: true,
media: true,
contacts: true,
notifications: true,
dial: radio,
callControl: radio,
sms: radio,
mms: radio,
alarms: false,
hardwareStatus: true,
wifiControl: Services.env?.get('VULPES_TUNDRA_WIFI') === '1',
};
}
export async function tundraRadio(operation, params = {}) {
if (Services.env.get('VULPES_TUNDRA_RADIO') !== '1') throw Error('RADIO_UNAVAILABLE');
if (!['deviceSettings', 'mobileSettings', 'status', 'calls', 'dial', 'hangup', 'answer', 'sms', 'incomingMessages', 'ackMessages', 'messageState'].includes(operation)) throw Error('RADIO_INVALID_REQUEST');
const { Subprocess } = ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
const process = await Subprocess.call({
command: '/usr/bin/python3',
arguments: ['/etc/tundra/radio-control.py', '--client-envelope'],
stderr: 'ignore',
});
await process.stdin.write(JSON.stringify({ version: 1, operation, ...params }));
await process.stdin.close();
let output = '';
for (;;) {
const chunk = await process.stdout.readString();
if (!chunk) break;
output += chunk;
if (output.length > 65536) {
process.kill();
throw Error('RADIO_UNAVAILABLE');
}
}
const { exitCode } = await process.wait();
if (exitCode) throw Error('RADIO_UNAVAILABLE');
let reply;
try {
reply = JSON.parse(output);
} catch (_) {
throw Error('RADIO_INVALID_REPLY');
}
if (reply?.ok === false) {
const errors = ['RADIO_NOT_REGISTERED', 'RADIO_BUSY', 'RADIO_NOT_READY', 'RADIO_TIMEOUT',
'RADIO_REQUEST_FAILED', 'RADIO_READBACK_FAILED', 'INVALID_REQUEST', 'APN_IN_USE', 'APN_NOT_FOUND', 'APN_WRITE_FAILED', 'APN_ROLLBACK_FAILED', 'CLOCK_REQUEST_FAILED', 'CLOCK_UNAVAILABLE', 'INVALID_NUMBER', 'INVALID_SMS', 'INVALID_CALL'];
throw Error(errors.includes(reply.error) ? reply.error : 'RADIO_REQUEST_FAILED');
}
if (reply?.ok !== true) throw Error('RADIO_INVALID_REPLY');
return reply.result;
}
let wifiScanPending = null;
let wifiScanCache = null;
const WIFI_SCAN_CACHE_MS = 5000;
export async function tundraWifi(params) {
if (Services.env.get('VULPES_TUNDRA_WIFI') !== '1') throw Error('NOT_SUPPORTED');
if (!['scan', 'connect', 'connectHidden', 'disconnect', 'status', 'setEnabled', 'known', 'forget','hotspotStatus','hotspotSave','hotspotPower'].includes(params.operation))
throw Error('WIFI_INVALID_REQUEST');
if (params.operation !== 'scan') {
try { return await wifiCommand(params); }
finally { if (['connect','connectHidden','disconnect','forget','setEnabled','hotspotPower'].includes(params.operation)) wifiScanCache = null; }
}
// System and Settings share a broker: at most one Python scan client exists.
if (wifiScanPending) return wifiScanPending;
if (wifiScanCache && Date.now() < wifiScanCache.expires) {
if (wifiScanCache.error) throw wifiScanCache.error;
return wifiScanCache.result;
}
wifiScanPending = wifiCommand(params).then(result => {
wifiScanCache = {result, expires:Date.now() + WIFI_SCAN_CACHE_MS};
return result;
}, error => {
// A missing interface must not turn retries into another process storm.
wifiScanCache = {error, expires:Date.now() + WIFI_SCAN_CACHE_MS};
throw error;
}).finally(() => { wifiScanPending = null; });
return wifiScanPending;
}
async function wifiCommand(params) {
const { Subprocess } = ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
const process = await Subprocess.call({
command: '/usr/bin/python3',
arguments: ['/etc/tundra/wifi-control.py', '--client'],
stderr: 'ignore',
});
// Passwords are never command-line arguments, preferences or log messages.
await process.stdin.write(JSON.stringify(params));
await process.stdin.close();
let output = '';
for (;;) {
const chunk = await process.stdout.readString();
if (!chunk) break;
output += chunk;
if (output.length > 65536) {
process.kill();
throw Error('WIFI_UNAVAILABLE');
}
}
const { exitCode } = await process.wait();
if (exitCode) throw Error('WIFI_UNAVAILABLE');
const reply = JSON.parse(output);
if (reply.error) throw Error(reply.error);
return reply.result;
}
export async function tundraHardware(params) {
if (Services.env.get('VULPES_TUNDRA') !== '1') return;
const { Subprocess } = ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
const process = await Subprocess.call({command:'/usr/bin/python3',
arguments:['/etc/tundra/buttons.py', '--client'], stderr:'ignore'});
await process.stdin.write(JSON.stringify(params));
await process.stdin.close();
const output = await process.stdout.readString();
const {exitCode} = await process.wait();
if (exitCode || !JSON.parse(output)?.ok) throw Error('HARDWARE_REQUEST_FAILED');
}
export async function tundraCallAudio(params) {
const {Subprocess} = ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
const process = await Subprocess.call({command:'/usr/bin/python3',
arguments:['/etc/tundra/call-audio.py','--client'],stderr:'ignore'});
await process.stdin.write(JSON.stringify(params));
await process.stdin.close();
let text = '';
for (;;) {
const chunk = await process.stdout.readString();
if (!chunk) break;
text += chunk;
if (text.length > 4096) {
process.kill();
throw Error('CALL_AUDIO_INVALID_REPLY');
}
}
const {exitCode} = await process.wait();
if (exitCode) throw Error('CALL_AUDIO_UNAVAILABLE');
const reply = JSON.parse(text);
if (reply.error) throw Error(reply.error);
return reply.result;
}
// Separate socket and bounded payload so MMS traffic cannot block call control.
export async function tundraMms(operation, params={}) {
if(Services.env.get('VULPES_TUNDRA_RADIO')!=='1') throw Error('NotSupportedError');
if(!['send','state','incoming','ack'].includes(operation)) throw Error('INVALID_REQUEST');
const {Subprocess}=ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
const process=await Subprocess.call({command:'/usr/bin/python3',arguments:['/etc/tundra/mms-control.py','--client'],stderr:'ignore'});
await process.stdin.write(JSON.stringify({version:1,operation,...params}));await process.stdin.close();
let output='';for(;;){const chunk=await process.stdout.readString();if(!chunk)break;output+=chunk;if(output.length>1228800){process.kill();throw Error('MMS_TOO_LARGE');}}
const {exitCode}=await process.wait();if(exitCode)throw Error('MMS_UNAVAILABLE');
const reply=JSON.parse(output);if(reply.ok!==true)throw Error(reply.error || 'MMS_UNAVAILABLE');return reply.result;
}
// A persistent daemon owns BlueZ discovery and pending pairing requests.
export async function tundraBluetooth(params) {
if (Services.env.get('VULPES_TUNDRA') !== '1') throw Error('NotSupportedError');
const {Subprocess}=ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
const process=await Subprocess.call({command:'/usr/bin/python3',
arguments:['/etc/tundra/bluetooth-control.py','--client'],stderr:'ignore'});
await process.stdin.write(JSON.stringify({...params,version:1}));await process.stdin.close();
let output='';
for (;;) {
const chunk=await process.stdout.readString();if(!chunk)break;
output+=chunk;if(output.length>65536){process.kill();throw Error('BLUETOOTH_INVALID_REPLY');}
}
const {exitCode}=await process.wait();if(exitCode)throw Error('BLUETOOTH_UNAVAILABLE');
const reply=JSON.parse(output);if(reply.error)throw Error(reply.error);
return reply.result;
}