背景
- SPLE(ソフトウェアプロダクトライン)開発のイメージが湧かない。
- C言語でオブジェクト指向を実現する方法がよくわからない。
- ESP32を使って実際に動かしたものを見てほしい。
開発環境
- マイコン:ESP32-DevKitC ESP-WROOM-32開発ボード
- PC:MacBook Air (Retina, 13-inch, 2020) macOS:Monterey 12.6
- IDE:Arduino v1.8.13
- サーボモータ:マイクロサーボ SG92R
- LCD1:0.96インチ 128×64ドット有機ELディスプレイ(OLED) 白色
- LCD2:I2C接続キャラクタLCDモジュール 16×2行 白色バックライト付
参考文献
ソフトウェアプロダクトライン開発:
ソフトウェアプロダクトラインエンジニアリング―ソフトウェア製品系列開発の基礎と概念から技法まで
オブジェクト指向関連記事:
オブジェクト指向プログラミング参考書籍:
やりたいこと
- 旅行などで家を空けるときに、実家にいるネコにごはんをあげたい。
- 熱帯魚にもごはんをあげたい。
- LCDが変わっても同じように動かしたい。
システム構成
可変点、変異体の整理
可変点、変異体の定義は「ソフトウェアプロダクトライン開発」を参照いただきたいのですが、わかりやすく言うと、
可変点:製品毎に変化するところ
変異体:実際に可変点に割り当てられるもの
です。可変点毎に変異体が1個以上あります。
そういう性質のことを可変性といいます。
上記やりたいことから可変点、変異体を整理すると、
可変点:
・餌箱の回転させ方
・LCD
餌箱を回転させ方の変異体:
・ネコ用
・熱帯魚用
※ネコだとカリカリを入れた餌箱を一回回転させれば良いですが、
熱帯魚にあげるときは複数回回転させないと上手く餌が出てこない。
LCDの変異体:
・LCD1(OLCD)
・LCD2(1602)
これを図示すると以下のようになります。
(可変性図、VP:Variation Point、V:Variation)
これが今回作る自動給仕システムにおいて変化する点として想定している範囲であり、この範囲内であれば変化したとしても、ちょい変で製品を構成することができる設計を目指します。
上記可変性図は、可変点に着目した図ですが、さらに共通部に着目したフィーチャーツリーなどがありますが割愛します。
C言語における可変性実現方法
製品毎に変化する可変点をソフトウェアでどう実現するか、
C++などのオブジェクト指向言語では言語的にサポートされていますが、C言語で実現しようとすると色々工夫する必要があります。
以下の記事を参考にさせていただきました。
C言語でオブジェクト指向を表現する (インターフェース)
基本的には、必要となる可変点毎にクラスを定義し、
そのクラスには共通インターフェースを持たせます。
メイン処理の中では、共通インターフェースを使って制御を行います。(製品毎に変化しないテンプレート)
製品毎に、変異体のインスタンスを生成しますが、共通クラスのインターフェース経由でインスタンスの実体を呼び出します。
つまり変異体のインスタンス生成部と、変異体の実装さえ変更すれば、共通の処理は変えずに製品を構築することができます。
これはデザインパターンのテンプレートメソッドパターンと呼ばれていて、Java言語で学ぶデザインパターン入門を参考にしています。
クラス設計
ディスプレイに表示するための共通インターフェース「表示」を持つ「ディスプレイ」クラスを継承した「OCLD」クラスと、
「給餌」インターフェースを持つ「給餌」クラスを継承した「ネコ」クラスがあります。
インターフェースクラスのインターフェースは、具象クラスで実装します。
熱帯魚用に変更したい場合や、
LCDを変更する場合、各具象クラスの実装を変更すれば良いです。
※今回は本質でない状態遷移や、ボタン制御、タイマー制御などは省略します。
実装
ディスプレイクラス(実装を持たないインターフェースクラス)
#ifndef ___INTERFACEDISPLAY_H___
#define ___INTERFACEDISPLAY_H___
enum Display_mode {
DISPLAY_MODE_INITIAL,
DISPLAY_MODE_SETTING_MODE,
DISPLAY_MODE_COUNT_MODE,
DISPLAY_MODE_FEED
};
//前方宣言
struct iDisplayMethod;
typedef struct _IDisplay
{
const struct iDisplayMethod *p_methods;
}IDisplay;
typedef struct iDisplayMethod{
//Displayでインターフェースを統一
void (*Display)(IDisplay* const, int display_mode, int SettingTime);
}IDisplayMethod;
#endif
OLCDクラス(ディスプレイクラスを継承した具象クラス)
#include "Display_OLCD.h"
#include <Wire.h>
#include "SSD1306.h"//ディスプレイ用ライブラリを読み込み
SSD1306 display(0x3c, 25, 27); //SSD1306インスタンスの作成(I2Cアドレス,SDA,SCL)
// インターフェース実装本体
// 第一引数にはインターフェースを実装したインスタンスのポインタを指定
static void Display_OLCD(IDisplay* const p_this, int display_mode, int display_time){
Displayolcd* const p = (Displayolcd* const)p_this;
int hours;
int minites;
int seconds;
switch(display_mode){
case DISPLAY_MODE_INITIAL:
break;
case DISPLAY_MODE_SETTING_MODE:
display.init(); //ディスプレイを初期化
display.setFont(ArialMT_Plain_16); //フォントを設定
display.drawString(0, 0, "SettingTime = ");
display.drawString(0, 15, String(display_time) +" h");
display.display(); //指定された情報を描画
break;
case DISPLAY_MODE_COUNT_MODE:
hours = (display_time / 60) / 60;
minites = (display_time / 60) % 60;
seconds = display_time % 60;
display.clear();
display.setFont(ArialMT_Plain_16); //フォントを設定
display.drawString(0, 0, "restTime : ");
display.drawString(0, 15, String(hours) + "h");
display.drawString(32, 15, String(minites) + "m");
display.drawString(70, 15, String(seconds) + "s");
display.display(); //指定された情報を描画
break;
case DISPLAY_MODE_FEED:
display.init(); //ディスプレイを初期化
display.setFont(ArialMT_Plain_16); //フォントを設定
display.drawString(0, 0, "Feed");
display.display(); //指定された情報を描画
break;
}
}
// Feedクラスのインターフェース実装へアクセスするための関数テーブル
static const IDisplayMethod DISPLAY_OLCD_METHODS = {
Display_OLCD
};
// 公開関数定義
void Display_construct( IDisplay* const p_this){
// インターフェースの関数テーブルのポインタ初期化
// インターフェース定義から実装本体へのアクセスが設定できる
((IDisplay*)p_this)->p_methods = &DISPLAY_OLCD_METHODS;
// debug
Serial.begin(9600);
Serial.println("construct!");
//p_this->private_display_mode = AUTO_SET_INITIAL;
display.init(); //ディスプレイを初期化
display.setFont(ArialMT_Plain_16); //フォントを設定
display.drawString(0, 0, "ISHII Feed System"); //(0,0)の位置にHello Worldを表示
display.setFont(ArialMT_Plain_10); //フォントを設定
display.drawString(0, 24, "Please Push Bottom...");
display.display(); //指定された情報を描画
}
給餌クラス(インターフェースクラス)
#ifndef ___INTERFACEFEED_H___
#define ___INTERFACEFEED_H___
//前方宣言
struct iFeedMethod;
typedef struct _IFeed
{
const struct iFeedMethod *p_methods;
}IFeed;
typedef struct iFeedMethod{
//Feedでインターフェースを統一
void (*Feed)(IFeed* const);
}IFeedMethod;
#endif
ネコ クラス(給餌インターフェースを継承した具象クラス)
#ifndef ___FEED_CAT_H___
#define ___FEED_CAT_H___
//インターフェースの実装部のヘッダ
#include "InterfaceFeed.h"
#define AUTO_SET_INITIAL 0x00u
#define AUTO_SET_MODE 0x01u
#define AUTO_END_MODE 0x02u
typedef struct feedcat
{
// インターフェースを実装するため
// インターフェース型の変数をクラスの先頭に定義
IFeed interface;
// 具象クラスなので、メンバ変数も定義可能
const int* private_set_mode;
}FeedCat;
// 公開関数定義
void Feed_construct( IFeed* const p_this);
#endif
#include "Feed_cat.h"
#include <Servo.h>
static Servo myservo1; //Servoオブジェクトを作成
// FeedクラスのSetFeedTimeメソッドのインターフェース実装本体
// 第一引数にはインターフェースを実装したインスタンスのポインタを指定
static void Feed_CatFood(IFeed* const p_this){
FeedCat* const p = (FeedCat* const)p_this;
Serial.println("test1");
myservo1.write(360); //180度へ回転
delay(5000);
myservo1.write(0); //元に戻る
Serial.println("test2");
}
// Feedクラスのインターフェース実装へアクセスするための関数テーブル
static const IFeedMethod FEED_CAT_METHODS = {
Feed_CatFood,
};
// 公開関数定義
void Feed_construct( IFeed* const p_this){
// インターフェースの関数テーブルのポインタ初期化
// インターフェース定義から実装本体へのアクセスが設定できる
((IFeed*)p_this)->p_methods = &FEED_CAT_METHODS;
myservo1.attach(13); //13番ピンにサーボ制御線(オレンジ)を接続
Serial.println("servo_construct");
}
メイン処理
#include "InterfaceFeed.h"
#include "InterfaceDisplay.h"
#include "Display_OLCD.h"
#include "button.h"
#include "Feed_cat.h"
#include "Feed_fish.h"
#define FEEDSYSTEM_SELECT 0x00
#define FEEDSYSTEM_ENTER 0x01
#define FEEDSYSTEM_TIMER 0x02
#define FEEDSYSTEM_FEED 0x03
#define FEEDSYSTEM_END 0x04
#define TIMER_UNELAPSED 0x00
#define TIMER_ELAPSED 0x01
// 変数
static unsigned long SettingTime = 0;
static unsigned long PreSettingTime = 0;
static int FeedSystem_mode = 0;
static int Timer_limit_time = 0;
static unsigned long Timer_setting_time = 0;
// インスタンス
IFeed feed_instance;
IDisplay Display_instance;
static void Feed_processing(IFeed* const p_feed);
static void Timer_settime(unsigned long SettingTime);
static unsigned long Timer_judge_elapsed(unsigned long* io_SettingTime);
void setup() {
//インスタンス初期化
Display_construct((IDisplay*)&Display_instance);
Feed_construct((IFeed*)&feed_instance);
button_init();
}
void loop() {
int ret_buttonread;
int judge_elapsed_time;
// pin14, からボタン入力取得
ret_buttonread = buttonread();
switch(FeedSystem_mode){
case FEEDSYSTEM_SELECT:
if (ret_buttonread == SELECT_BUTTON){
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_SETTING_MODE, SettingTime);
SettingTime++;
}else if(ret_buttonread == ENTER_BUTTON){
FeedSystem_mode = FEEDSYSTEM_ENTER;
}
break;
case FEEDSYSTEM_ENTER:
SettingTime--;//時間調整
Timer_settime(SettingTime);
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_COUNT_MODE, SettingTime);
FeedSystem_mode = FEEDSYSTEM_TIMER;
break;
case FEEDSYSTEM_TIMER:
judge_elapsed_time = Timer_judge_elapsed(&SettingTime);
if (PreSettingTime != SettingTime){
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_COUNT_MODE, SettingTime);
PreSettingTime = SettingTime;
}
if (judge_elapsed_time == TIMER_ELAPSED){
FeedSystem_mode = FEEDSYSTEM_FEED;
}
break;
case FEEDSYSTEM_FEED:
Display_processing((IDisplay*)&Display_instance, DISPLAY_MODE_FEED, SettingTime);
Feed_processing((IFeed*)&feed_instance );
FeedSystem_mode = FEEDSYSTEM_END;
break;
default:
break;
}
}
static void Feed_processing(IFeed* const p_feed ){
p_feed->p_methods->Feed( p_feed );
}
static void Display_processing(IDisplay* const p_display, const int display_mode, const int display_Time){
p_display->p_methods->Display( p_display, display_mode, display_Time);
}
return ret;
}
結果
実家のネコに披露しました、もうご飯に困ることはないとわかると喜んでいる様子でした。
まとめ
本記事の前編では、システムの概要とシステム構造、クラス設計、ソフトの実装について説明しました。
後編は違う変異体を選択した場合に、ソフトのどこを変更しないといけないのか、について説明します。