Arduino用のマルチレベルメニューだけでなく



数か月前、ハブに「ディスプレイでArduinoのマルチレベルメニューを実装する」という記事が掲載されました。 しかし、ちょっと、私は思った。 「6年前にこのメニューを書きました!」

2009年に、私は「照明制御オートメーション」と呼ばれるマイクロコントローラーとディスプレイに基づいた最初のプロジェクトを書きました。 プロジェクトはこれまで正常に作成され、コンパイルされ、動作するようになりました。OSメニューシェルは、 ダメージ指向プログラミングのベストプラクティスを使用して、プロジェクト間を移動しました。 「やめて」と私は言って、コードを書き直した。

選択した品質のレガシーコード、私がそれをどのように書き直したかについての物語、およびそれを使用したい人のための指示があります。

OSメニューの要件と機能


まず、メニューに設定する要件を決定しましょう。
  1. 使いやすさ、ボタンの左右、上下、前後。
  2. 適切な深さ(最大256)のツリー構造。
  3. 全員に十分なメニュー項目の総数(10 ^ 616);
  4. 設定の編集。
  5. プログラムの起動。
  6. シンプルなビルトインタスクマネージャー。

それでも、これらすべてを可能な限り軽量化し、リソースを気にせず、任意のプラットフォームで実行する必要があります(AVRの場合は、GLCDおよびテキストLCDで動作します)。
理論的には、適切なドライバーを使用すると、このOSメニューを簡単に取得してRTOSに接続できます。

ファイル構造


例として、次のメニュー構造(左側のアイテム番号)を分析します。
0 / 1 -  1/ -    3 --  1 4 --  2 5 --  3/ -     .       6 ---  3.1 6 ---  3.2 6 ---  3.3 6 ---  6 ---  3.64 2 -  2/ -    7 --   1 8 --   2 9 --   3 10 --  / 

OSメニューの主な原則は「すべてがファイルです」です。 それで。
各ファイルには、タイプ、名前、親フォルダー、その他のパラメーターがあります
構造について説明します。
 struct filedata{ uint8_t type; uint8_t parent; uint8_t mode1;// 1 uint8_t mode2;// 2 char name[20]; }; 

各ファイルについて、fileData配列に4バイトを定義します。
  1. タイプ、
  2. 親、すべての情報はパンくずリストにあるため、実際には必要ありませんが、レガシーのままです
  3. mode1、各ファイルタイプに固有の2つのパラメーター
  4. mode2


タイプ== T_FOLDER

メインファイルはフォルダーです。 メニュー全体のツリー構造を作成できます。
ここで最も重要なことは、ルートフォルダ番号nullです。 何が起こっても、最終的にはそれに戻ります。
フォルダオプションは
 mode1 =    , mode2 =    . 

ルートフォルダー0には、ファイル1と2、合計2個があります。
次のように説明します。
 T_FOLDER, 0, 1, 2, 


タイプ== T_DFOLDER

フォルダ3には、同じプログラムの複数のコピーが含まれていますが、起動キーが異なります。
たとえば、照明制御ユニットでは、最大16個の毎日のプログラムをそれぞれ16個の間隔で設定できます。 各項目を説明する場合、1024個のファイルが必要です。 実際には、2つで十分です。 パン粉をパラメーターの形でプログラムに送ります。
 mode1 =   ,     mode2 =   . 

単純な数学では、256個のファイルすべてが最大コピー数の動的フォルダーである場合、システム内のメニュー項目の総数は256 ^ 256 = 3.2 x 10 ^ 616になります。 これは、適切であり、そうでない場合には正確に十分です。

タイプ== T_APP

アプリケーション。 そのタスクは、タスクマネージャー(組み込みまたは外部)に登録し、ボタンを制御して編集することです。
 mode1 = id  . 


タイプ== T_CONF

すべての大騒ぎを開始するための設定ファイル。 パラメータのブール値または数値を設定できます。 int16_tで動作します。
 mode1 = id  

構成には独自のconfigsLimit配列があり、各構成には3つのint16_t構成番号があります。
  1. セルID-データを保存するメモリセルの開始番号。 すべてのデータは2バイトを占有します。
  2. 最小-最小データ値
  3. 最大-データの最大値。

たとえば、セル2に-100〜150の数値を書き込むと、行は次の形式になります。
 2, -100, 150, 


タイプ== S_CONF

T_SFOLDERと連携して動作する興味深い(ただし、古いコードのみ)構成
 mode1 = id  


タイプ== T_SFOLDER

特別な種類のフォルダーは、その種類の1つであるため、構成の近くにレンダリングされます。
システムでRS-485プロトコルA、B、またはCを操作できるとしたら、S_CONFタイプのファイルをフォルダーに入れて、必要なファイルを選択してください。 さらに、フォルダに再び移動すると、カーソルはアクティブなオプションを強調表示します。
mode1、mode2はT_FOLDERの場合と同様です。 子ファイルはT_SCONFのみです

リファクタリング結果


私は自分自身でアーキテクチャを改訂するタスクを設定しませんでした。多くの場所で、作業のロジックをそのまま残しました。 非常に面白い松葉杖があります。
主なタスクは、新しいプロジェクトでの使用が簡単になるようにシステムを整理することです。 要約すると:
  1. 少なくとも別のファイルで機能を分離するために、ハードウェアで作業を割り当てます。 HWIには以下が含まれます。
  2. クラス用に書き直されたモジュール。 可能性のあるものはすべてプライベートに隠され、外観は統一され、クラスと多かれ少なかれ統一されたインターフェースを備えたチップが後で便利になります。
  3. RTOSと連携するための「追加」インターフェース。 むしろ、通常のタスクマネージャーは非常に簡単に他のものに置き換えることができます。
  4. コードを整理し、理解しやすくし、マジックナンバーを削除し、インターフェイスを改善しました。 今、彼は見せることを恥じていません。

クロック設定モジュールhwiで書き換えるのが面倒でした。 それでも、完全にやり直す必要があります。 彼はひどいです。
リファクタリングが行われた方法は、リポジトリで明確に見ることができます。

独自のプロジェクトを作成する


プロジェクトのセットアップには、次の項目が含まれます。

ファイル作成

以前に考慮した構造に従って配列を作成しましょう。
 //  static const uint8_t fileStruct[FILENUMB*FILEREW] PROGMEM = { T_FOLDER, 0, 1, 2, //0 T_FOLDER, 0, 3, 3, //1 T_FOLDER, 0, 7, 4, //2 T_APP, 1, 1, 0, //3 T_APP, 1, 2, 0, //4 T_DFOLDER, 1, 6, 66, //5 T_APP, 5, 2, 0, //6 T_CONF, 2, 0, 0, //7 T_CONF, 2, 1, 0, //8 T_CONF, 2, 2, 0, //9 T_APP, 2, 3, 0 //10 }; //  static PROGMEM const char file_0[] = "Root"; static PROGMEM const char file_1[] = "Folder 1"; static PROGMEM const char file_2[] = "Folder 2"; static PROGMEM const char file_3[] = "App 1"; static PROGMEM const char file_4[] = "App 2"; static PROGMEM const char file_5[] = "Dyn Folder"; static PROGMEM const char file_6[] = "App"; static PROGMEM const char file_7[] = "config 0"; static PROGMEM const char file_8[] = "config 1"; static PROGMEM const char file_9[] = "config 2"; static PROGMEM const char file_10[] = "Date and Time"; PROGMEM static const char *fileNames[] = { file_0, file_1, file_2, file_3, file_4, file_5, file_6, file_7, file_8, file_9, file_10 }; 

構成用の配列を作成しましょう:
 //number of cell(step by 2), minimal value, maximum value static const PROGMEM int16_t configsLimit[] = { 0,0,0,// config 0: 0 + 0    2,-8099,8096,//config 1 4,1,48,//config 2 }; 


ボタン設定

地絡故障とプルアップ抵抗を備えたボタンを電源に接続することを好みます。これは、MKで常に利用可能です。

ファイルhw / hwdef.hで、レジスタの名前とボタンの位置を示します。
  #define BUTTONSDDR DDRB #define BUTTONSPORT PORTB #define BUTTONSPIN PINB #define BUTTONSMASK 0x1F #define BSLOTS 5 /**Button mask*/ enum{ BUTTONRETURN = 0x01, BUTTONLEFT = 0x02, BUTTONRIGHT = 0x10, BUTTONUP = 0x08, BUTTONDOWN = 0x04 }; 


ディスプレイ設定

現在、プロジェクトはGLCDv3ライブラリをドラッグしていますが、これは良くありません。 歴史的にそう。
google-codeへのリンク-https ://code.google.com/p/glcd-arduino

アプリケーション作成

基本的なメニュー機能を使用するアプリケーションの例を考えてみましょう。
menuos / app / sampleapp.cpp

次の構造を持つクラスを作成します。
 #ifndef __SAMPLEAPP_H__ #define __SAMPLEAPP_H__ #include "hw/hwi.h" #include "menuos/MTask.h" #include "menuos/buttons.h" class sampleapp { //variables public: uint8_t Setup(uint8_t argc, uint8_t *argv);// .    -       uint8_t ButtonsLogic(uint8_t button);//  uint8_t TaskLogic(void);//  protected: private: uint8_t tick; void Return();//    //functions public: sampleapp(); ~sampleapp(); protected: private: }; //sampleapp extern sampleapp SampleApp; // <s></s>      void SampleAppButtonsHandler(uint8_t button); void SampleAppTaskHandler(); #endif //__SAMPLEAPP_H__ 

そして、主な機能の概要を説明します。
 uint8_t sampleapp::Setup(uint8_t argc, uint8_t *argv) { tick = 0; //     Buttons.Add(SampleAppButtonsHandler);//add button handler Task.Add(1, SampleAppTaskHandler, 1000);//add task ha GLCD.ClearScreen();//  //      GLCD.CursorTo((HwDispGetStringsLength()-11)/2, HwDispGetStringsNumb()/2); GLCD.Puts("Hello Habr"); return 0; } 


ラッパー:
 void SampleAppButtonsHandler(uint8_t button){ SampleApp.ButtonsLogic(button); } void SampleAppTaskHandler(){ SampleApp.TaskLogic(); } 


ボタンハンドラー:
 uint8_t sampleapp::ButtonsLogic(uint8_t button){ switch (button){ case BUTTONLEFT: break; case BUTTONRIGHT: break; case BUTTONRETURN: Return(); break; case BUTTONUP: break; case BUTTONDOWN: break; default: break; } return 0; } 

そして、毎秒呼び出される関数:
 uint8_t sampleapp::TaskLogic(void){ GLCD.CursorTo((HwDispGetStringsLength()-11)/2, HwDispGetStringsNumb()/2+1); GLCD.PrintNumber(tick++); } 

次に、menu.cppに、プログラムが番号2で呼び出されることを記述します。
 void MMenu::AppStart(void){ if (file.mode2 != BACKGROUND){ Task.Add(MENUSLOT, MenuAppStop, 10);//100 ms update Task.ActiveApp = 1;//app should release AtiveApp to zero itself } switch (file.mode1){//AppNumber case 2: SampleApp.Setup(level, brCrumbs); break; case 3: Clock.Setup(level, brCrumbs); break; default: Task.ActiveApp = 0; break; } } 

プロジェクトを組み立てて、得られたものを見てみましょう。


ビジュアルでも同じ


ファイル構造とアーキテクチャに関する詳細でやや退屈な指示、およびビデオ素材での作業例。


リンクとリポジトリ


このプロジェクトはAtmel Studioプログラミング環境で構築されましたが、その日が来て、Eclipseの下で分岐します。 プロジェクトの現在のバージョンは、任意のリポジトリで利用できます(予約)。
  1. GitHubのリポジトリ: https : //github.com/radiolok/menuosv1
  2. Bitbucketリポジトリ: https : //bitbucket.org/radiolok/menuosv1
  3. GLCDv3: https ://code.google.com/p/glcd-arduino/
  4. openLCD: https ://bitbucket.org/bperrybap/openglcd/

Source: https://habr.com/ru/post/J257607/


All Articles