Thread ağını Google Thread Credentials API'leriyle paylaşma

1. Başlamadan önce

Thread Border Router (TBR) codelab'imizde, Raspberry Pi tabanlı bir Thread Border Router'ın nasıl oluşturulacağını gösteriyoruz. Bu codelab'de

  • Thread ile kablosuz/Ethernet ağları arasında iki yönlü IP bağlantısı oluşturun.
  • mDNS (Kablosuz/Ethernet bağlantısında) ve SRP (Thread ağında) aracılığıyla iki yönlü hizmet keşfi sağlayın.

Bu codelab, önceki codelab'in üzerine inşa edilmiştir ve kendi sınır yönlendiricinizin ve uygulamanızın tek bir Thread ağı oluşturmak için Google API'leriyle nasıl etkileşime geçebileceğini ele alır. Thread kimlik bilgilerini birleştirme, ağın sağlamlığını artırdığı ve kullanıcıların Thread'e dayalı uygulamalarla etkileşimlerini basitleştirdiği için önemlidir.

Ön koşullar

  • OTBR Codelab'i tamamlayın.
  • Linux, Android/Kotlin ve Thread ağları hakkında temel düzeyde bilgi

Neler öğreneceksiniz?

  • Kimlik bilgisi kümelerini almak ve ayarlamak için Thread Sharing API'lerini kullanma
  • Google'ın ağı ile aynı kimlik bilgilerini kullanarak kendi OpenThread Sınır Yönlendiricinizi ayarlama

İhtiyacınız olanlar

  • Raspberry Pi 4 kartı veya Open Thread Border Router'ı (OTBR) çalıştıran başka bir Linux tabanlı kart
  • Radyo Yardımcı İşlemcisi (RCP) olarak IEEE 802.15.4 bağlantısı sağlayan kart. Farklı SoC tedarikçi firmalarının depolarının listesini ve talimatlarını OpenThread GitHub sayfasında bulabilirsiniz.

2. HTTP hizmetini ayarlama

İhtiyacımız olan ilk yapı taşı, Etkin Kimlik Bilgileri'ni okumamıza ve Beklemede Kimlik Bilgileri'ni OTBR'nize yazmamıza olanak tanıyan bir arayüzdür. TBR oluştururken burada iki örnekle gösterildiği gibi kendi tescilli mekanizmalarınızı kullanın. İlk seçenek, DBUS üzerinden OTBR aracısıyla yerel olarak nasıl iletişim kurulacağını gösterirken ikinci seçenek, OTBR'de oluşturulabilecek Rest API'den yararlanır.

Bu yöntemlerin hiçbiri güvenli değildir ve üretim ortamında olduğu gibi kullanılmamalıdır. Ancak bir tedarikçi, üretim ortamında kullanmak için bu iki yöntemden birini temel alan bir şifreleme oluşturabilir veya kendi izleme hizmetinizi, döngüsel HTTP veya doğal olarak yerel DBUS çağrıları gönderecek şekilde genişletebilirsiniz.

1. Seçenek: Python komut dosyasında DBUS ve HTTP API

91e5fdeed83e9354.png

Bu adım, kimlik bilgilerini okumak ve ayarlamak için iki uç nokta gösteren ve nihayetinde DBUS komutlarını çağıran temel bir HTTP hizmeti oluşturur.

OTBR'niz olarak kullanılacak RPi'ye Python 3 bağımlılıklarını yükleyin:

$ pip install dbus-python shlex json

Komut dosyasını şu şekilde çalıştırın:

$  sudo python credentials_server.py 8081
serving at port 8081

Örnek, 8081 numaralı bağlantı noktasında bir HTTP sunucusu oluşturur ve kök yolda, ileti dizisi kimlik bilgilerini almak için bir GET isteği veya ileti dizisi kimlik bilgilerini ayarlamak için bir POST isteği bekler. Yük her zaman TLV içeren bir JSON yapısıdır.

Aşağıdaki PUT isteği, /node/dataset/pending yolunu kullanarak OTBR'de yeni Beklemedeki Mesaj Kimlik Bilgileri ayarlar. Bu durumda, bekleyen kimlik bilgileri 10 saniye içinde uygulanır:

PUT /node/dataset/pending
Host: <IP>:8081
ContentType: "application/json"
acceptMimeType: "application/json"
...
{
        "ActiveDataset": "<TLV encoded new Thread Dataset>"
"PendingTimestamp": {
        "Seconds": <Unix timestamp in seconds>,
        "Ticks": 0,
        "Authoritative": false
},
"Delay": 10000 // in milliseconds
}

/node/dataset/active adresine gönderilen bir GET isteği, şu anda etkin olan kimlik bilgilerini getirir.

GET /node/dataset/active
Host: <IP>:8081
ContentType = "application/json"
acceptMimeType = "text/plain"
...
<TLV encoded Thread Dataset>

Komut dosyası, io.openthread.BorderRouter.wpan0 otobüs yoluna ve /io/openthread/BorderRouter/wpan0 nesne yoluna DBUS R/W komutları çağırır:

# D-BUS interface
def call_dbus_method(interface, method_name, *arguments):
    bus = dbus.SystemBus()
    obj = bus.get_object('io.openthread.BorderRouter.wpan0', '/io/openthread/BorderRouter/wpan0')
    iface = dbus.Interface(obj, interface)
    method = getattr(iface, method_name)
    res = method(*arguments)
    return res

def get_dbus_property(property_name):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter',
                                 property_name)

def set_dbus_property(property_name, property_value):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter',
                                 property_name, property_value)                               

DBUS, kendi özelliklerinin içsel incelenmesine olanak tanır. Bunu aşağıdaki gibi yapabilirsiniz:

$ sudo dbus-send --system --dest=io.openthread.BorderRouter.wpan0 \
        --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 \
        org.freedesktop.DBus.Introspectable.Introspect

Ayrıca, desteklenen özellikleri buradaki dokümanda da bulabilirsiniz.

2. Seçenek: OTBR Agent yerel HTTP Rest API

c748ca5151b6cacb.png

OpenThread Sınır Yönlendiricisi, varsayılan olarak REST_API=1 işaretiyle oluşturulur ve REST API'yi etkinleştirir. Önceki bir codelab'den derlemeniz REST API'yi etkinleştirmediyse RPi'nizde OTBR'yi bu işaretle derlediğinizden emin olun:

$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup

OTBR temsilcisi şu komutu çalıştırarak yeniden başlatılabilir:

$ sudo systemctl restart otbr-agent.service

Temsilci, 8081 numaralı bağlantı noktasında bir HTTP sunucusu başlatır. Bu sunucu, kullanıcının veya izleme programının OTBR'de birçok görevi gerçekleştirmesine olanak tanır (burada açıklanmıştır). İçeriğini incelemek için tarayıcınızı, curl veya wget'ü kullanabilirsiniz. Desteklenen birçok yol arasında, /node/dataset/active üzerinde GET fiili ve /node/dataset/pending üzerinde PUT fiili bulunan yukarıda açıklanan kullanım alanları da yer alır.

3. Android'de Kimlik Bilgisi Çerçevesi'ni ayarlama

Tercih Edilen Kimlik Bilgileri

Android'deki Google Play Hizmetleri, ağınızdaki tüm TBR'ler için kimlik bilgilerinin kaydedilmesine izin verir ve bunu bekler. Her biri, Sınır Yönlendirici Aracısı Kimliği (BAID) ile tanımlanır. Bu görevi gerçekleştirmek için ThreadNetworkClient arayüzünün addCredentials() yöntemini kullanacaksınız. Google Play Hizmetleri depolama alanına eklenen ilk TBR, bu mobil cihaz için Tercih Edilen Kimlik Bilgileri'ni belirler.

BAID'sine bir dizi Thread ağı kimlik bilgisi ekleyen uygulama, kimlik bilgilerinin sahibi olur ve bu bilgilere erişmek için tam izinlere sahiptir. Diğer uygulamalar tarafından eklenen kimlik bilgilerine erişmeye çalışırsanız PERMISSION_DENIED hatası alırsınız. Ancak tercih edilen kimlik bilgileri, kullanıcının izni üzerine her uygulama için her zaman kullanılabilir. Thread Border Router ağı güncellendiğinde Google Play Hizmetleri'nde depolanan kimlik bilgilerini güncel tutmanızı öneririz. Bu bilgiler şu anda kullanılmasa da gelecekte gelişmiş yolculuklar sunabiliriz.

İlk TBR daha sonra hariç tutulsa bile Tercih Edilen Kimlik Bilgileri Android cihazda kalır. Ayarlandıktan sonra, ileti dizisi kimlik bilgilerini yöneten diğer uygulamalar, kimlik bilgilerini bir getPreferredCredentials() çağrısından edinebilir.

Google TBR Sync

Android cihazlar Google TBR'leriyle otomatik olarak senkronize edilir. Android'de kimlik bilgileri yoksa cihazlar bunları ağınızdaki Google TBR'lerinden alır ve bu kimlik bilgileri Tercih Edilen Kimlik Bilgileri olur. TBR'ler ile Android cihaz arasında senkronizasyon yalnızca TBR tek bir kullanıcıyla veya aynı Akıllı Ev'de (yapı) bulunan iki kullanıcıyla eşlenirse gerçekleşir.

Bu işlem, aynı yapıdaki başka bir Google kullanıcısı Android için GHA'da veya iOS için GHA'dayken de gerçekleşir. iOS için GHA'da, tercih edilen kimlik bilgileri yoksa iOS depolama alanında tercih edilen kimlik bilgileri ayarlanır.

Aynı ağda farklı tercih edilen kimlik bilgisi gruplarına sahip iki Android cihaz (veya Android + iGHA) varsa TBR'de öncelik, TBR'yi ilk yapılandıran cihaza verilir.

Üçüncü taraf TBR ilk katılımı

Kimlik bilgisinin depolama alanı şu anda kullanıcının Akıllı Evi (Yapı) tarafından kapsamlandırılmıyor. Her Android cihazın BAID depolama alanı vardır ancak ağda bir Google TBR olduğunda iOS için Google Home uygulamasını çalıştıran diğer Android cihazlar ve iOS cihazlar bu TBR ile senkronize olur ve telefon depolama alanında yerel kimlik bilgilerini ayarlamaya çalışır.

Yeni bir OOB TBR ağ oluşturmadan önce Android'in depolama alanında tercih edilen bir ağın olup olmadığını kontrol etmek önemlidir.

  • Tercih edilen bir ağ varsa tedarikçi bunu kullanmalıdır. Bu sayede, Thread cihazların mümkün olduğunda tek bir Thread ağına bağlanması sağlanır.
  • Tercih edilen ağ yoksa yeni bir kimlik bilgisi grubu oluşturun ve bunu Google Play Hizmetleri'ndeki TBR'nize atayın. Android, bu kimlik bilgilerini tüm Google tabanlı TBR'lerde ayarlanan standart kimlik bilgileri olarak kabul eder. Diğer tedarikçiler de ek cihazlarla ağlı erişiminizi ve sağlamlığınızı artırabilir.

cd8bc726f67b1fa1.png

4. Android uygulamanızı klonlama ve değiştirme

Thread API'ye yapılabilecek ana çağrıları gösteren bir Android uygulaması oluşturduk. Bu kalıpları uygulamanızda kullanabilirsiniz. Bu kod laboratuvarında, Matter için Google Home Örnek Uygulamasını Github'dan klonlayacağız.

Burada gösterilen tüm kaynak kod, örnek uygulamada zaten kodlanmıştır. Bu kodu kendi ihtiyaçlarınıza göre değiştirmeniz önerilir ancak işlevi incelemek için uygulamayı klonlayabilir veya önceden derlenmiş ikili dosyaları çalıştırabilirsiniz.

  1. Aşağıdakileri kullanarak klonlayın:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. Android Studio'yu indirip açın.
  2. Dosya > Aç'ı tıklayın ve klonlanmış deponuzu seçin.
  3. Android telefonunuzda geliştirici modunu etkinleştirin.
  4. USB kablosuyla bilgisayarınıza bağlayın.
  5. Uygulamayı Android Studio'dan <Cmd+R> (OS X) veya <Ctrl+R> (Win, Linux) tuşlarını kullanarak çalıştırın.
  6. Tekerlek -> Geliştirici Araçları -> Thread ağı'na gidin.
  7. Mevcut farklı seçeneklerle etkileşim kurun. Aşağıdaki bölümlerde, her düğmede çalıştırılan kodun paketini açacağız.

Tercih edilen kimlik bilgileri var mı?

TBR üreticisinin Google'a sorması gereken ilk soru, cihazda tercih edilen bir kimlik bilgisi grubunun mevcut olup olmadığıdır. Bu, akışınızın başlangıç noktası olmalıdır. Aşağıdaki kod, kimlik bilgilerinin mevcudiyeti hakkında GPS'den sorgu alır. Hiçbir kimlik bilgisi paylaşılmadığından kullanıcı izni istenmez.

/**
* Prompts whether credentials exist in storage or not. Consent from user is not necessary
*/

fun doGPSPreferredCredsExist(activity: FragmentActivity) {
 try {
   // Uses the ThreadNetwork interface for the preferred credentials, adding
   // a listener that will receive an intentSenderResult. If that is NULL, 
   // preferred credentials don't exist. If that isn't NULL, they exist.
   // In this case we'll not use it.

   ThreadNetwork.getClient(activity).preferredCredentials.addOnSuccessListener { intentSenderResult ->
     intentSenderResult.intentSender?.let { intentSender ->
       ToastTimber.d("threadClient: preferred credentials exist", activity)
       // don't post the intent on `threadClientIntentSender` as we do when
       // we really want to know which are the credentials. That will prompt a
       // user consent. In this case we just want to know whether they exist
     } ?: ToastTimber.d(
       "threadClient: no preferred credentials found, or no thread module found", activity
     )
   }.addOnFailureListener { e: Exception ->
     Timber.d("ERROR: [${e}]")
   }
 } catch (e: Exception) {
   ToastTimber.e("Error $e", activity)
 }
}

GPS için tercih edilen kimlik bilgilerini alma

Varsa kimlik bilgilerini okumak istersiniz. Önceki koddan tek farkı, intentSenderResult aldıktan sonra gönderenden gelen bu sonucu kullanarak bir intent oluşturmak ve başlatmak istemenizdir.

Orijinal kod ViewModel'de (ThreadViewModel.kt) ve intent gözlemcileri Activity Fragment'te (ThreadFragment.kt) olduğu için kodumuzda, organizasyon/mimari amacıyla MutableLiveData<IntentSender?> kullanıyoruz. Bu nedenle, intentSenderResult canlı verilere gönderildikten sonra bu gözlemcinin içeriğini yürütürüz:

viewModel.threadClientIntentSender.observe(viewLifecycleOwner) { sender ->
 Timber.d(
   "threadClient: intent observe is called with [${intentSenderToString(sender)}]"
 )
 if (sender != null) {
   Timber.d("threadClient: Launch GPS activity to get ThreadClient")
   threadClientLauncher.launch(IntentSenderRequest.Builder(sender).build())
   viewModel.consumeThreadClientIntentSender()
 }
}

Bu işlem, kimlik bilgilerini paylaşarak kullanıcı iznini tetikler ve onaylanırsa içerikleri şu yöntemlerle döndürür:

threadClientLauncher =
 registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
   if (result.resultCode == RESULT_OK) {
     val threadNetworkCredentials =
       ThreadNetworkCredentials.fromIntentSenderResultData(result.data!!)
     viewModel.threadPreferredCredentialsOperationalDataset.postValue(
       threadNetworkCredentials
     )
   } else {
     val error = "User denied request."
     Timber.d(error)
     updateThreadInfo(null, "")
   }
 }

Kimlik bilgilerinin MutableLiveData<ThreadNetworkCredentials?>'e gönderilmesi aşağıda açıklanmıştır.

GPS kimlik bilgilerini ayarlama

Mevcut olsun veya olmasın, TBR'nizi Google Play Hizmetleri'ne kaydettirmeniz gerekir. TBR'nizin Sınır Görevlisi kimliğiyle ilişkili kimlik bilgilerini yalnızca uygulamanız okuyabilir. Ancak TBR'niz ilk kaydedilen tarafsa bu kimlik bilgileri Tercih Edilen Kimlik Bilgileri grubuna kopyalanır. Kullanıcı izin verdiği sürece bu bilgilere telefondaki tüm uygulamalar erişebilir.

/**
* Last step in setting the GPS thread credentials of a TBR
*/
private fun associateGPSThreadCredentialsToThreadBorderRouterAgent(
 credentials: ThreadNetworkCredentials?,
 activity: FragmentActivity,
 threadBorderAgent: ThreadBorderAgent,
) {
 credentials?.let {
   ThreadNetwork.getClient(activity).addCredentials(threadBorderAgent, credentials)
     .addOnSuccessListener {
       ToastTimber.d("threadClient: Credentials added", activity)
     }.addOnFailureListener { e: Exception ->
       ToastTimber.e("threadClient: Error adding the new credentials: $e", activity)
     }
 }
}

TBR ürününüzün kimlik bilgilerini ayarlama

Bu bölüm her tedarikçiye özeldir ve bu kod laboratuvarında DBUS+Python HTTP Rest sunucusu veya OTBR'den yerel HTTP Rest sunucusu ile uygularız.

/**
* Creates credentials in the format used by the OTBR HTTP server. See its documentation in
* https://github.com/openthread/ot-br-posix/blob/main/src/rest/openapi.yaml#L215
*/
fun createJsonCredentialsObject(newCredentials: ThreadNetworkCredentials): JSONObject {
 val jsonTimestamp = JSONObject()
 jsonTimestamp.put("Seconds", System.currentTimeMillis() / 1000)
 jsonTimestamp.put("Ticks", 0)
 jsonTimestamp.put("Authoritative", false)

 val jsonQuery = JSONObject()
 jsonQuery.put(
   "ActiveDataset",
   BaseEncoding.base16().encode(newCredentials.activeOperationalDataset)
 )
 jsonQuery.put("PendingTimestamp", jsonTimestamp)
 // delay of committing the pending set into active set: 10000ms
 jsonQuery.put("Delay", 10000)

 Timber.d(jsonQuery.toString())

 return jsonQuery
}

//(...)

var response = OtbrHttpClient.createJsonHttpRequest(
 URL("http://$ipAddress:$otbrPort$otbrDatasetPendingEndpoint"),
 activity,
 OtbrHttpClient.Verbs.PUT,
 jsonQuery.toString()
)

TBR ürününüzden kimlik bilgilerini alma

Daha önce gösterildiği gibi, TBR'nizden kimlik bilgilerini almak için GET HTTP fiili kullanın. Örnek Python komut dosyasını inceleyin.

Derleme ve İçe Aktarma

Android uygulamanızı oluştururken Google Play Hizmetleri Thread Modülü'nü desteklemek için manifest'inizde, derlemenizde ve içe aktarma işlemlerinde değişiklik yapmanız gerekir. Aşağıdaki üç snippet, eklemelerin çoğunu özetler.

Örnek uygulamamızın öncelikle Matter devreye alma işlemi için tasarlandığını unutmayın. Bu nedenle, Manifest ve Gradle dosyaları, yalnızca Thread Kimlik Bilgileri'ni kullanmak için gereken eklemelerden daha karmaşıktır.

Manifest değişiklikleri

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    (...)
    <!-- usesCleartextTraffic needed for OTBR local unencrypted communication -->
    <!-- Not needed for Thread Module, only used for HTTP -->
    <uses-feature
    (...)
        android:usesCleartextTraffic="true">

    <application>
    (...)
    <!-- GPS automatically downloads scanner module when app is installed -->
    <!-- Not needed for Thread Module, only used for scanning QR Codes -->
    <meta-data
        android:name="com.google.mlkit.vision.DEPENDENCIES"
        android:value="barcode_ui"/>
    </application>
</manifest>

Build.gradle

// Thread Network
implementation 'com.google.android.gms:play-services-threadnetwork:16.0.0'
// Thread QR Code Scanning
implementation 'com.google.android.gms:play-services-code-scanner:16.0.0'
// Thread QR Code Generation
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'
// Needed for using BaseEncoding class
implementation 'com.google.guava:guava:31.1-jre'

Alakalı İçe Aktarımlar

// Thread Network Module
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
import com.google.android.gms.threadnetwork.ThreadBorderAgent
import com.google.android.gms.threadnetwork.ThreadNetwork

// Conversion of credentials to/fro Base16 (hex)
import com.google.common.io.BaseEncoding

// HTTP
import java.io.BufferedInputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets

// Co-routines for HTTP calls
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


// JSON
import org.json.JSONObject

// Logs
import timber.log.Timber

// mDNS/SD
import android.net.nsd.NsdServiceInfo

// QR Code reader / writer
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder

5. mDNS/SD Keşfi

Örnek uygulamamız, ağdaki kullanılabilir Thread Sınır Yönlendiricilerinin ve ilgili BAID'lerinin listesini oluşturmak için mDNS/SD keşfini kullanır.

Bu, TBR'nizin bilgilerini GPS kimlik bilgisinin depolama alanına girerken çok faydalıdır. Ancak bu işlevin kullanımı bu codelab'in kapsamı dışındadır. Android Hizmet Keşfi kitaplığını (NSDManager) kullanıyoruz. Kaynak kodunun tamamı, Örnek Uygulama'da ServiceDiscovery.kt yer almaktadır.

6. Konuyu toparlamak gerekirse

Bu çağrıları uyguladıktan veya Örnek Uygulama'yı kullandıktan sonra RPi OTBR'nizi tamamen kullanmaya başlayabilirsiniz. Örnek uygulamamızda 8 düğme gösterilmektedir:

91979bf065e9673d.png

TBR'nizi ilk katılım için kullanabileceğiniz olası bir sıra:

  1. Tercih edilen kimlik bilgilerinin olup olmadığını sorgulama (mavi, 1. satır)
  2. Yanıta bağlı olarak
  3. GPS tercih edilen kimlik bilgilerini alma (mavi, 2. satır)
  4. GPS'de TBR kimlik bilgilerini ayarlama (mavi, 3. satır) -> TBR'nizi seçin -> Rastgele Oluştur -> Ağ adını girin -> Tamam
  5. Tercih ettiğiniz kimlik bilgilerini belirledikten sonra RPi OTBR kimlik bilgilerini ayarla'yı kullanarak OTBR'nize ayarlayın. Bu işlem, kimlik bilgilerini bekleyen gruba uygular.

Örnek uygulamada varsayılan olarak 10 saniyelik gecikme kullanılır. Bu nedenle, bu sürenin ardından RPi TBR'nizin (ve ağında bulunabilecek diğer düğümlerin) kimlik bilgileri yeni veri kümesine taşınır.

7. Sonuç

Bu codelab'de, örnek bir Android uygulamasını klonladık ve Google Play Hizmetleri'nin Thread Storage API'lerini kullanan çeşitli kod snippet'lerini analiz ettik. Bu API'leri, bir tedarikçinin TBR'sini gösteren bir RPi TBR'ye ekleyebileceğimiz ortak bir veri kümesine sahip olmak için kullandık.

Bir kullanıcının tüm TBR'sinin aynı ağda olması, Mesaj Ağı'nın dayanıklılığını ve erişimini artırır. Ayrıca, kimlik bilgilerine erişemedikleri için uygulamaların Thread cihazlarını kullanamadığı kusurlu kullanıcı yolculuklarını da önler.

Bu codelab'in ve örnek uygulamaların, kendi uygulamanızı ve Thread Sınır Yönlendirici ürününüzü tasarlayıp geliştirmenize yardımcı olacağını umuyoruz.

8. Referanslar

RCP yardımcı işlemcisi

DBUS