방법: Windows Phone의 자이로스코프 센서에서 데이터 가져오기

2012-02-09

이 항목에서는 자이로스코프 데이터의 그래픽 표현 및 숫자 표시를 가능하게 하는 자이로스코프 응용프로그램을 만드는 과정에 대해 설명합니다. 또한 예제 응용프로그램은 센서 측정값을 종합하여 단말기의 누적 회전각을 확인하는 방법을 알려 줍니다.

자이로스코프 센서는 세 개의 기본 축을 따라 단말기의 회전 속도를 측정합니다. 단말기가 정지해 있으면 자이로스코프 측정값은 모든 축에 대해 0입니다. 비행기 프로펠러처럼, 단말기가 사용자와 마주한 상태에서 중심점을 기준으로 단말기를 회전할 경우 Z축의 회전 속도는 0보다 높아지며, 단말기를 빨리 회전할수록 더 커집니다. 회전 속도는 초당 라디안 단위로 측정되고, 2 * Pi 라디안이 360도 회전입니다. 공간에서 단말기의 절대 방향(요, 피치 롤)을 확인하고 싶으면 Motion 클래스를 사용하여 액세스되는 복합 동작 API를 사용하는 것이 좋습니다.

다음 단계는 자이로스코프 응용프로그램을 만드는 방법을 보여 줍니다.

자이로스코프 응용프로그램을 만들려면

  1. Visual Studio 에서 새 Windows Phone 응용프로그램 프로젝트를 만듭니다. 이 템플릿은 Windows Phone용 Silverlight 카테고리에 있습니다.

  2. 이 응용프로그램에는 센서 API 및 XNA Framework를 포함하는 어셈블리에 대한 참조가 필요합니다. 자이로스코프 데이터가 XNA Framework Vector3 개체 형태로 전달되기 때문입니다. 프로젝트 메뉴에서 참조 추가…를 클릭하고 Microsoft.Devices.SensorsMicrosoft.Xna.Framework를 선택한 다음 확인을 클릭합니다.

  3. MainPage.xaml 파일에서 "ContentPanel"이라는 Grid 요소에 다음 XAML 코드를 넣습니다. 이 코드는 응용프로그램의 현재 상태를 표시하고 현재 자이로스코프 측정을 숫자로 표시하는 데 사용되는 TextBlock 요소를 만듭니다. 이 코드에는 또한 각 축을 따라 회전 속도를 그래픽으로 나타내는 데 사용되는 세 개의 Line 요소가 있습니다. 이 응용프로그램은 단말기 회전의 누적 합계를 유지할 것이므로 TextBlockLine 요소의 다른 집합이 누적 회전각을 표시하기 위해 제공됩니다.

    <StackPanel Orientation="Vertical">
      <StackPanel Orientation="Vertical">
        <TextBlock Height="30" Name="statusTextBlock" Text="status: " VerticalAlignment="Top"  />
        <TextBlock Height="30" Name="timeBetweenUpdatesTextBlock" Text="time between updates: " VerticalAlignment="Top"/>
      </StackPanel>
      <TextBlock Text="current rotational velocity (rads/sec)"></TextBlock>
      <Grid>
        <TextBlock Height="30" HorizontalAlignment="Left" Name="currentXTextBlock" Text="X: 1.0" VerticalAlignment="Top" Foreground="Red" FontSize="28" FontWeight="Bold"/>
        <TextBlock Height="30" HorizontalAlignment="Center" Name="currentYTextBlock" Text="Y: 1.0" VerticalAlignment="Top" Foreground="Green" FontSize="28" FontWeight="Bold"/>
        <TextBlock Height="30" HorizontalAlignment="Right"  Name="currentZTextBlock" Text="Z: 1.0" VerticalAlignment="Top"  Foreground="Blue" FontSize="28" FontWeight="Bold"/>
      </Grid>
      <Grid Height="140">
        <Line x:Name="currentXLine" X1="240" Y1="40" X2="240" Y2="40" Stroke="Red" StrokeThickness="14"></Line>
        <Line x:Name="currentYLine" X1="240" Y1="70" X2="240" Y2="70" Stroke="Green" StrokeThickness="14"></Line>
        <Line x:Name="currentZLine" X1="240" Y1="100" X2="240" Y2="100" Stroke="Blue" StrokeThickness="14"></Line>
      </Grid>
      <TextBlock Text="cumulative rotation (degrees)"></TextBlock>
      <Grid>
        <TextBlock Height="30" HorizontalAlignment="Left" Name="cumulativeXTextBlock" Text="X: 1.0" VerticalAlignment="Top" Foreground="Red" FontSize="28" FontWeight="Bold"/>
        <TextBlock Height="30" HorizontalAlignment="Center" Name="cumulativeYTextBlock" Text="Y: 1.0" VerticalAlignment="Top" Foreground="Green" FontSize="28" FontWeight="Bold"/>
        <TextBlock Height="30" HorizontalAlignment="Right" Name="cumulativeZTextBlock" Text="Z: 1.0" VerticalAlignment="Top"  Foreground="Blue" FontSize="28" FontWeight="Bold"/>
      </Grid>
      <Grid Height="200"  Name="cumulativeGrid">
        <Line x:Name="cumulativeXLine" X1="240" Y1="100" X2="240" Y2="0" Stroke="Red" StrokeThickness="14"></Line>
        <Line x:Name="cumulativeYLine" X1="240" Y1="100" X2="240" Y2="0" Stroke="Green" StrokeThickness="14"></Line>
        <Line x:Name="cumulativeZLine" X1="240" Y1="100" X2="240" Y2="0" Stroke="Blue" StrokeThickness="14"></Line>
      </Grid>
    </StackPanel>
    
    

    다음 그림은 UI가 표시되는 방식입니다.

    자이로스코프 응용프로그램 UI
  4. MainPage.xaml에 추가할 마지막 UI 코드는 나침반으로부터 데이터 가져오기를 시작하거나 중지할 버튼 하나가 있는 응용프로그램 모음의 정의입니다. 주석으로 처리된 응용프로그램 모음 코드(프로젝트 템플릿과 함께 포함) 위에 다음 코드를 붙여 넣습니다.

    <phone:PhoneApplicationPage.ApplicationBar>
      <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
        <shell:ApplicationBarIconButton IconUri="/Images/onoff.png" Text="on/off" Click="ApplicationBarIconButton_Click"/>
      </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>
    
    
  5. 이제 MainPage.xaml.cs 코드 숨김 페이지를 열고 센서 및 XNA Framework 네임스페이스에 대한 using 지시문을 해당 페이지의 맨 위에 있는 다른 using 지시문에 추가합니다. 또한 이 응용프로그램은 DispatcherTimer를 사용하여 UI를 업데이트하므로, System.Windows.Threading 네임스페이스도 포함시킵니다.

    using Microsoft.Devices.Sensors;
    using Microsoft.Xna.Framework;
    using System.Windows.Threading;
    
    
  6. MainPage 클래스 정의의 맨 위에 있는 일부 멤버 변수를 선언합니다.

    public partial class MainPage : PhoneApplicationPage
    {
      Gyroscope gyroscope;
      DispatcherTimer timer;
    
      Vector3 currentRotationRate = Vector3.Zero;
      Vector3 cumulativeRotation = Vector3.Zero;
      DateTimeOffset lastUpdateTime = DateTimeOffset.MinValue;
      bool isDataValid;
    
    

    첫 번째 변수는 Gyroscope 유형의 개체로, 나침반 센서에서 데이터를 가져오는 데 사용됩니다. 다음으로 정기적으로 UI를 업데이트하는 데 사용될 DispatcherTimer가 선언됩니다. currentRotationRatecumulativeRotation 변수는 Gyroscope 클래스의 CurrentValueChanged 이벤트에서 가져온 데이터를 저장하는 데 사용되고 DispatcherTimerTick 이벤트에서 UI를 업데이트하는 데 사용됩니다. lastUpdateTime은 누적 회전각을 계산하는 데 사용되고 isDataValid는 자이로스코프가 현재 활성 상태인지 추적합니다.

  7. 페이지 생성자에서 응용프로그램이 실행되고 있는 단말기의 자이로스코프 센서 지원 여부를 확인합니다. 모든 단말기가 모든 센서를 지원하지는 않으므로 센서를 사용하기 전에 항상 확인해야 합니다. 자이로스코프가 지원되지 않을 경우 메시지가 표시되고 응용프로그램 모음이 숨겨집니다. 자이로스코프가 지원되는 경우 DispatcherTimer가 초기화되고 이벤트 처리기가 할당되지만 타이머는 이 시점에 시작되지 않습니다. 기존 페이지 생성자를 다음 코드로 바꿉니다.

    // Constructor
    public MainPage()
    {
      InitializeComponent();
      if (!Gyroscope.IsSupported)
      {
        // The device on which the application is running does not support
        // the gyroscope sensor. Alert the user and hide the
        // application bar.
        statusTextBlock.Text = "device does not support gyroscope";
        ApplicationBar.IsVisible = false;
      }
      else
      {
        // Initialize the timer and add Tick event handler, but don't start it yet.
        timer = new DispatcherTimer();
        timer.Interval = TimeSpan.FromMilliseconds(60);
        timer.Tick += new EventHandler(timer_Tick);
      }
    }
    
    
  8. 응용프로그램 모음 버튼에 대한 Click 이벤트의 처리기를 추가합니다. 위의 XAML 코드를 추가한 방식에 따라 Visual Studio 에서 이 처리기를 추가했을 수 있습니다. 이 경우 처리기 내부에서 모든 코드를 제거합니다. 처리기가 자동으로 추가되지 않았으면 다음 빈 함수를 복사하여 MainPage 클래스 정의에 붙여 넣습니다.

    private void ApplicationBarIconButton_Click(object sender, EventArgs e)
    {
                
    }
    
  9. 응용프로그램 모음 버튼 클릭 처리기에서 먼저 Gyroscope 개체가 null이 아니고 데이터를 수신 중인지 확인합니다. 그렇다면 사용자가 자이로스코프를 중지하기 위해 버튼을 클릭하는 중이므로 GyroscopeDispatcherTimer 둘 다에 대해 Stop()()()()을 호출합니다. 다음 코드를 빈 버튼 클릭 처리기 내부에 붙여 넣습니다.

      if (gyroscope != null && gyroscope.IsDataValid)
      {
        // Stop data acquisition from the gyroscope.
        gyroscope.Stop();
        timer.Stop();
        statusTextBlock.Text = "gyroscope stopped.";
      }
    
    
  10. 그러면 코드는 사용자가 자이로스코프를 시작하는 경우를 처리하게 됩니다. Gyroscope 개체가 null이면 새 인스턴스를 만듭니다. 원하는 업데이트 간 시간을 설정합니다. 단말기별로 센서가 지원하는 업데이트 간격이 서로 다르므로 이 예에서는 센서 실제 간격을 사용자에게 표시하기 위해 속성이 설정된 후 쿼리됩니다. 그런 다음 자이로스코프에 새 데이터가 생길 때마다 발생하는 CurrentValueChanged 이벤트에 대한 이벤트 처리기가 추가됩니다. 이 코드를 이전 코드 섹션 뒤에 나오는 버튼 클릭 처리기 내부에 붙여 넣습니다.

      else
      {
        if (gyroscope == null)
        {
          // Instantiate the Gyroscope.
          gyroscope = new Gyroscope();
    
          // Specify the desired time between updates. The sensor accepts
          // intervals in multiples of 20 ms.
          gyroscope.TimeBetweenUpdates = TimeSpan.FromMilliseconds(20);
    
          // The sensor may not support the requested time between updates.
          // The TimeBetweenUpdates property reflects the actual rate.
          timeBetweenUpdatesTextBlock.Text = "time between updates: " + gyroscope.TimeBetweenUpdates.TotalMilliseconds + " ms";
    
          gyroscope.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<GyroscopeReading>>(gyroscope_CurrentValueChanged);
        }
    
    
  11. 이제 Start()()()() 메서드를 사용하여 자이로스코프를 시작합니다. Start에 대한 호출이 실패할 수 있으므로 이 호출을 try 블록에 넣어야 합니다. catch 블록에서 사용자에게 자이로스코프를 시작할 수 없음을 경고할 수 있습니다. 또한 이 코드는 DispatcherTimer도 시작합니다. 이 코드를 이전 코드 섹션 뒤에 나오는 버튼 클릭 처리기에 붙여 넣습니다.

        try
        {
          statusTextBlock.Text = "starting gyroscope.";
          gyroscope.Start();
          timer.Start();
        }
        catch (InvalidOperationException ex)
        {
          statusTextBlock.Text = "unable to start gyroscope.";
        }
      }
    
    
  12. 이제 CurrentValueChanged 이벤트 처리기를 구현합니다. 이 메서드는 TimeBetweenUpdates를 사용하여 지정한 빈도로 새 자이로스코프 데이터가 있는 시스템에서 호출됩니다. 처리기는 자이로스코프 데이터가 포함된 GyroscopeReading 개체를 수신합니다. 이 처리기는 UI에 대한 액세스 권한이 없는 백그라운드 스레드에서 호출됩니다. 따라서 이 메서드에서 UI를 수정하려면 UI 스레드에서 지정된 코드를 호출하는 Dispatcher.Invoke 메서드를 사용해야 합니다. 이 예제에서는 타이머를 사용하여 UI를 업데이트하므로 그러한 작업이 필요하지 않습니다. 이 코드는 isDataValid 변수를 Gyroscope 개체의 IsDataValid 멤버로 설정합니다. 그런 다음 코드는 lastUpdateTime 멤버가 이전에 설정된 적이 있는지 확인합니다. 설정된 적이 없으면 이 변수를 자이로스코프 측정의 Timestamp 속성으로 설정하고 메서드를 종료합니다. lastUpdateTime이 이전에 설정되었으면 currentRotationRate 변수는 GyroscopeReading 클래스의RotationRate 멤버로 설정됩니다. 이제 lastUpdateTimeTimeStamp 값에서 빼 업데이트 간 발생한 초의 소수 부분을 확인합니다. 마지막 측정값 업데이트 이후 회전량은 현재 회전 속도에 마지막 업데이트 이후 경과된 시간(초)을 곱한 것입니다. 이러한 두 값의 곱이 cumulativeRotation 변수에 추가됩니다. 마지막으로 lastUpdateTime 변수가 현재 타임스탬프에 업데이트됩니다.

    void gyroscope_CurrentValueChanged(object sender, SensorReadingEventArgs<GyroscopeReading> e)
    {
      // Note that this event handler is called from a background thread
      // and therefore does not have access to the UI thread. To update 
      // the UI from this handler, use Dispatcher.BeginInvoke() as shown.
      // Dispatcher.BeginInvoke(() => { statusTextBlock.Text = "in CurrentValueChanged"; });
    
      isDataValid = gyroscope.IsDataValid;
    
      if (lastUpdateTime.Equals(DateTimeOffset.MinValue))
      {
        // If this is the first time CurrentValueChanged was raised,
        // only update the lastUpdateTime variable.
        lastUpdateTime = e.SensorReading.Timestamp;
      }
      else
      {
        // Get the current rotation rate. This value is in 
        // radians per second.
        currentRotationRate = e.SensorReading.RotationRate;
    
        // Subtract the previous timestamp from the current one
        // to determine the time between readings
        TimeSpan timeSinceLastUpdate = e.SensorReading.Timestamp - lastUpdateTime;
    
        // Obtain the amount the device rotated since the last update
        // by multiplying by the rotation rate by the time since the last update.
        // (radians/second) * secondsSinceLastReading = radiansSinceLastReading
        cumulativeRotation += currentRotationRate * (float)(timeSinceLastUpdate.TotalSeconds);
    
        lastUpdateTime = e.SensorReading.Timestamp;
      }
    }
    
    
  13. 사용자에게 자이로스코프 데이터를 보여 줄 DispatcherTimer Tick 이벤트 처리기를 구현합니다. 이 메서드는 데이터가 수신되고 있음을 나타내기 위해 첫 번째로 상태 TextBlock을 업데이트합니다. 다음으로 TextBlock 개체가 업데이트되어 회전 가속도의 숫자 값 및 센서의 각 축을 기준으로 한 누적 회전각이 표시됩니다. 마지막으로, Line 개체가 업데이트되어 가속도 및 회전을 그래픽으로 나타냅니다.

    void timer_Tick(object sender, EventArgs e)
    {
      if (isDataValid)
      {
        statusTextBlock.Text = "receiving data from gyroscope.";
      }
    
      currentXTextBlock.Text = currentRotationRate.X.ToString("0.000");
      currentYTextBlock.Text = currentRotationRate.Y.ToString("0.000");
      currentZTextBlock.Text = currentRotationRate.Z.ToString("0.000");
    
      cumulativeXTextBlock.Text = MathHelper.ToDegrees(cumulativeRotation.X).ToString("0.00");
      cumulativeYTextBlock.Text = MathHelper.ToDegrees(cumulativeRotation.Y).ToString("0.00");
      cumulativeZTextBlock.Text = MathHelper.ToDegrees(cumulativeRotation.Z).ToString("0.00");
    
    
      double centerX = cumulativeGrid.ActualWidth / 2.0;
      double centerY = cumulativeGrid.ActualHeight / 2.0;
    
      currentXLine.X2 = centerX + currentRotationRate.X * 100;
      currentYLine.X2 = centerX + currentRotationRate.Y * 100;
      currentZLine.X2 = centerX + currentRotationRate.Z * 100;
    
      cumulativeXLine.X2 = centerX - centerY * Math.Sin(cumulativeRotation.X);
      cumulativeXLine.Y2 = centerY - centerY * Math.Cos(cumulativeRotation.X);
      cumulativeYLine.X2 = centerX - centerY * Math.Sin(cumulativeRotation.Y);
      cumulativeYLine.Y2 = centerY - centerY * Math.Cos(cumulativeRotation.Y);
      cumulativeZLine.X2 = centerX - centerY * Math.Sin(cumulativeRotation.Z);
      cumulativeZLine.Y2 = centerY - centerY * Math.Cos(cumulativeRotation.Z);
    }
    
    

표시: