レポート間隔の管理

ここでは、データ レポートのイベント間隔を管理する方法を説明します。ここで示す情報の大部分については、時間センサー サンプルから引用したコード例を使用して説明します。

センサー ドライバーは、レポート イベントの間隔に関する次の 2 つのプロパティをサポートしている必要があります。

意味

SENSOR_PROPERTY_CURRENT_REPORT_INTERVAL

センサー データ レポートが生成されるまでの現在の経過時間 (ミリ秒単位)。この値は、アプリケーションで Sensor API ISensor::SetProperties メソッドを使用して設定でき、ドライバーがデータ更新イベントを管理する方法を判断するときのヒントとして使用されます。

SENSOR_PROPERTY_MIN_REPORT_INTERVAL

ハードウェアがセンサー データ レポートの生成をサポートする最小経過時間の設定 (ミリ秒単位)。この値は、センサーのハードウェアの機能によって決定され、一定の値に保持する必要があります。

 

イベントをサブスクライブすることによってセンサー データを使用するアプリケーションでは、SENSOR_PROPERTY_CURRENT_REPORT_INTERVAL プロパティを設定することによって、データ更新イベントの頻度を指定できます。センサーのイベントは、サブスクライブしているすべてのクライアントにブロードキャストされます。一定の期間に各センサーについて指定できるレポート間隔は 1 つだけです。

センサー クラス拡張は、クライアント アプリケーションの接続、接続解除、イベントのサブスクライブ、およびイベントのサブスクライブの解除時に、通知を行います。接続されたアプリケーションの有効期間は、多くの場合、次の表のように進行します。

  1. アプリケーションが接続されます。

  2. アプリケーションがレポート間隔を指定します。

  3. アプリケーションがイベントをサブスクライブします。

  4. アプリケーションがイベントを処理します。

  5. アプリケーションがイベントのサブスクライブを解除します。

  6. アプリケーションが接続解除されます。

手順 5. と 6. は、アプリケーションが明示的にイベントのサブスクライブを解除しない場合でも、常にこの順序で発生します。ドライバーは、センサー プラットフォームの制御によって、接続解除呼び出しの前に、必ずサブスクライブ解除呼び出しを受信します。同様に、ドライバーは、センサー プラットフォームの制御によって、サブスクライブ呼び出しの前に、必ず接続呼び出しを受信します。

戦略の選択

センサーには任意の数のクライアント アプリケーションを同時に接続できますが、使用できるレポート間隔は 1 つだけであるため、使用するレポート間隔を決定するための方法をドライバーに実装する必要があります。必要以上の頻度でイベントを発生させると、使用電力が無駄に増加する可能性があるため、間隔を調整することによって、デバイスで電源管理上のメリットを得ることができます。

わかりやすい戦略の 1 つとして、直近に指定された間隔を使用する方法があります。しかし、この方法には問題があります。たとえば、あるアプリケーションではデータが毎秒必要であるにもかかわらず、直近に指定された間隔が 5 分であったとします。このアプリケーションでは、データ取得の頻度が十分でないため、パフォーマンスに関する問題が発生する可能性があります。

より問題の少ない戦略として、指定されたレポート間隔を記録しておき、一定の規則に従って間隔を選択する方法があります。多くの場合、指定された間隔のうち、デバイスでサポートされている最小間隔以上の最短の間隔を常に選択するのが最善の方法です。これにより、データの更新を頻繁に行う必要のあるアプリケーションには必要なデータを提供できる一方で、他のアプリケーションでは一部の通知を無視できます。大半のドライバーには、この方法をお勧めします。

アプリケーションが指定するレポート間隔として、既定の間隔を使用することもできます。これには、アプリケーションで間隔の値として 0 を指定します。この指定は、他の間隔指定と同様に扱う必要がありますが、ドライバーでは、0 の代わりに既定の間隔を使用できます。

アプリケーションの状態および遷移の管理

アプリケーションの特定の時点における状態は、次のいずれかです。

  • 接続解除された状態。

  • 接続されており、イベントをサブスクライブしておらず、レポート間隔を指定していない状態。

  • 接続されており、イベントをサブスクライブしておらず、レポート間隔を指定している状態。

  • 接続されており、イベントをサブスクライブしており、レポート間隔を指定していない状態。

  • 接続されており、イベントをサブスクライブしており、レポート間隔を指定している状態。

このうち最初の 2 つの状態については、選択する間隔による影響はありません。ただし、2 番目の状態では、アプリケーションがいつでもイベントをサブスクライブできるため、少なくとも接続されたアプリケーションの記録をドライバーで取っておく必要があります。他の 3 つの状態を管理するには、選択した戦略を実装するアルゴリズムを使用する必要があります。

時間センサー サンプルでは、ATL CSimpleMap クラスを使用して、接続されたクライアントの記録を取ります。このサンプルでは、次のメンバー変数が宣言されています

  CSimpleMap<IWDFFile*, ClientData*> Clients;

このサンプル ドライバーは、次のデータ構造体のインスタンスをマップに追加します。

  // Struct to keep track of connected client status.
struct ClientData
{
    BOOL bListening;  // TRUE when client is listening to events.
    ULONG ulInterval;  // Interval requested by client.
};

ドライバーは、クライアントが接続されるたびに、ISensorDriver::OnClientConnect がキーとして提供する IWDFFile ポインターを使用して、この構造体のインスタンスを作成し、マップに追加します。このキーは、一部のクラス拡張コールバック メソッドでアプリケーションを一意に識別するために提供されるもので ("アプリケーション ID")、ドライバーがマップからクライアント データ情報を取得する必要があるときに使用されます**。クライアントがレポート間隔を指定したことがない場合、ulnterval メンバーは 0 に設定されたままになります。クライアントが間隔として 0 を指定すると、このコードでは ulInterval メンバーがドライバーの既定の間隔に設定されます。これ以外の場合は、このメンバーはクライアントが指定した値に設定されます。

次のコード例では、ISensorDriver::OnClientConnect の実装を示します。

  HRESULT CSensorDdi:: OnClientConnect(
        __in IWDFFile* pClientFile,
        __in LPWSTR pwszSensorID
        )
{
    if(NULL == pClientFile ||
       NULL == pwszSensorID)
    {
        return E_POINTER;
    }

    HRESULT hr = S_OK;
    BOOL bRet = FALSE;

    // This memory freed in OnClientDisconnect.
    ClientData* cd = new ClientData();
 
    if(NULL == cd)
    {
        hr = E_OUTOFMEMORY;
    }

    if(SUCCEEDED(hr))
    {
        ::ZeroMemory(cd, sizeof(ClientData));

        // Add this client to the map.
        // Use the current report interval as the default.
        bRet = Clients.Add(pClientFile, cd);
        if(bRet == FALSE)
        {
            hr = E_OUTOFMEMORY;
        }
    }

    return hr;
}

レポート間隔の選択

GetNewReportInterval というヘルパー関数は、接続されたクライアントのマップを 1 つずつ確認し、接続されたクライアントのうちイベントをリッスンしているクライアントが指定したレポート間隔の中から、最も短い間隔を選択します。次のコード例では、この関数の実装を示します。

  ULONG CSensorDdi::GetNewReportInterval()
{
 int iSize = Clients.GetSize();

    ULONG temp = 0;

    // Find the shortest interval stored in the array.
    for(int i = 0; i < iSize; i++)
    {
        ClientData* pCurrent = Clients.GetValueAt(i);

        // We choose the shortest interval
        // from the clients that are listening to events
        // and have explicitly requested an interval.

        if(pCurrent->bListening == TRUE && // listening to events
        pCurrent->ulInterval != 0 && // set an interval at some time in the past
           (0 == temp || pCurrent->ulInterval < temp)) // shortest valid interval
        {
            temp = pCurrent->ulInterval;
        }
    }

    // Never return zero. Zero means use the default because no value is set.
    return 0 == temp ? g_dwDefaultInterval : temp;
}

レポート間隔の変更

クライアントがレポート間隔を指定すると、ドライバーは、マップ内のクライアントの ulInterval メンバーに格納されている値を更新します。次のコード例は、時間センサー サンプルの ISensorDriver::OnSetProperties の実装のうち、現在のレポート間隔が設定された場合のコード部分を示しています。

  //Check for properties that can be set.
if( IsEqualPropertyKey( Key, SENSOR_PROPERTY_CURRENT_REPORT_INTERVAL ) )
{
 ClientData *pCD = NULL;

    // Find the connected client info.
    pCD = Clients.Lookup(appId);
    if(NULL == pCD)
    {
        hr = E_UNEXPECTED;
    }

    if(SUCCEEDED(hr) && var.vt == VT_UI4)
    {
        // Update the client's information in the map.
        if(var.ulVal == 0)
        {
            // Client requested the driver's default interval.
            // Explicitly set the default value.
            // Only freshly initialized ClientData structs should
            // have a zero value for ulInterval. Zero is an indication
            // that the client app never requested an interval.
            pCD->ulInterval = g_dwDefaultInterval;
        }
        else
        {
            // Update the stored report interval for this client.
            pCD->ulInterval = var.ulVal;
        }

        // Update the actual report interval.
        m_ulReportInterval = GetNewReportInterval();

        // Notify the event class about the new interval.
        if(NULL != m_pSampleEvents)
        {
            m_pSampleEvents->SetInterval((DWORD)m_ulReportInterval);
        }
    }
    else if( var.vt == VT_EMPTY )
    {
        //Do not allow deletion of the property
        hr = spResults->SetErrorValue(Key, E_ACCESSDENIED);
        fHasError = TRUE;
    }
    else
    {
        hr = spResults->SetErrorValue(Key, E_INVALIDARG);
        fHasError = TRUE;
    }
}

マップ内のクライアントのエントリにある ulInterval の値は、初期状態では 0 に設定されています。この設定は、クライアントが既定の間隔を指定した状態とほぼ同じと言えます。しかし、クライアントがレポート間隔を指定した後は、クライアントが間隔を指定したことがない状態に戻る方法はありません。これは、クライアントが 0 を指定すると、コードによって明示的に既定の間隔が設定されるためです。このようなクライアントは、別の間隔を指定するか、既定の間隔を指定することしかできません。サンプルの実装では、この制限があることに注意してください。

イベントのサブスクライブ

イベントを生成するドライバーは、クライアント アプリケーションがイベントをサブスクライブしているかどうか記録したうえで、1 つ以上のアプリケーションがリッスンしているときにだけイベントを生成する必要があります。サンプルのコードでは、クライアント アプリケーションがイベントをサブスクライブすると、ClientData 構造体の bListening メンバーが TRUE に設定されます。また、クライアントがイベントのサブスクライブを解除すると、この値が FALSE に設定されます。

次のコード例では、ISensorDriver::OnClientSubscribeToEvents の実装を示します。

  HRESULT CSensorDdi::OnClientSubscribeToEvents(
        __in IWDFFile* pClientFile,
        __in LPWSTR pwszSensorID
        )
{
    if(NULL == pClientFile ||
       NULL == pwszSensorID)
    {
        return E_POINTER;
    }

    HRESULT hr = S_OK;
    ClientData* pCD = NULL;

    if(m_cEventClients == 0)
    {
        // Create the event class.
        m_pSampleEvents = new(CSampleEvents);

        if(NULL == m_pSampleEvents)
        {
            hr = E_OUTOFMEMORY;
        }

        if(SUCCEEDED(hr))
        {
            // Initialize the event class.
            hr = m_pSampleEvents->Initialize(m_spSensorCXT, this, m_ulReportInterval);
        }
    }

    // Increment the count of listening clients.
    m_cEventClients++;

    pCD = Clients.Lookup(pClientFile);
    if(NULL == pCD)
    {
        hr = E_UNEXPECTED;
    }

    if(SUCCEEDED(hr))
    {
        // Mark this client as an event listener.
        pCD->bListening = TRUE;

        // Update the actual report interval.

        // For this sample, we'll just use the helper function to choose the 
        // the correct interval. 
        m_ulReportInterval = GetNewReportInterval();

        // Notify the event class about the new interval.
        if(NULL != m_pSampleEvents)
        {
            m_pSampleEvents->SetInterval((DWORD)m_ulReportInterval);
        }
    }

    return hr;
}

ここで、コードの最適化について考えてみます。ここでは、クライアント アプリケーションがイベントをサブスクライブしたときに、現在のレポート間隔をクライアントが指定した間隔と比較して、短い方を選択するという方法について検討します。短い方のレポート間隔を選択することによって、GetNewReportInterval を呼び出して、クライアントのコレクションから最短の間隔を探す必要がなくなります (クライアントがサブスクライブを解除した場合を除く)。ただし、現在の間隔が既定の間隔と同じである場合、この最適化技法では問題が生じます。

たとえば、時間センサー サンプルで、現在の間隔が 1000 である場合に、コードによる比較が必要な場合を考えます。情報を記録していない場合、現在の間隔が明示的に 1000 に設定されたのか、または既定でこの値になっているのかを判断することはできません。また、比較による選択では、明示的に指定された値が 1000 よりも長い場合に、この値の方が適切であっても、既定の間隔が選択される場合があります。

この状態を修正するには、次のテストを行う必要があります。

  • 0 に設定されたレポート間隔を確認します。0 は、サブスクライブしているアプリケーションが値を指定したことがないことを示しています。

  • 現在の間隔が既定の間隔と同じであるかどうか確認します。

  • その既定の間隔が実際に既定で設定されたものなのか、値によって明示的に指定されたものなのかを確認します。たとえば、既定値が 1000 である場合に、アプリケーションが偶然に 1000 を指定することがあります。

  • 短い方の間隔と既定でない間隔のどちらを選択するのかを、前に示したテストを基準に判断します。

サンプルのコードでは、GetReportInterval ヘルパー関数を使用して、この判断のプロセスを単純化しています。通常、時間センサー サンプルのようなドライバーでは、多数のクライアント アプリケーションが接続されることはないため、適切な間隔の判断は難しくありません。記述対象のセンサー ドライバーに合った方法を判断してください。

イベントのサブスクライブの解除

イベントのサブスクライブを解除するコードは、イベントをサブスクライブするコードに比べると、少し複雑です。現在のレポート間隔が、サブスクライブを解除するクライアントによって設定された間隔である場合、間隔を変更するかどうかをコードで判断する必要があります。次のコード例では、ISensorDriver::OnClientUnsubscribeFromEvents の実装を示します。

  HRESULT CSensorDdi::OnClientUnsubscribeFromEvents(
        __in IWDFFile* pClientFile,
        __in LPWSTR pwszSensorID
        )
{
    if(NULL == pClientFile ||
       NULL == pwszSensorID)
    {
        return E_POINTER;
    }

    HRESULT hr = S_OK;
    ClientData* pCD = NULL;
    BOOL bCurrent = FALSE;

    if(m_cEventClients > 0)
    {
        // Decrement the count of event listeners.
        m_cEventClients--;
    }

    // Find the client that is unsubscribing.
    pCD = Clients.Lookup(pClientFile);
    if(NULL == pCD)
    {
        hr = E_UNEXPECTED;
    }

    if(SUCCEEDED(hr))
    {
        // Unmark this client as an event listener.
        pCD->bListening = FALSE;
    }

    if(SUCCEEDED(hr))
    {
        // Test whether the current report interval equals
        // the one set by this client.
        // There may be multiple clients that requested the
        // same interval, but we have no way of knowing that.
        if(pCD->ulInterval == m_ulReportInterval)
        {
           bCurrent = TRUE;
        }

        // Update the actual report interval.
        m_ulReportInterval = GetNewReportInterval();
    }

    if(m_cEventClients == 0 &&
    m_pSampleEvents != NULL)
    {
        // No clients listening to events.
        // This case will also occur when the class extension
        // shuts down.
        hr = m_pSampleEvents->Uninitialize();

        // Destroy the event class.
        delete m_pSampleEvents;
        m_pSampleEvents = NULL;

        // Use the default interval.
        m_ulReportInterval = (ULONG) g_dwDefaultInterval;
    }
    else if(SUCCEEDED(hr) && bCurrent == TRUE)
    {
        // Notify the event class about the new interval.
        if(NULL != m_pSampleEvents)
        {
            Lock();

            m_pSampleEvents->SetInterval((DWORD)m_ulReportInterval);

            Unlock();
        }
    }

    return hr;
}

接続の解除

サンプルでは、クライアントの接続が解除されると、クライアント ファイル キーを使用して、マップ内の該当する構造体ポインターを探し、メモリを開放します。次のコード例では、ISensorDriver::OnClientDisconnect の実装を示します。

  HRESULT CSensorDdi:: OnClientDisconnect(
        __in IWDFFile* pClientFile,
        __in LPWSTR pwszSensorID
        )
{
    if(NULL == pClientFile ||
       NULL == pwszSensorID)
    {
        return E_POINTER;
    }

    HRESULT hr = S_OK;
    ClientData* pCD = NULL;
    BOOL bRet = FALSE;

    // Find this client in the map.
    pCD = Clients.Lookup(pClientFile);
    if(pCD == NULL)
    {
        hr = E_UNEXPECTED;
    }

    if(SUCCEEDED(hr))
    {
        // Free the client data memory.
        delete pCD;
        pCD = NULL;

        // Remove this client from the array.
        bRet = Clients.Remove(pClientFile);
        if(FALSE == bRet)
        {
            hr = E_UNEXPECTED;
        }
    }

    return hr;
}