2019-08-09

Raspberry Pi3を使ったリアル波形描画(9) アーキテクチャの設計思想について

本特集記事の「Raspberry Pi3を使ったリアル波形描画」を実現するにあたり、採用したアーキテクチャの設計思想を説明しようと思う。

なお、今回より、Qt で作成したリアル波形描画のシステム(基本フレームワークだけで、フィルタや心拍検出等の中身はスケルトンにしてある)について解説するのだが、実際に動いた実績のあるソースコードを見ながら解説を読んでもらった方がよいと思う。そこで、Qt でコンパイルできるソースファイル一式と Qt の設定画面を PDF にしたものを希望する方が入手できるようにした。(Qt の設定画面を付けたのは、同じ環境にするのは意外と大変なので)

リアル波形描画のソースコード入手方法

入手方法は第一回目の記事『Raspberry Pi3を使ったリアル波形描画(1) イントロダクション』で Windows で実行できるデモファイルを配布したときと同様に、Google Drive の共有設定で共有できるフォルダにパスワード付きの圧縮ファイルを格納してダウンロードできるようにし、パスワードはメールの自動返信機能で送るようにした。

なお、最近、WEBブラウザのセキュリティが強化され、ファイルをダウンロードするときにWEBブラウザによっては、いろいろ聞いてくるようになったようだ。gmailのアドレスを持っていなくてもダウンロードできるはず。共有フォルダがからファイルをダウンロードする手順については IE11 と Microsoft Edge ではできることを確認したので、上手くいかなかったときは、これらのWEBブラウザで試して欲しい。

【リアル波形描画のソースファイルの入手方法】
次のメールアドレスに件名:ソースファイル送信 と書いてメールをお送りください。(件名は”ソースファイル送信”でなければ返信されません。メール本文は何でも構いません。
メールアドレス:CriticalSoftwareConsulting[あっとまーく]gmail.com

リアル波形描画のソースコードについて

本ソースコードは 2019年8月時点で Qt 5.12.2 / Qt creator 4.9.0 にてコンパイル・リンクし、Windows 10上で 動作しているものである。

自分のパソコンに Qtをインストールし、コンパイル/リンクして、実際に動くことを確認した上で、どのようなソースコードで動いているのか確認してみて欲しい。

どんな環境であれ、「実際に動いている」という実績は設計者に取って絶大な安心感につながる。裏返せば、ソフトウェアは一行どころか、1文字変更しただけで、システムが動かなくなるということもある。だから、一度、ちゃんと動いているという確信を得た上で、ソフトウェアを修正しては動作を確認しという作業を繰り返していれば、上手くいかなかったときに少しの後戻りでまたちゃんと動いていたところに戻ることができる。

※設計せずにコーディングをするのはダメという金言をよく聞くが、小さい実験システムでも、まずは動くコードを押さえて安心した上で、コードも設計書もきれいにするというのが自分のスタイルだ。小さいながらも「動くコード」がない状態で、設計書だけを書き進めていって実装する壁にぶつかって止まるのはイヤだ。

さて、「Raspberry Pi3を使ったリアル波形描画アプリ」のユーザーインターフェースは次のようになっている。

画面の説明







これがリアル波形描画のソフトウェアをWindows 上で実行したときの画面になる。
  1. 波形表示領域:上部の3種類のデジタルフィルタを通した後の波形。
  2. デバッグ用波形表示領域:内部処理が意図通りに動いているかどうかを確認するための表示領域。
  3. 入力ソースの切り替え(DEMO, A/D変換器, SIN波):画面に映っているのはDEMO用の三角波。
  4. 波形の切り替え(三角波1, 三角波2, 疑似心電図):三角波1 は心拍数が60になるように調節してある。入力ソースが DEMOのときのみ有効。
  5. 感度切り替え(×1/4, ×1/2, ×1, ×2, ×4)
  6. 波形掃引の開始/停止ボタン:4msインターバルと10msインターバルのスレッドを起動/終了している。
  7. 心拍数の表示:心電図のとがった部分(QRS波形)が1分間に何回計測したかをカウントして表示している。※マークはQRS波形検出時に200ms表示している。
  8. 電源ノイズフィルターON/OFF:心電図に重畳した電源ノイズ50Hz/60Hzを除去するためのデジタルフィルターのON/OFFスイッチ。ソースの中身は空となっている。(自分でフィルタを設計して実装する)
  9. ドリフトフリーフィルタON/OFF:心電図は体動などの影響で基線動揺が起こる。基線が動揺すると画面からはみ出したりして見にくいので、ドリフトフリーフィルタ(ローカットフィルタ)を入れる。ソースの中身は空となっている。(自分でフィルタを設計して実装する)
  10. ハイカットフィルターON/OFF:A/D変換前にアナログのハイカットフィルターを入れるが、それよりも低い周波数帯域のハイカットデジタルフィルター。ソースの中身は空となっている。(自分でフィルタを設計して実装する)
  11. フィルタの遅れ時間:デジタルフィルタをかけると遅れ時間が生じる。特にドリフトフリーフィルタは遅れ時間が大きい。入力波形に対して表示している波形がどれくらい遅れているのか表示する。
  12. 入力波形:フィルタ後の入力波形。
  13. 心拍検出のためのスレッショルドレベルの変化の様子:心拍数を計測するために14のフィルタ出力値のピークを計測して、そのピークを階段状に下げていき、スレッショルドレベルを設定している。(簡易的なものを実装している。このスレッショルドレベルを超えた時が新たなQRSの起点となる。)
  14. 心電図波形の微分値:心拍数を計測するために、入力波形のうち心電図のとがった部分(QRS波形)だけを際立たせるようなフィルタ(現在はサンプルで入力波形を微分して、絶対値を取っている。暫定的なもの。)をかけている。
心拍検出部分とデジタルフィルタをスケルトン(メソッドと入出力のみ実装し、中身を空にしてある)にしているのは、そこに業務ドメイン上のノウハウがあると考えているからであり、その他の部分のソースコードを公開したのは、そのアーキテクチャ自体はドメインには特化していない技術であり、学ぼうと思えばデザインパターンの本など教材は入手する方法がいろいろあるからである。

繰り返すが、もの作りをするソフトウェアエンジニアにとって、「実際に動いている」実績のあるソースコードは貴重で、「動いている」ことを確認した上で、自分自身で一から同じものを作り上げてみて、同じように「動いている」状態にまで仕上げることと、アーキテクチャを含めより理解が深まるし、自信につながる。

こちらはPC上で波形表示を行ったデモ画像


こちらは 5インチのLCD上で波形表示を行ったデモ画像

アーキテクチャの目標

今回のソフトウェアのアーキテクチャの目標は次の通り。
  1. 一発勝負ではなく、商品や商品群として10年間は骨格を変えずに使えるアーキテクチャにしたい。
  2. 変わりやすい部分(画面表示を含むUI)と変わりにくい部分(デジタルフィルタや心拍数検出の論理など)を分離したい。
  3. 心拍数計測等のアルゴリズムの改善作業を見越して、実際の生体信号の入力(A/D変換)ではなく、サンプルデータやSIN波を入力ソースとできるようにしたい。
  4. デジタルフィルタや心拍数計測アルゴリズムが意図通りに働いているかどうかをデバッグ画面で確認できるようにしたい。
  5. 普段はPC上で検討を重ね、シミュレーションで上手くいくことが検証をかさね、その後、実機でバリデーションするようにしたい。(開発効率を高めるため)
  6. タイマー制御、スレッド制御といったリアルタイム制御に関する部分は Qt で行い、作成するアプリケーションソフトウェアからはできるだけ隠蔽したい。
1回動くものができればいいという発想ではなく、商品群として長く保守や性能向上を継続し、開発効率が良く、競争力が高く、研究開発もでき、保守しやすいアーキテクチャを目指している。

ただただ早く動くものを作りたいという考え方でソフトウェアの開発を続けていくと、開発効率は上がらず、過去の作ったソフトウェアの保守に追われるという悪循環から抜け出すことができない。

そのような負のスパイラルから脱却するためには、長持ちして、競争力が高く、保守もしやすいアーキテクチャを目指す必要がある。

実現の可能性(Feasibility Study)の結果

上記の目標を実現するのに重要な「実現可能性」のポイントは、Qt で正確なタイマ割り込みを発生させ、かつ、優先度の高いスレッドと低いスレッドを走らせることができるかどうかだった。

Raspberry Pi3B+ のCPU は ARM Cortex-A53 1.4GHz でクワッドコア(CPU4つ)なので、4ms の定期割り込みの発生など、CPUスペックから考えれば楽勝と思えるのだが、そう簡単にはいかない。

なにしろ、Linux はリアルタイムOSではないし、OSの制限もあり、msオーダーの定期割り込みを発生させるのは簡単ではない。

さらに、このシステムを実現するには、マルチスレッドが必須であり、かつスレッド間のデータの受け渡しが必要で、これが意外に難しい。

でも、Qt の SIGNAL/SLOT のしくみを使うことによって、マルチスレッドとイベントの伝達、データの受け渡しができることが分かった。

ただ、できれば、CPUコアが4つあるので、4msインターバルの割り込み処理を行うスレッドは4つのコアのうち一つに割り当てたかったのだが、Qt 上でその設定ができなかった。

その代わり、定期インターバルの起動間隔に大きなバラツキが発生したいないことは、常に確認できるようにした。

10ms 間隔で起動されるスレッドで、システムタイマーの値を読み込んで、前回のタイマーの値との差分を printf で表示するようにしてある。下記は Windows で走らせたときの様子だ。これを Raspberry Pi3B+ で実行したときも、間隔に大きなずれがなければ、定期割り込みは成功していることになる。ちなみに、下記の例ではインターバル間隔に 0.5% 程度の誤差(ジッター)がある。この誤差がシステムとして許容できないようならば、Raspberry Pi3B+ の外側で発生させるか、Raspberry Pi3B+ にリアルタイムOS を搭載する。今回は、できる限り、ハードウェアの追加なしで実装したかったので、0.5%のジッターは許容したが、常にブレが大きくなっていないかどうかをWatchしておくことは重要だ。

time: 1475803.318404771 CLOCK_REALTIME 09987 DIFF:uSec
time: 1475803.328394707 CLOCK_REALTIME 09989 DIFF:uSec
time: 1475803.338389771 CLOCK_REALTIME 09995 DIFF:uSec
time: 1475803.348481033 CLOCK_REALTIME 10091 DIFF:uSec
time: 1475803.358499317 CLOCK_REALTIME 10018 DIFF:uSec
time: 1475803.368381897 CLOCK_REALTIME 09882 DIFF:uSec
time: 1475803.378349216 CLOCK_REALTIME 09967 DIFF:uSec
time: 1475803.388392228 CLOCK_REALTIME 10043 DIFF:uSec
time: 1475803.398597482 CLOCK_REALTIME 10205 DIFF:uSec
time: 1475803.408382657 CLOCK_REALTIME 09785 DIFF:uSec
time: 1475803.418337612 CLOCK_REALTIME 09954 DIFF:uSec
time: 1475803.428369466 CLOCK_REALTIME 10031 DIFF:uSec
time: 1475803.438366942 CLOCK_REALTIME 09997 DIFF:uSec
time: 1475803.448362910 CLOCK_REALTIME 09995 DIFF:uSec
time: 1475803.458338070 CLOCK_REALTIME 09975 DIFF:uSec
time: 1475803.468366909 CLOCK_REALTIME 10028 DIFF:uSec
time: 1475803.478349608 CLOCK_REALTIME 09982 DIFF:uSec
time: 1475803.488375431 CLOCK_REALTIME 10025 DIFF:uSec
time: 1475803.498362352 CLOCK_REALTIME 09986 DIFF:uSec
time: 1475803.508351083 CLOCK_REALTIME 09988 DIFF:uSec
time: 1475803.518362431 CLOCK_REALTIME 10011 DIFF:uSec
time: 1475803.528348146 CLOCK_REALTIME 09985 DIFF:uSec
time: 1475803.538355272 CLOCK_REALTIME 10007 DIFF:uSec
time: 1475803.548384713 CLOCK_REALTIME 10029 DIFF:uSec
time: 1475803.558325194 CLOCK_REALTIME 09940 DIFF:uSec
time: 1475803.568313623 CLOCK_REALTIME 09988 DIFF:uSec
time: 1475803.578347889 CLOCK_REALTIME 10034 DIFF:uSec
time: 1475803.588317923 CLOCK_REALTIME 09970 DIFF:uSec
time: 1475803.598452309 CLOCK_REALTIME 10134 DIFF:uSec
time: 1475803.608321620 CLOCK_REALTIME 09869 DIFF:uSec
time: 1475803.618302208 CLOCK_REALTIME 09980 DIFF:uSec

ちなみに、この部分を実験したのが『Raspberry Pi3を使ったリアル波形描画(6) Qtでマルチスレッド処理をやってみる』の記事になる。 この実験によって、およそQt上で4ms と 10ms の間隔のタイマー割り込みを発生させ、マルチスレッドで優先度の高いスレッドにタイマー割り込み処理を任せることができた。(目標6) その確認なくして、Qtを使ったリアル波形描画 の構想はありえないし、意味がない。

 また、Qt そのものの特長により、マルチプラットフォームでの開発と、UI部分の開発効率を高めることができることも分かった。(目標2,5) 以前の記事でアーキテクトは「実現の可能性(Feasibility Study)」について「いける!」という感覚をつかめるかどうかが重要だと言うことを書いた。 「実現の可能性(Feasibility Study)」について「いける」のか「いけないのか」を判断するには、その商品に対して求められている要求と要求の優先度が頭の中に入っており、その要求を実現するための実現方法の引き出しをたくさん持っている必要がある。

要求はさまざまあり、全部実現しようとするとコストが上がったり、開発の時間が足りなくなったりするので、競争力の高い商品にするためには、どの部分に独自の研究開発のリソースをつぎ込んで、どの部分はすでにある技術や市販のソフトウェアで補うのか、一番いいバラインスポイントを探す必要がある。

それには深いドメイン知識と、広いソフトウェアエンジニアリングの技術、豊富なソリューションの引き出しを持っていることが大事だ。

ちなみに、オブジェクト思考設計のコンサルタントは「広いソフトウェアエンジニアリングの技術」、「豊富なソリューションの引き出し」を持っているが、「深いドメイン知識」はない。一般的に組込みソフトウェア製品の企業のエンジニアは「深いドメイン知識」を持っているが、「広いソフトウェアエンジニアリングの技術」と「豊富なソリューションの引き出し」を持っている者は少ない。

よって、長持ちして、競争力が高く、保守もしやすいアーキテクチャを目指すためには、両者が協力するか、企業のエンジニアががんばって知識やスキルを身につける必要がある。

なお、ドメイン知識として、ユーザーまたは業務ドメインにおける市場要求の分析は、製品企業側で分析しておく必要がある。お客さんの声を吸い上げることができるのは、製品を設計販売している企業であり、そこに実現すべき機能や性能のもととなる要求がある。

要求の整理についてはQFD(品質機能展開。左図が一例)などを使うとよい。または、マインドマップなどで整理してもよい。 それらの要求が実現可能かどうかは、さまざまな条件(コスト、リソース、エンジニアが使いこなせるかどうか等々)があるので、単純ではない。なにしろ幅広い知識(引き出し)と、最新の情報にアンテナを張っていることが必要になる。 今の世の中情報はあふれかえっているので、やりたいこと、実現したいことは常に頭の中に記憶しておいて、やりたいことの実現に関係ある情報を見つけたら、その情報をクリップしておいて掘り下げるという習慣を付けておく必要がある。 働き方改革で、オンとオフをキッチリ分けて休むときは休んでエネルギーを充電するのはいいと思うが、アンテナはオフのときも立てておいて、実現可能性の種がピピッときたらメモっておくぐらいのとをやっていないと、なかなか競争には勝てない。

静的構造の説明(3層構造)



さて、「Raspberry Pi3を使ったリアル波形描画」のアーキテクチャ(静的な構造、クラス図)について説明していこうと思う。 なお、このアーキテクチャについては今回記事の冒頭でソースコードを入手できるようにしてあるので、ソースコードを見ながらクラス図を眺めて欲しい。

デジタルフィルタと心拍数検出のアルゴリズムについては、それぞれソースコードはインタフェースだけ用意してあり、中身は空または簡易的なものにしてある。(ここが最も重要なノウハウ=コア資産と考えているため)

この部分の性能を高めることができれば、他社に対する競合力となり、その資産は組織の財産となる。また、その資産が真似しにくい若しくは特許で保護されていれば、商品群のコア資産として商品の価値を生み出す宝となりうる。

 このアーキテクチャは プレゼンテーションレイヤー、ドメインレイヤー、データソースレイヤーの3層構造となっている。 プレゼンテーションレイヤーは、ユーザーとのユーザインタフェースを担う部分であり、表現方法は商品群の中でも変わりやすい(ハイエンド製品は大画面、ローエンド機器は小画面など)。逆に言えば、プレゼンテーションレイヤー部分を変えることで、ラインナップを揃えたり、マイナーチェンジをして商品を新しく見せることができる。

仕向け地や特別なユーザー用にカスタマイズすることもあるかもしれない。 プレゼンテーションレイヤーは変わりやすく、変化に柔軟に対応できるようにしておく必要がある。

プレゼンテーションレイヤーの部分の変化に対する対応は Qt Creator を使うことで、簡便にいろいろな画面を実現することが可能だ。

また、ドメインレイヤーは、その商品あるいはその商品群に特有の資産を配置している。ドメインに特化しているため、その商品の本質的な機能や性能を実現していることが多い。別な言い方をすれば「本業の強み」を実現する部分だ。場合によってはソフトウェアだけでなくハードウェアも含めてコア資産となる場合もある。 基本、このドメインは「変わりにくい部分」であり、ノウハウとなり得る。

なお、今回のケースでは、黄色背景のタイマー処理とスレッドのクラスもドメインレイヤーに配置した。これらはドメインに特化したクラスではないものの、プレゼンテーションレイヤーやデータソースレイヤーとも異なり、リアルタイムシステム実現のための要素でり、システム実現の根幹と考えたからである。

データソースレイヤーはその名の通り、システムが使用する入力データの元となるレイヤーであり、ここは、 EcgAcquirer (心電図の受入れ者)から派生させた、3つのクラスを実装している。 その意図は、入力クラスのインタフェースを共通にしておき、入力元の実態を A/D変換器と、サンプルデータと sin波 の3種類で切り替えることができるようにしている。 信号処理アルゴリズムの設計、性能分析、性能向上を検討するためには、リファレンスとなる波形データや、問題が起こったときの 波形データをファイルから読み込んでシミュレーションでさまざまな確認を行うことが重要となる。

アルゴリズムを変更したときなど、今まで動いていた機能や性能が、意図通りに動いているかどうかを確認することはとても重要であり、その確認・検証には、検証に使うシミュレーション用のデータがあるとよい。そのための入力切り替えでもある。

また、デジタルフィルタの効き具合を確かめるのに、シミュレーションでsin波を入力できると都合がよい。 入力の切り替えのしくみは、C言語でも条件コンパイルを駆使したりして実現は可能だが、オブジェクト指向言語であるC++を使えば、スマートに実現できるし、クラス図でその設計思想を表現するのもやりやすい。

実際、このクラス図も、Qtで設計して上手くいくことを確認したC++のソースファイルを UMLツールである Enterprise Architect に読み込ませて、整えることでクラス図にした。 そうすれば、実際のソースコードの設計思想を正確にクラス図で表現できるからだ。 入力の切り替えは、紫背景のQt Creator で作成したボタンの押下をトリガーにして、緑背景の EcgMonitor で入力のオブジェクトインスタンスを入れ替えることで、簡単に切り替えることができる。 これができると非常に気持ちがいい。また、入力のインタフェースが変わらなければ、さらに別の入力例えば、既存の心電図データベースを追加することもできる。
る。

Qt Creator の画面

左記が QtCreator で RealWaveDisplay01 のプロジェクトファイルを開いたときの画面である。ざっと、ソースファイルの説明をしておく。

まず、頭に ap_ がついているファイルは、リアル波形描画特有のアプリケーションを意味する。

qt_ がついているファイルは、Qt に特化した部分、今回は周期起動するスレッド処理。

qcustomplot.h と qcustomplot.cpp はオシロスコープのように右から左に波形を流すために使っているソースで、『Raspberry Pi3を使ったリアル波形描画(3) Qt + QCustomPlot で静止グラフを描いてみる』の記事の一番下に使い方の解説動画がある。

main.cpp は Qt のお決まりの入り口関数で、mainwindow.h と mainwindow.cpp は Qt のメインウインドウの画面の初期化やら、画面で使っている変数や、ボタンが押されたときの処理のメソッドなどが入っている。

アプリケーションのソフトウェアのファイルの説明は次の通り。
  • ap_EcgAcquirorFromSampleData.cpp :DEMO波形のデータ
  • ap_EcgAcquirorFromADConvertor.cpp :A/Dコンバータからの入力(現在スケルトン)
  • ap_EcgAcLineNoiseFilter.cpp :電源ノイズフィルター(現在スケルトン)
  • ap_EcgHighCutFilter.cpp :ハイカットフィルター(現在スケルトン)
  • ap_EcgDriftFreeFilter.cpp :ドリフトフリーフィルタ(現在スケルトン)
  • ap_EcgMonitor.cpp:波形計測の中心となるクラス(各種オブジェクトの生成も行う)
  • ap_EcgAcquirorFromSinWave.cpp:SIN波発生
  • ap_EcgDetectQrsTypeTest1.cpp:QRS検出(簡易的なアルゴリズムにしている)

mainwindow.cpp の説明

mainwindow.cpp 内の MainWindow クラスは QMainWindow クラスから派生したクラスでヘッダは次のようになっている。

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    // コンストラクタ
    explicit MainWindow(QWidget *parent = nullptr);
    // デストラクタ
    ~MainWindow();

    // グラフ初期化
    void intializeGraph();

private slots:
    // Run/Stop ボタンを押された時の処理
    void on_pbt_run_clicked();
    // 波形タイプボタンを押された時の処理
    void on_pbt_wavetype_clicked();
    // 電源ノイズフィルターボタンが押された時の処理
    void on_pbt_ac_line_filter_clicked();
    // ドリフトフリーフィルタボタンが押された時の処理
    void on_pbt_drift_free_filter_clicked();
    // ハイカットフィルタボタンが押されたときの処理
    void on_pbt_hicut_filter_clicked();
    // 表示感度ボタンが押された時の処理
    void on_pbt_displaygain_clicked();
    // 入力ソースボタンが押されたときの処理
    void on_pbt_input_type_clicked();
    // 周波数スピンボックスの値が変化したときの処理
    void on_doubleSpinBox_frequency_valueChanged(double arg1);

public slots:
    // 4ms周期処理
    void work4msPeriodicOnMainWindow(void);
    // 10ms周期処理
    void work10msPeriodicOnMainWindow(void);
private:
    Ui::MainWindow *ui;

    CyclicThread *mpCyclicThread4ms     = new CyclicThread;      // 周期起動のスレッドオブジェクト(プライベートメンバ変数)
    CyclicThread *mpCyclicThread10ms    = new CyclicThread;      // 周期起動のスレッドオブジェクト(プライベートメンバ変数)

    float mTimekey_wave = 0;                        // 波形表示間隔 (s)
    float mWave_data = 0;                           // 波形データ
    float mAdd_data = 0.1F;

    float mTimekey_debugwave=0;                     // デバッグ用波形表示間隔
    float mQrsFilterWave_data=0;                    // QRSフィルt出力用波形データ
    float mThresholdLevel_data=0;                   // スレッショルドレベルデータ

    unsigned short mDisplayCounter=0;               // ディスプレイ用のカウンタ(10msの倍数で表示する)

    bool mRunStopFlag = false;                      // Start/Stop フラグ

    unsigned short mEcgDemoCounter=0;               // 心電図デモカウンター
    unsigned short mDemoWaveType=0;                 // デモ波形タイプ0:三角波 HR60 1:三角波 HR300 2:疑似心電図,);
    float mMaxWave=0;                               // 最大波形
    float mMinWave=0;                               // 最小波形
    float mPreviousPlotData=0;                      // 前回のプロットデータ

    bool mRealWaveFlag=false;                       // リアル波形<->デモ波形スイッチ

    // 心電図モニタクラスを生成
    EcgMonitor  *mpEcgMoniter = new EcgMonitor;     // 心電図モニタクラス

    bool mAcLineFilterFlag;                         // 電源ノイズファルタフラグ
    bool mDriftFreeFilterFlag;                      // ドリフトフリーフィルタフラグ
    bool mHicutFilterFlag;                          // ハイカットフィルターフラグ
    unsigned short mHRDisplayCounter;               // ハートレート表示用カウンタ(1秒)
    unsigned short mQrsMarkDislpayCounter;          // QRSマーク表示用カウンタ
    bool mQrsMarkDislpayFlag;                       // QRSマーク表示用フラグ

    unsigned short mDisplayGain;                    // 表示用の感度変数
    char mGainText[5][6]={("x1/4"), ("x1/2"),("x1  "),("x2  "),("x4  ")};
    unsigned short mInputType;                      // 入力種別
    char mInputTypeText[3][18]={("Demo             "),
                                ("A/D Convertor    "),
                                ("Sin Wave         ")};
    double mFrequency;                              // sin波の周波数

};

頭が on_pbt_・・・ となっているメソッドは、画面上でボタンが押されたときに、呼ばれるメソッドとなる。

各種変数の定義と初期化、4msと10msの周期起動のスレッドの生成、心電図モニタメインクラスの生成などを行っている。(青字部分)

入力の切り替え


入力の切り替え部分を説明する。登場人物の説明
  • EcgAcquirorクラス:心電図入力の基底クラスで実態を持たない。
  • EcgAcquirorFromADConvertor:EcgAcquirorクラスから派生したクラスで、A/D変換器からの入力を意図している。(公開ソースでは、スケルトンにしてある)
  • EcgAcquirorFromSampleData:EcgAcquirorクラスから派生したクラスで、データで定義したサンプルデータ(三角波二種類と疑似心電図データ)を供給することを意図している。
  • EcgAcquirorFromSinWave:EcgAcquirorクラスから派生したクラスで、SIN波のデータをEcgMonitorクラスに供給することを意図している。
EcgAcquiror は 心電図入力の基底クラスで getEcgWaveData() を純粋仮想関数にして、心電図波形データの入力ソースを切り替えられるようにしている。

EcgAcquiror 入力のインタフェースを固定しておき、EcgAcquiror クラスから 派生させた入力ソースのクラスから、さまざまなデータを EcgMonitor クラスに共有する。

EcgAcquirorFromADConvertorクラス以外は、実機(製品)では使わないのだが、デジタルフィルタや心拍計測アルゴリズムの検証したり、デモンストレーションしたりするときに、EcgAcquirorFromSampleDataクラスや、EcgAcquirorFromSinWaveクラスはあると便利だ。

また、例えば、実際にA/D変換して取り込んだ心電図データや、いろいろな心電図のデータライブラリをファイルから読み込むような時には、EcgAcquirorクラスから派生させたクラスを新たに作成して、EcgMonitor クラスに持たせれば 簡単かつ、安全に入力ソースを追加することができる。

この「簡単かつ安全に追加できる」ということがオブジェクト指向言語である C++ を使う目的であり、組込みソフトウェアでも C言語にこだわっている時代ではないと思う理由だ。C言語でも同じことはできると思うが、「簡単かつ安全に追加する」ためにはC++を使った方がよいと思う。

具体的には EcgMonitor クラス のメソッドに 心電図入力オブジェクトのポインタ変数を作っておき、派生させた3つのクラスを new で作成してそれらのポインタをポインタ変数に格納している。

protected:
    // 心電図入力オブジェクトのポインタ(メンバ)をする
    EcgAcquiror *mpEcgAcquiror;
    EcgAcquirorFromSampleData   *mpEcgAcquirorDemo  = new EcgAcquirorFromSampleData;
    EcgAcquirorFromADConvertor  *mpEcgAcquirorADC   = new EcgAcquirorFromADConvertor;

    EcgAcquirorFromSinWave      *mpEcgAcquirorSin   = new EcgAcquirorFromSinWave;


void EcgMonitor::activateEcgDemoMode(void)
{
    // 心電図入力オブジェクトにデモ波形オブジェクトを設定して、デモタイプを三角波にする
    mpEcgAcquiror       = mpEcgAcquirorDemo;
    mpEcgAcquirorDemo->setEcgDemoType(0);

}


void EcgMonitor::activateEcgAdcMode(void)
{
    // 心電図入力オブジェクトにADCオブジェクトを設定する
    mpEcgAcquiror       = mpEcgAcquirorADC;

}


void EcgMonitor::setEcgDemoType(unsigned short type)
{
    if ( type > 2) type = 0;        // 3以上だったら強制的に 0:ECGにする

    // 波形種別を設定する。
    mpEcgAcquirorDemo->setEcgDemoType(type);

}


そして、入力モードの切り替えが指示されたら、mpEcgAcquiror に各、入力ソースのポインタをセットしてあげればよい。

それだけで、その後の処理は何も変える必要がない。

   //----------------------------------------------
    //     内容:疑似心電図三角波
    // サンプリング: 4ms
    //   データ数: 250
    //     振幅:1000
    //----------------------------------------------
    // HR = 60のデータ
        {
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.000 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.010 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.020 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.030 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.040 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.050 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.060 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.070 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.080 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.090 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.100 
               0, 100, 200, 300, 400, 500, 600, 700, 800, 900, // No.110 
            1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, // No.120 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.130 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.140 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.150 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.160 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.170 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.180 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.190 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.200 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.210 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.220 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0, // No.230 
               0,   0,   0,   0,   0,   0,   0,   0,   0,   0 // No.240 

        },

サンプルデータはこんな感じでデータが定義されている。

そして、このような切り替え可能な構造になっていいることは、作成したソースコードを UML ツールに読み込んで、整形して クラス図にすることで、モデルで確認することができる。

それが、上記のクラス図だ。クラス図を描いてからソースコードを作ったり、ソースコードをリバースエンジニアリングしてクラス図を作ったりを繰り返していると、だんだん派生のしくみや、クラス図どおりに実装するにはどうすればよいかが分かってくる。

次回以降は、入力部以外のアーキテクチャを解説していく。