Contactお問い合わせ
お気軽にお問い合わせください。
(ご案内)
CodeProject は、CodeProject が有用で、開発者にとって価値があると考えられた製品およびサービスに関する情報を提供することを目的に公開されているサイトです。
ここでは、Actian 製品に関連した公開情報の一部をお知らせしております。
なお、お知らせしている情報は、すべて CodeProject に著作権等、帰属しています。本サイトは、提供元より了承を得て、エージーテックが翻訳してご紹介しております。記載情報のご利用の際は、オリジナルサイトとあわせご確認ください。
Carey Payette |
この記事では、Windows IoT Core オペレーティング システムを実行している ARM ベースの Raspberry Pi に Actian Zen Edge Server(for IoT)をインストールし、簡単な時系列データを取得して Zen データベースに書き込み、Zen データベースからデータを取得します。
当記事は、CodeProject においてスポンサー向けに提供する「Product Showcase」セクションにあります。これらの記事は、製品およびサービスに関して、開発者向けに有益と考えられる情報を提供します。
Actian Zen は、数多くの実装において ETL のオーバーヘッドを発生させることなく、シームレスな統合とデータの移植性を提供する組み込みデータベース製品のスイートです。
これらの実装は、Windows および Linux などの本格的なサーバーから、デスクトップ、モバイル デバイスおよび、Windows IoT Core、Windows Nano Server(日本語版は非サポート)、Raspbian Linux などの Intel または ARM チップセットをベースとした IoT ハードウェアにまで及びます。
この記事では、Windows IoT Core オペレーティング システムを実行している ARM ベースの Raspberry Pi に Actian Zen Edge Server(for IoT)をインストールし、簡単な時系列データを取得して Zen データベースに書き込み、Zen データベースからデータを取得します。
サンプル ソリューションでは、Windows IoT Core を実行する Raspberry Pi(ARM)デバイスを、費用対効果の高い高性能な IoT デバイスとして使用します。
このサンプル ソリューションでは、時系列の温度データを取り込みます。この使用事例に示されている温度システムは、実際のデバイスで実行される可能性のある多数のデータ収集プロセスの 1 つにすぎず、データ ストレージ ソリューションは関連データを同時に格納できる必要があります。
デバイス上で直接 Zen を実行すると、統合用プログラミングの作業を可能な限り減らすことできます。プラットフォーム間でデータを変換する場合、ETL(抽出、変換、格納)処理は破損したりエラーが発生したりしやすく、問題を伴う傾向があります。
既述のように、デモンストレーションに使用するデバイスは Windows IoT Core を実行する Raspberry Pi です。このデバイスに Actian Zen をセットアップします。次に、センサー データを読み取って当該デバイス上の Zen データベースに保存するための、C# ユニバーサル Windows プラットフォーム(UWP)アプリケーションを作成します。
この記事の残り部分では、以下の前提条件が満たされているものとします。
Actian Zen for Windows IoT Core をダウンロードするには、「Actian Zen Edge 評価版」で「Windows Iot Core プラットフォーム用」の評価版をお申し込みください。
返信メールに記載されている手順に従って「Actian Zen Edge 評価版(Windows Iot Core プラットフォーム用)」ダウンロードページにて「Zen Edge – Windows IoT Core (ARM)」のファイルをダウンロードしてください。
IoT ダッシュボード アプリケーションの[My Devices](マイ デバイス)タブに表示されるようにデバイスを起動します。お使いのデバイスを右クリックして、[Copy IPv4 address](IPv4 アドレスのコピー)を選択します。
次に、Windows ファイル エクスプローラーのインスタンスを開き、[location](場所)タブの < ipv4_address> に、コピーした IPv4 アドレスを入力します。
\\<ipv4_address>\c$
プロンプトが表示されたら、Windows IoT Core を最初にセットアップした際に Pi デバイスに割り当てた管理者のユーザー名とパスワードを入力します。
ファイル エクスプローラーでネットワーク共有が開いたら、temp という名前の新しいフォルダーを作成し、以前にダウンロードした Actian Zen Edge の zip ファイルを Pi デバイス上の新しい temp ディレクトリにコピーします。わかりやすくするため、zip ファイルの名前を install-zen.zip に変更しておきました(ただし、そのように変更することはまったく任意です)。
IoT ダッシュボード アプリケーションに戻り、デバイスを右クリックして[Launch PowerShell)](PowerShell の起動)を選択します。プロンプトが表示されたら、Pi 管理者のユーザー名とパスワードをもう一度入力して認証を受けます(接続にはしばらく時間がかかります)。
PowerShell プロンプトで、ディレクトリを C のルート フォルダーに移動するため、以下を入力します。
cd\
次に、zip ファイルを展開するため、次のコマンドを実行します(必要に応じて zip ファイルの名前を置き換えてください)。
Expand-Archive -Path "C:\temp\install-zen.zip" -DestinationPath "C:\temp"
これで、次のコマンドを実行することで、Actian Zen Edge データベースをインストールできます。
C:\temp\PSQL\Install-PSQL.ps1 server
データベース エンジンがインストールされる際に、一連の出力が表示されます。インストールが完了すると、メッセージが表示されます。
最後の手順は、Windows ファイアウォール規則を有効にすることです。これを実行するには、次のコマンドを実行します。
& “C:\Program Files\Actian\PSQL\bin\Enable-PsqlFirewallRules.ps1” domain
一連の情報が再度出力された後で、PowerShell プロンプトに戻ります。
データベース エンジンが実行されていることを確認するには、次のコマンドを実行します。
Get-Service -DisplayName “Actian PSQL*”
データベースが実行されていることを確認できたら、PowerShell ウィンドウを閉じて構いません。
デモンストレーション用に、記録するデータを生成する温度センサーが必要です。Raspberry Pi デバイスにはいくつかのオンチップの温度センサーが内蔵されていますが、これらのセンサーは現在、Windows IoT Core では直接利用できなくなっています。
代わりに、Raspberry Pi のようなデバイスを販売している多数のベンダー経由で購入できる、簡単な温度/圧力センサー BMP280 を使用します。この記事では、次に示す図に従ってこのハードウェアを組み立てました。ハードウェア ソリューションを準備する前に、安全のため、必ず Raspberry Pi デバイスをシャットダウンして電源から取り外してください(画面用にどの接地ピンを使用したかによって、代替の接地ピンを使用する必要が生じるかもしれません。必要に応じて、ピン配置図を参照してください。[画像ソース]。
Actian Zen データベースと通信するには、適切な ADO.NET SDK をダウンロードし、それに対して NuGet パッケージ ソースを作成する必要があります。
「Actian Zen/PSQL SDK Library」から、[PSQL ADO.NET Provider 4.3 (.NET Standard 2.0 用)(Actian Zen / PSQL v13 サポート) ]をダウンロードします。
SDK が含まれている zip ファイルをダウンロードしたら、それを任意のフォルダーに展開します。展開したファイルの中には、本プロジェクトで使用する NuGet パッケージが含まれています。このパッケージを使用するために、ローカルの Nuget パッケージ ソースを作成します。
Windows のファイル エクスプローラーを開き、C:\ ドライブに localnuget という名前のフォルダーを作成します。作成した localnuget フォルダーに対し、<ファイルの展開先>\PSQL-SDK-AdoNetDataProvider4.3-NetStandard-13.xx.xxx.000\packages\netstandard2.0\Pervasive.Data.SqlClientStd.4.3.0.nupkg ファイルをコピーします。
Visual Studio 2019(無料の Community Edition で結構です)を開き、[コードなしで続行]リンクをクリックします。
次に、[ツール] -> [NuGet パッケージ マネージャー] -> [パッケージ マネージャー設定]項目にアクセスします。
オプション ウィンドウが表示されたら、[パッケージ ソース]を選択して[+(追加)]ボタンを押します。[名前]に「ローカル」を、[ソース]に「C:\localnuget」を入力します。
既に完成したソリューションをダウンロードしたい場合は、こちらからダウンロードしてください。
Visual Studio 2019 で、[ファイル]メニューにアクセスし、[新規作成]、[プロジェクト]の順に選択します。検索ボックスに「uwp」を入力し、”(空白のアプリ(UWP))” テンプレートを選択して[次へ]ボタンをクリックします。
[新しいプロジェクトを構成します]画面で、プロジェクトに「TempPressure」という名前を付けて[作成]ボタンをクリックします。
[最小バージョン]項目で “Windows 10 Fall Creators Update(10.0; Build 16299)” 以降を選択し、[OK]を押します。
プロジェクトが生成されたら、次にいくつかの NuGet パッケージを追加します。プロジェクト ファイルを右クリックして、[NuGet パッケージの管理]をクリックします。パッケージ ソース nuget.org を選択して、System.Runtime.CompilerServices.Unsafe パッケージを検索してインストールします。
パッケージ ソース nuget.org を引き続き選択したまま、LiveCharts.Uwp パッケージを検索してインストールします。
次に、パッケージ ソースを “ローカル” に変更して検索ボックスをクリアし、Pervasive.Data.SqlClientStd 項目を選択してインストールします。
BMP280.cs という名前のプロジェクトに新しいクラスを追加しましょう。このファイルの内容をこのリンクにあるコードで置き換えます。このコードは、I2C を介して BMP280 センサーとやり取りするために Adafruit によって Windows IoT Core 用に作成されたクラスの微修正されたバージョンです。このコードは、データベースへのデータの格納には関係ないため、説明いたしません。このコードを確認したい場合は、コード内に説明がありますので参照してください。
Reading.cs という別の新しいクラスを作成します。このクラスは、BMP280 から読み取りを行う 1 つのセンサーから取り込んだデータをカプセル化したものであり、Zen データベースに格納するデータも表しています(コードのリストはこちらからも入手可能です)。
using System; namespace TempPressure { public class Reading { public string DeviceName { get; set; } public double Temperature { get; set; } public double Pressure { get; set; } public double Altitude { get; set; } public DateTime ReadingTs { get; set; } } }
次に、ActianZenDataSource.cs という新しいクラスを作成します。このクラスは、Raspberry Pi 上のデータベース エンジンと通信する役割を果たします。例として、データベース エンジンとともにインストールされた既存のデータベースの 1 つである DEMODATA を使用します。
このクラスには、UWP_SensorReadings テーブルを作成および削除する関数、およびレコード挿入とデータ取得を行うコードがあります。このコードが、よく知られている ANSI SQL 構文をどのように使用しているかに注目してください。メソッドの名前は意味が自明となっており、コードについてもわかりやすくするためにコード内に説明があります(コードのリストはこちらからも入手可能です)。
using Pervasive.Data.SqlClient; using System; using System.Collections.Generic; using System.Diagnostics; namespace TempPressure { public class ActianZenDataSource { private string _tableName = "UWP_SensorReadings"; private PsqlConnection _conn = new PsqlConnection("Host=localhost;Port=1583;ServerDSN=DEMODATA;"); private PsqlCommand _cmd = new PsqlCommand(); private void _createTable() { if (!_tableExists()) { var query = $@"CREATE TABLE {_tableName}(DeviceName varchar(50), Temperature double, Pressure double, Altitude double, ReadingTs datetime)"; int recordsAffected = _executeNonQuery(query); Debug.WriteLine((recordsAffected == -1) ? "Table: '" + _tableName + "' Successfully Created !!\n" : string.Empty); } } public void DropTable() { if (_tableExists()) { var query = $@"DROP TABLE {_tableName}"; _executeNonQuery(query); if (_tableExists()) { Debug.WriteLine("Table not dropped\n"); } else { Debug.WriteLine("Table " + _tableName + " Successfully Dropped !!\n"); } } } public void AddReading(Reading reading) { //Create Reading Record in database // **NOTE: SQL String is being generated for debug output reasons only, // IN PRODUCTION UTILIZE named parameters using the PsqlParameter object int recordsAffected = 0; var query = $@"INSERT INTO {_tableName} VALUES ( '{ reading.DeviceName }',{ reading.Temperature }, { reading.Pressure }, {reading.Altitude}, '{ reading.ReadingTs.ToString("yyyy-MM-dd HH:mm:ss") }' )"; if (!_tableExists()) { this._createTable(); } recordsAffected = _executeNonQuery(query); Debug.WriteLine("\nRecords Affected: " + recordsAffected + "\n\n"); } public List<Reading> GetReadings() { var query = $"SELECT * FROM {_tableName}"; PsqlDataReader rdr = null; string logText = string.Empty; List<Reading> retValue = new List<Reading>(); if (_tableExists()) { try { if ((_conn.State != System.Data.ConnectionState.Open)) { _conn.Open(); } _cmd.Connection = _conn; _cmd.CommandText = query; logText = "Query Executed : " + query + "\n\n"; rdr = _cmd.ExecuteReader(); int rowCount = 0; while (rdr.Read()) { Reading rdg = new Reading(); rdg.DeviceName = rdr.GetString(0); rdg.Temperature = rdr.GetDouble(1); rdg.Pressure = rdr.GetDouble(2); rdg.Altitude = rdr.GetDouble(3); rdg.ReadingTs = rdr.GetDateTime(4); retValue.Add(rdg); rowCount++; } logText += "Total Rows :" + rowCount; } catch (Exception ex) { logText = logText + "\nQuery execution failed with exception: " + ex.Message; } finally { _conn.Close(); } Debug.WriteLine(logText); } else { Debug.WriteLine("Table does not exist."); } return retValue; } private int _executeNonQuery(string query) { int rowsAffected = 0; var logText = string.Empty; try { if ((_conn.State != System.Data.ConnectionState.Open)) { _conn.Open(); Debug.WriteLine("Connection Opened: "); } _cmd.Connection = _conn; _cmd.CommandText = query; logText = "Query Executed : " + query + "\n\n"; rowsAffected = _cmd.ExecuteNonQuery(); } catch (Exception ex) { logText = logText + "Query execution failed with exception: " + ex.Message; } finally { _conn.Close(); } Debug.WriteLine(logText); return rowsAffected; } private bool _tableExists() { bool result = false; int count = 0; try { if ((_conn.State != System.Data.ConnectionState.Open)) { _conn.Open(); Debug.WriteLine("Connection Opened: "); } _cmd.Connection = _conn; _cmd.CommandText = $"select count(*) from X$File where Xf$Name = '{_tableName}'"; count = (int)_cmd.ExecuteScalar(); result = (count >= 1); } catch (Exception ex) { Debug.WriteLine("**** Exception : " + ex.Message + " ****"); } finally { _conn.Close(); } return result; } } }
次に、アプリケーションのユーザー インターフェイスを定義しましょう。MainPage.xaml を開き、マークアップを以下のもの(こちらからも入手できます)に置き換えます。
<Page x:Class="TempPressure.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:lvc="using:LiveCharts.Uwp" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Button x:Name="btnCollectReadings" Width="250" Height="250" Click="BtnCollectReadings_Click" Grid.Column="0" Grid.Row="0">Collect 10 Readings</Button> <Button x:Name="btnRefreshReadings" Width="250" Height="250" Grid.Row="0" Grid.Column="1" Click="BtnRefreshReadings_Click">Refresh Readings</Button> <Button x:Name="btnDropTable" Width="250" Height="250" Grid.Row="0" Grid.Column="2" Click="BtnDropTable_Click">Drop Table</Button> <ListBox x:Name="lstReadings" Grid.ColumnSpan="3" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Readings}" > <ListBox.ItemTemplate> <DataTemplate> <Border> <StackPanel Orientation="Vertical"> <TextBlock Text="{Binding DeviceName}"></TextBlock> <StackPanel Orientation="Horizontal"> <TextBlock Text="Temperature (C): "></TextBlock> <TextBlock Text="{Binding Temperature}"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="Pressure (Pa): "></TextBlock> <TextBlock Text="{Binding Pressure}"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="Altitude (m): "></TextBlock> <TextBlock Text="{Binding Altitude}"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="Timestamp: "></TextBlock> <TextBlock Text="{Binding ReadingTs}"></TextBlock> </StackPanel> </StackPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <lvc:CartesianChart Series="{Binding Series}" Grid.ColumnSpan="3" Grid.Row="2" Grid.Column="0"> <lvc:CartesianChart.AxisX> <lvc:Axis LabelFormatter="{Binding LabelFormatter}"></lvc:Axis> </lvc:CartesianChart.AxisX> </lvc:CartesianChart> </Grid> </Page>
実装クラス MainPage.xaml を開き、コードを以下のもの(こちらにもあります)に置き換えます。
using LiveCharts; using LiveCharts.Configurations; using LiveCharts.Uwp; using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; namespace TempPressure { /// <summary> /// Control Screen for reading and displaying temperature data /// </summary> public sealed partial class MainPage : Page { private BMP280 _bmp280; private ActianZenDataSource _ds; private string _deviceName; public ObservableCollection<Reading> Readings { get; set; } //chart items public SeriesCollection Series { get; set; } public Func<double, string> LabelFormatter { get; set; } public MainPage() { this.InitializeComponent(); } //This method will be called by the application framework when the page is first loaded protected override async void OnNavigatedTo(NavigationEventArgs navArgs) { try { //initialize public properties //Create a new object for our barometric sensor class _bmp280 = new BMP280(); //Initialize the sensor await _bmp280.Initialize(); _ds = new ActianZenDataSource(); _deviceName = "CPI3"; Readings = new ObservableCollection<Reading>(); //initialize series configuration and label formatting for the chart var chartConfig = Mappers.Xy<Reading>() .X(model => (double)model.ReadingTs.Ticks) .Y(model => model.Temperature); Series = new SeriesCollection(chartConfig); LabelFormatter = value => new System.DateTime((long)value).ToString("hh:mm:ss tt"); this.DataContext = this; } catch(Exception ex) { Debug.WriteLine(ex.Message); } } private async void BtnCollectReadings_Click(object sender, RoutedEventArgs e) { try { //Create variables to store the sensor data: temperature, pressure and altitude. //Initialize them to 0. double temp = 0; double pressure = 0; double altitude = 0; //Create a constant for pressure at sea level. //This is based on your local sea level pressure (Unit: Hectopascal) //visit https://www.weather.gov/ and input zip code to obtain the barometer value const double seaLevelPressure = 1013.5; //Read 10 samples of the data at an interval of 1 second for (int i = 0; i < 10; i++) { temp = await _bmp280.ReadTemperature(); pressure = await _bmp280.ReadPressure(); altitude = await _bmp280.ReadAltitude(seaLevelPressure); //Write the values to your debug console Debug.WriteLine("Temperature: " + temp.ToString() + " deg C"); Debug.WriteLine("Pressure: " + pressure.ToString() + " Pa"); Debug.WriteLine("Altitude: " + altitude.ToString() + " m"); //add the reading to the Actian table _ds.AddReading(new Reading() { DeviceName = _deviceName, Temperature = temp, Pressure = pressure, Altitude = altitude, ReadingTs = DateTime.Now }); await Task.Delay(1000); } } catch (Exception ex) { Debug.WriteLine(ex.Message); } } private void BtnRefreshReadings_Click(object sender, RoutedEventArgs e) { //update list and chart with values retrieved from the Actian database Readings.Clear(); Series.Clear(); var chartValues = new ChartValues<Reading>(); var readings = _ds.GetReadings(); readings.ForEach((read) => { Readings.Add(read); chartValues.Add(read); }); Series.Add(new LineSeries() { Values = chartValues }); } private void BtnDropTable_Click(object sender, RoutedEventArgs e) { //clean up - remove readings table from the database _ds.DropTable(); } } }
このページ(およびその実装)には以下の 3 つのボタンがあります。
次に、Raspberry Pi デバイス上でアプリケーションを実行しましょう。ターゲット CPU としての “ARM” と “リモート マシン” を選択します(自動的に生成されたリストから Raspberry Pi デバイスを選択するか、または接続するデバイスの IP アドレスを手動で入力します)。
F5 キーを押して Raspberry Pi にアプリケーションを配置します(アプリケーションの初回の配置には、2 回目以降よりも時間が少し長くかかります)。
読み込みが完了したら、[Collect 10 Readings]ボタンを押して Visual Studio のデバッグ出力ウィンドウを観察し、センサーから取得され(その後、Zen データベースに格納され)た読み取り値を確認します。次のような一連の 10 個の読み取り値が表示されます。
Temperature: 23.8488803863525 deg C
Pressure: 98287.984375 Pa
Altitude: 258.046339692289 m
Connection Opened:
Connection Opened:
Query Executed : INSERT INTO UWP_SensorReadings VALUES ( ‘CPI3’,23.8488803863525,
98287.984375, 258.046339692289, ‘2019-04-25 03:18:07’ )
Records Affected: 1
次に、[Refresh Readings]ボタンを押してリスト データをスクロールし、レンダリングされたグラフを観察します。
データの確認が終わったら、[Drop Table]ボタンを押すことで DEMODATA データベースから UWP_SensorReadings テーブルを削除してください。
当記事および関連するソース コードとファイルは、The Code Project Open License (CPOL) に基づいてライセンスされます。