واجهة JavaScript bridge API
Web Bluetooth
شكل navigator.bluetooth، يُجاب عنه بشكل أصلي ليعمل أي موقع مكتوب لـ Chrome دون تغيير.
wbConnect الجسر (bridge)#
window.WebToApk.wbConnect(requestId: String, deviceId: String)
طبقة النقل الخام لـ WebBluetoothPolyfill. لا تستدعي الصفحة هذه الدوال مباشرة، بل تستدعي navigator.bluetooth تمامًا كما تفعل في المتصفح.
موصوف ضمن مجموعته، Web Bluetooth (navigator.bluetooth)، وليس على حدة.
مثال
ناقل داخلي وراء device.gatt.connect(). الصفحات تستخدم navigator.bluetooth؛ لا تستدعِ هذا مباشرة.
يُرجع: (عبر gatt.connect()) Promise بـ BluetoothRemoteGATTServer، بعد اكتشاف خدمات الجهاز. يتطلب: Bluetooth (يفعّله إنشاء ZIP أو HTML عندما تذكر شيفرتك navigator.bluetooth).
async function connectAndList(device) {
try {
var server = await device.gatt.connect();
var services = await server.getPrimaryServices();
services.forEach(function (s) { console.log('service', s.uuid); });
return server;
} catch (err) {
showStatus('Could not connect: ' + err.message); // NetworkError, NotFoundError
return null;
}
}أعد الاتصال عندما ينقطع الجهاز. استدعاء connect() مرة أخرى على الجهاز نفسه يعمل:
device.addEventListener('gattserverdisconnected', async function () {
showStatus('Reconnecting...');
try { await connectAndList(device); showStatus('Connected'); } catch (e) {}
});اتصل بمستشعرين في الوقت نفسه، مثل حزام نبض القلب وعدّاد القدرة:
async function connectBoth(hrDevice, powerDevice) {
var servers = await Promise.all([hrDevice.gatt.connect(), powerDevice.gatt.connect()]);
var hr = await servers[0].getPrimaryService('heart_rate');
var power = await servers[1].getPrimaryService('cycling_power');
return {
heartRate: await hr.getCharacteristic('heart_rate_measurement'),
power: await power.getCharacteristic('cycling_power_measurement')
};
}ملاحظات: التوقيع الخام wbConnect(requestId, deviceId). الاتصال بجهاز متصل أصلًا يُحَل مجددًا فورًا. getPrimaryService(s) يُرفض بـ NetworkError قبل أن ينتهي connect(). يمكن اتصال عدة أجهزة في الوقت نفسه.
wbDisconnect الجسر (bridge)#
window.WebToApk.wbDisconnect(deviceId: String)
طبقة النقل الخام لـ WebBluetoothPolyfill. لا تستدعي الصفحة هذه الدوال مباشرة، بل تستدعي navigator.bluetooth تمامًا كما تفعل في المتصفح.
موصوف ضمن مجموعته، Web Bluetooth (navigator.bluetooth)، وليس على حدة.
مثال
طبقة نقل داخلية خلف device.gatt.disconnect() (وdevice.forget()). تستخدم الصفحات navigator.bluetooth؛ لا تستدعِ هذه الدالة مباشرة.
القيمة المُرجَعة: لا شيء (disconnect() متزامنة، كما في معيار الويب). ثم يُطلق الجهاز gattserverdisconnected. المتطلبات: Bluetooth (يُفعَّل تلقائيًا في بناء ZIP أو HTML عندما يذكر الكود navigator.bluetooth).
var device = null; // from navigator.bluetooth.requestDevice(...)
document.getElementById('stop').addEventListener('click', function () {
if (device && device.gatt.connected) device.gatt.disconnect();
});
// Fires for your own disconnect() and when the device goes away by itself.
function watch(dev) {
dev.addEventListener('gattserverdisconnected', function () {
showStatus('Disconnected from ' + (dev.name || dev.id));
});
}ميّز بين ضغطك على «إيقاف» وبين خروج المستشعر من النطاق، وأعد الاتصال في الحالة الثانية فقط:
var userStopped = false;
function stopSession(device) {
userStopped = true;
if (device.gatt.connected) device.gatt.disconnect();
}
function watchDrops(device) {
device.addEventListener('gattserverdisconnected', function () {
if (userStopped) { userStopped = false; showStatus('Session ended'); return; }
showStatus('Sensor out of range, reconnecting...');
device.gatt.connect()
.then(resubscribe) // your code: get the characteristics, startNotifications()
.catch(function () { showStatus('Could not reconnect'); });
});
}زر "إزالة المستشعر": forget() يفصل الجهاز ويُسقطه، ولا يتبعه gattserverdisconnected:
var device = null; // from navigator.bluetooth.requestDevice(...)
document.getElementById('forget').addEventListener('click', async function () {
if (!device) return;
await device.forget();
device = null;
showStatus('Sensor removed. Tap Connect to pick one again.');
});ملاحظات: التوقيع الخام wbDisconnect(deviceId). بعد الانقطاع، كائنات الخدمة والخاصية القديمة تتوقف عن تلقي الإشعارات؛ وبعد connect() التالي احصل عليها مجددًا واستدعِ startNotifications() مجددًا. يُسقط التطبيق كل الاتصالات عند إغلاقه.
wbGetCharacteristics الجسر (bridge)#
window.WebToApk.wbGetCharacteristics(requestId: String, deviceId: String, service: String)
طبقة النقل الخام لـ WebBluetoothPolyfill. لا تستدعي الصفحة هذه الدوال مباشرة، بل تستدعي navigator.bluetooth تمامًا كما تفعل في المتصفح.
موصوف ضمن مجموعته، Web Bluetooth (navigator.bluetooth)، وليس على حدة.
مثال
ناقل داخلي وراء service.getCharacteristic(uuid) وservice.getCharacteristics(). الصفحات تستخدم navigator.bluetooth؛ لا تستدعِ هذا مباشرة.
يُرجع: (عبر الخدمة) Promise بـ BluetoothRemoteGATTCharacteristic واحد أو مصفوفة منها، لكل منها uuid وproperties (read وwrite وwriteWithoutResponse وnotify وindicate وbroadcast وauthenticatedSignedWrites). يتطلب: Bluetooth (يفعّله إنشاء ZIP أو HTML عندما تذكر شيفرتك navigator.bluetooth).
async function describe(server) {
var service = await server.getPrimaryService('heart_rate');
// One characteristic, by standard name, 16-bit number or full UUID:
var measurement = await service.getCharacteristic('heart_rate_measurement'); // same as 0x2a37
// All of them:
var list = await service.getCharacteristics();
list.forEach(function (c) {
var p = c.properties;
console.log(c.uuid, p.read ? 'read' : '', p.write ? 'write' : '', p.notify ? 'notify' : '');
});
return measurement;
}اعثر على خصائص خدمة مخصصة بحسب ما تستطيع فعله، بدلًا من كتابة معرّفات UUID الخاصة بها يدويًا في الشيفرة:
async function findSerialPipes(server) {
var svc = await server.getPrimaryService('0000ffe0-0000-1000-8000-00805f9b34fb');
var list = await svc.getCharacteristics();
var incoming = list.find(function (c) { return c.properties.notify || c.properties.indicate; });
var outgoing = list.find(function (c) { return c.properties.write || c.properties.writeWithoutResponse; });
if (!incoming || !outgoing) throw new Error('This device does not look like a serial adapter');
return { incoming: incoming, outgoing: outgoing };
}اقرأ خاصية اختيارية واعتبر NotFoundError بمعنى «هذا الجهاز لا يملكها»:
var LOCATIONS = ['Other', 'Chest', 'Wrist', 'Finger', 'Hand', 'Ear lobe', 'Foot'];
async function sensorLocation(server) {
var hr = await server.getPrimaryService('heart_rate');
try {
var chr = await hr.getCharacteristic('body_sensor_location'); // 0x2a38, optional in the spec
var v = await chr.readValue();
return LOCATIONS[v.getUint8(0)] || 'Unknown';
} catch (err) {
if (err.name === 'NotFoundError') return null; // this strap does not report it
throw err;
}
}ملاحظات: الصيغة الخام wbGetCharacteristics(requestId, deviceId, service). معرّف UUID لا تملكه الخدمة يُرفَض بالخطأ NotFoundError؛ واسم غير معروف يُطلق TypeError (مرّر UUID كاملًا للخصائص المخصصة). تُجلَب القائمة مرة واحدة لكل خدمة ثم يُعاد استخدامها.
wbIsAvailable الجسر (bridge)#
window.WebToApk.wbIsAvailable(): Boolean
getAvailability(): ما إذا كان الهاتف يملك Bluetooth (إجابة Chrome)، وليس ما إذا كان مشغّلًا. يطلب requestDevice() من المستخدم تشغيله؛ وكان الرد بـ false لمجرد أنه مطفأ يجعل الصفحات التي تفحص أولًا ("if (!await getAvailability())") تقول "Bluetooth غير مدعوم" ولا تصل أبدًا إلى تلك المطالبة (تقرير اللياقة 2026-10-03).
مثال
ناقل داخلي وراء navigator.bluetooth.getAvailability(). الصفحات تستخدم navigator.bluetooth؛ لا تستدعِ هذا مباشرة.
يُرجع: (عبر getAvailability()) Promise بـ true عندما يحتوي الإنشاء على Bluetooth وبلوتوث الهاتف مفعّل، وإلا false. يتطلب: Bluetooth (يفعّله إنشاء ZIP أو HTML عندما تذكر شيفرتك navigator.bluetooth).
async function updateBluetoothHint() {
var hint = document.getElementById('bt-hint');
if (!navigator.bluetooth) { hint.textContent = 'Bluetooth is not available in this app.'; return; }
var on = await navigator.bluetooth.getAvailability();
hint.textContent = on ? '' : 'Turn on Bluetooth to connect your sensor.';
}
document.addEventListener('visibilitychange', function () {
if (!document.hidden) updateBluetoothHint();
});فعّل زر الاتصال فقط أثناء تشغيل Bluetooth، وافحص مجددًا كلما عاد المستخدم إلى التطبيق:
async function refreshConnectButton() {
var btn = document.getElementById('connect');
if (!navigator.bluetooth) { btn.hidden = true; return; }
btn.disabled = !(await navigator.bluetooth.getAvailability());
}
window.addEventListener('load', refreshConnectButton);
document.addEventListener('visibilitychange', function () {
if (!document.hidden) refreshConnectButton();
});أعطِ رسالة واضحة "Bluetooth مطفأ" قبل فتح المنتقي، بدلًا من NotFoundError بعده:
document.getElementById('scan').addEventListener('click', async function () {
if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
if (!(await navigator.bluetooth.getAvailability())) {
showStatus('Bluetooth is off. Turn it on, then tap Scan again.');
return;
}
try {
var device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true });
showStatus('Picked ' + (device.name || device.id));
} catch (err) {
showStatus(err.name === 'NotFoundError' ? 'No device chosen.' : err.message);
}
});ملاحظات: التوقيع الخام wbIsAvailable(): Boolean (متزامن). يفحص المحوّل فقط ولا يطلب أي إذن. لا يُطلق حدث availabilitychanged في المعيار، فافحص مجددًا عندما تصبح الصفحة ظاهرة.
wbNotify الجسر (bridge)#
window.WebToApk.wbNotify(
طبقة النقل الخام لـ WebBluetoothPolyfill. لا تستدعي الصفحة هذه الدوال مباشرة، بل تستدعي navigator.bluetooth تمامًا كما تفعل في المتصفح.
موصوف ضمن مجموعته، Web Bluetooth (navigator.bluetooth)، وليس على حدة.
مثال
ناقل داخلي وراء characteristic.startNotifications() وstopNotifications(). الصفحات تستخدم navigator.bluetooth؛ لا تستدعِ هذا مباشرة.
يُرجع: (عبر الخاصية) Promise بالخاصية نفسها. كل قيمة جديدة تُطلق characteristicvaluechanged عليها، مع القيمة في event.target.value (من نوع DataView). يتطلب: Bluetooth (يفعّله إنشاء ZIP أو HTML عندما تذكر شيفرتك navigator.bluetooth).
var cadenceChr = null;
async function startCadence(server) {
var svc = await server.getPrimaryService('running_speed_and_cadence');
cadenceChr = await svc.getCharacteristic('rsc_measurement');
cadenceChr.addEventListener('characteristicvaluechanged', onRsc);
await cadenceChr.startNotifications();
}
function onRsc(e) {
var v = e.target.value;
var kmh = v.getUint16(1, true) / 256 * 3.6;
var spm = v.getUint8(3);
show('run', kmh.toFixed(1) + ' km/h, ' + spm + ' steps/min');
}
async function stopCadence() {
if (!cadenceChr) return;
cadenceChr.removeEventListener('characteristicvaluechanged', onRsc);
try { await cadenceChr.stopNotifications(); } catch (e) {}
}يمكن الاشتراك بالطريقة نفسها في مقياس حرارة لا يدعم قياسه سوى المؤشرات (indications)، وهنا عبر الخاصية oncharacteristicvaluechanged:
async function watchThermometer(server) {
var svc = await server.getPrimaryService('health_thermometer');
var chr = await svc.getCharacteristic('temperature_measurement');
chr.oncharacteristicvaluechanged = function (e) {
var v = e.target.value;
var mantissa = v.getUint32(1, true) & 0x00ffffff; // IEEE-11073 FLOAT
if (mantissa & 0x800000) mantissa -= 0x1000000;
var fahrenheit = v.getUint8(0) & 1;
show('temp', (mantissa * Math.pow(10, v.getInt8(4))).toFixed(1) + (fahrenheit ? ' °F' : ' °C'));
};
await chr.startNotifications();
}استقبل أسطرًا نصية من جهاز بنمط تسلسلي، مع ضم الإشعارات التي تقسم سطرًا في المنتصف:
async function listenForLines(server, onLine) {
var uart = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e');
var tx = await uart.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e');
if (!tx.properties.notify && !tx.properties.indicate) throw new Error('This device does not stream data');
var decoder = new TextDecoder(), buffer = '';
tx.addEventListener('characteristicvaluechanged', function (e) {
buffer += decoder.decode(e.target.value, { stream: true });
var lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(onLine);
});
await tx.startNotifications();
return tx;
}ملاحظات: التوقيع الخام wbNotify(requestId, deviceId, service, characteristic, enable). الخاصية التي تدعم الإشعارات الراجعة (indications) فقط يُشترك فيها بها تلقائيًا. والخاصية التي لا تستطيع الإشعار تُرفض بـ NotSupportedError. وoncharacteristicvaluechanged = fn يعمل أيضًا. بعد الانقطاع، اشترك مجددًا.
wbRead الجسر (bridge)#
window.WebToApk.wbRead(requestId: String, deviceId: String, service: String, characteristic: String)
طبقة النقل الخام لـ WebBluetoothPolyfill. لا تستدعي الصفحة هذه الدوال مباشرة، بل تستدعي navigator.bluetooth تمامًا كما تفعل في المتصفح.
موصوف ضمن مجموعته، Web Bluetooth (navigator.bluetooth)، وليس على حدة.
مثال
ناقل داخلي وراء characteristic.readValue(). الصفحات تستخدم navigator.bluetooth؛ لا تستدعِ هذا مباشرة.
يُرجع: (عبر readValue()) Promise بـ DataView؛ ويُحدَّث characteristic.value أيضًا. يتطلب: Bluetooth (يفعّله إنشاء ZIP أو HTML عندما تذكر شيفرتك navigator.bluetooth).
async function readInfo(server) {
try {
var battery = await server.getPrimaryService('battery_service');
var level = await (await battery.getCharacteristic('battery_level')).readValue();
show('battery', level.getUint8(0) + '%');
var info = await server.getPrimaryService('device_information');
var maker = await (await info.getCharacteristic('manufacturer_name_string')).readValue();
show('maker', new TextDecoder().decode(maker));
} catch (err) {
// NotFoundError: the device has no such service/characteristic
// NotSupportedError: read rejected by the device NetworkError: not connected / read failed
show('info', err.message);
}
}استطلع مستشعرًا يمكن قراءته فقط، وهنا خاصية Temperature القياسية (0x2a6e، بأجزاء من مئة من الدرجة):
var pollTimer = null;
async function startTemperaturePolling(server) {
var env = await server.getPrimaryService('environmental_sensing');
var temp = await env.getCharacteristic(0x2a6e);
pollTimer = setInterval(async function () {
try {
var v = await temp.readValue();
show('temp', (v.getInt16(0, true) / 100).toFixed(1) + ' °C');
} catch (err) {
clearInterval(pollTimer); // NetworkError once the sensor is gone
show('temp', err.message);
}
}, 5000);
}املأ لوحة «عن هذا الجهاز» ببدء عدة قراءات دفعة واحدة؛ تُنفَّذ الواحدة تلو الأخرى تلقائيًا:
async function readDeviceInfo(server) {
var info = await server.getPrimaryService('device_information');
var names = ['model_number_string', 'serial_number_string', 'firmware_revision_string'];
var decoder = new TextDecoder();
var results = await Promise.allSettled(names.map(async function (name) {
var chr = await info.getCharacteristic(name);
return decoder.decode(await chr.readValue());
}));
results.forEach(function (r, i) {
show(names[i], r.status === 'fulfilled' ? r.value : '-'); // missing ones reject with NotFoundError
});
}ملاحظات: الصيغة الخام wbRead(requestId, deviceId, service, characteristic). تُنفَّذ عمليات القراءة والكتابة وتغييرات الإشعارات على جهاز واحد الواحدة تلو الأخرى بالترتيب الذي تستدعيها به، فيمكنك إطلاق عدة استدعاءات دون انتظار بينها.
wbRequestDevice الجسر (bridge)#
window.WebToApk.wbRequestDevice(requestId: String, optionsJson: String)
طبقة النقل الخام لـ WebBluetoothPolyfill. لا تستدعي الصفحة هذه الدوال مباشرة، بل تستدعي navigator.bluetooth تمامًا كما تفعل في المتصفح.
موصوف ضمن مجموعته، Web Bluetooth (navigator.bluetooth)، وليس على حدة.
مثال
ناقل داخلي وراء navigator.bluetooth.requestDevice(options): يعرض منتقي الأجهزة في التطبيق. الصفحات تستخدم navigator.bluetooth.requestDevice؛ لا تستدعِ هذا مباشرة.
يُرجع: (عبر requestDevice) Promise بـ BluetoothDevice من الشكل {id, name, gatt}. يتطلب: Bluetooth (يفعّله إنشاء ZIP أو HTML عندما تذكر شيفرتك navigator.bluetooth) وإذن الأجهزة القريبة، ويُطلب عند أول استخدام.
document.getElementById('pick').addEventListener('click', async function () {
if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
try {
var device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['cycling_power'] }, { namePrefix: 'KICKR' }],
optionalServices: ['battery_service']
});
showStatus('Picked ' + (device.name || device.id));
} catch (err) {
if (err.name === 'NotFoundError') showStatus('No device chosen.'); // also: Bluetooth off
else showStatus(err.name + ': ' + err.message); // SecurityError = permission
}
});لعرض جميع الأجهزة القريبة بدلًا من التصفية: requestDevice({ acceptAllDevices: true }).
اختر جهازًا مخصصًا باسمه الدقيق أو ببادئة اسمه، مع إدراج خدمته الخاصة ذات 128 بت ليمكن استخدامها بعد الاتصال:
async function pickLamp() {
try {
return await navigator.bluetooth.requestDevice({
filters: [{ name: 'Desk Lamp' }, { namePrefix: 'LAMP-' }], // either one matches
optionalServices: ['0000ffe0-0000-1000-8000-00805f9b34fb']
});
} catch (err) {
if (err.name === 'SecurityError') showStatus('Allow Nearby devices to find your lamp.');
else if (err.name !== 'NotFoundError') showStatus(err.message); // NotFoundError = cancelled
return null;
}
}اطلب الجهاز مرة واحدة في كل جلسة وأعد استخدامه في النقرات اللاحقة (getDevices() غير متاحة، لذا احتفظ بالكائن الذي حصلت عليه):
var scale = null;
document.getElementById('weigh').addEventListener('click', async function () {
if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
try {
if (!scale) {
scale = await navigator.bluetooth.requestDevice({
filters: [{ services: ['weight_scale'], namePrefix: 'MI' }] // both keys must match
});
}
var server = await scale.gatt.connect();
var svc = await server.getPrimaryService('weight_scale');
var chr = await svc.getCharacteristic('weight_measurement');
chr.addEventListener('characteristicvaluechanged', onWeight);
await chr.startNotifications();
} catch (err) {
showStatus(err.name + ': ' + err.message);
}
});ملاحظات: التوقيع الخام wbRequestDevice(requestId, optionsJson)؛ وتعود الإجابة إلى البديل وليس إلى الصفحة. استدعِ requestDevice من نقرة. تستخدم المرشحات services (يجب أن تكون معلنة) وname (مطابقة تامة) وnamePrefix؛ ويطابق المرشح عندما تطابق كل مفاتيحه، ويطابق الجهاز عندما يطابق أي مرشح. إذا لم يوجد filters ولا acceptAllDevices ← TypeError. إلغاء المنتقي يُرفض بـ NotFoundError. يمسح المنتقي 30 ثانية. device.id هو عنوان Bluetooth للجهاز.
wbWrite الجسر (bridge)#
window.WebToApk.wbWrite(
طبقة النقل الخام لـ WebBluetoothPolyfill. لا تستدعي الصفحة هذه الدوال مباشرة، بل تستدعي navigator.bluetooth تمامًا كما تفعل في المتصفح.
موصوف ضمن مجموعته، Web Bluetooth (navigator.bluetooth)، وليس على حدة.
مثال
ناقل داخلي وراء characteristic.writeValue() وwriteValueWithResponse() وwriteValueWithoutResponse(). الصفحات تستخدم navigator.bluetooth؛ لا تستدعِ هذا مباشرة.
القيمة المُرجَعة: (عبر الخاصية) وعد Promise بـ undefined. المتطلبات: Bluetooth (يُفعَّل تلقائيًا في بناء ZIP أو HTML عندما يذكر الكود navigator.bluetooth).
مع استجابة (وwriteValue مثله): يكتمل عندما يؤكد الجهاز عملية الكتابة.
async function setTargetPower(server, watts) {
var ftms = await server.getPrimaryService('fitness_machine');
var cp = await ftms.getCharacteristic('00002ad9-0000-1000-8000-00805f9b34fb'); // FTMS control point
await cp.writeValueWithResponse(new Uint8Array([0x00])); // request control
var cmd = new DataView(new ArrayBuffer(3));
cmd.setUint8(0, 0x05); // set target power
cmd.setInt16(1, watts, true);
await cp.writeValueWithResponse(cmd);
}بدون استجابة: أسرع، للتدفقات التي لا يهم فيها ضياع حزمة.
async function sendText(characteristic, text) {
if (!characteristic.properties.writeWithoutResponse) throw new Error('This characteristic needs writeValue');
await characteristic.writeValueWithoutResponse(new TextEncoder().encode(text));
}أرسل أمرًا نصيًا طويلًا على قطع بحجم 20 بايت. لا يطلب التطبيق من الجهاز حجم حزمة أكبر، فـ 20 بايت هو ما يتسع في كتابة واحدة:
async function sendLine(rx, text) {
var bytes = new TextEncoder().encode(text + '\n');
for (var i = 0; i < bytes.length; i += 20) {
await rx.writeValueWithoutResponse(bytes.subarray(i, i + 20));
}
}
// sendLine(rxCharacteristic, 'SET COLOR 255 120 0 BRIGHTNESS 80');ملاحظات: الصيغة الخام wbWrite(requestId, deviceId, service, characteristic, valueBase64, withResponse). يمكن أن تكون القيمة ArrayBuffer أو أي مصفوفة مطبوعة أو DataView أو مصفوفة بايتات عادية؛ وأي شيء آخر يُطلق TypeError. الأخطاء: NotSupportedError (رُفضت الكتابة)، NetworkError (فشلت الكتابة أو لا يوجد اتصال)، NotFoundError.