[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]
洗濯物が乾燥しているであろう絶対湿度まで下がる過程で段階的にアプリの表示が変わるようにしました。
絶対湿度は5分間の移動平均値(右側の数値)で判定しています。
あとは実際の浴室乾燥でテストしながらアプリを調整していきます。
これでガス代を少しは節約できそうです。また浴室の相対湿度を70%以下に近づけるよう意識することでカビの発生を低減できます。




[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]
洗濯物が乾燥しているであろう絶対湿度まで下がる過程で段階的にアプリの表示が変わるようにしました。
絶対湿度は5分間の移動平均値(右側の数値)で判定しています。
あとは実際の浴室乾燥でテストしながらアプリを調整していきます。
これでガス代を少しは節約できそうです。また浴室の相対湿度を70%以下に近づけるよう意識することでカビの発生を低減できます。
[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]
エアコンを操作するAPIリクエストのところでつまづきました。
Pythonでは100%成功していましたが、C++では成功率50%以下になりました。色々調べた結果、HMAC_CTXによる署名生成に問題があることが判明し、これを使わずに署名生成して解決しました。
Pythonではシンプルに正常なコードになる場合でも、C++に移植するのは容易ではないことがたまにあります。
#include "signGenerate.h"
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <openssl/buffer.h>
#include <uuid/uuid.h>
std::string generate_uuid() {
uuid_t uuid;
uuid_generate_random(uuid);
char uuid_str[37];
uuid_unparse(uuid, uuid_str);
return std::string(uuid_str);
}
std::string base64_encode(const unsigned char* input, int length) {
BIO *bmem, *b64;
BUF_MEM *bptr;
b64 = BIO_new(BIO_f_base64());
bmem = BIO_new(BIO_s_mem());
b64 = BIO_push(b64, bmem);
BIO_write(b64, input, length);
BIO_flush(b64);
BIO_get_mem_ptr(b64, &bptr);
std::string output(bptr->data, bptr->length - 1);
BIO_free_all(b64);
return output;
}
std::string hmac_sha256(const std::string& key, const std::string& data) {
unsigned char* digest;
digest = HMAC(EVP_sha256(), key.c_str(), key.length(), (unsigned char*)data.c_str(), data.length(), NULL, NULL);
return std::string((char*)digest, SHA256_DIGEST_LENGTH);
}
[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]
右側の浴室乾燥のところにも手を付けました。
とりあえずガワだけそれなりに整えます。
[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]
ボタンの角を丸くして枠を消しラベル色と背景色を指定できるRoundedButtonクラスをChatGPTに作成してもらいました。当然ヘッダファイルもさくっと作ってくれます。
FLTKを苦労して学んでいたころからは信じられない楽さです。もっともあのころにC++の基本やMakefileの作り方を習得していたからできることではあります。
#include "RoundedButton.h"
RoundedButton::RoundedButton(wxWindow* parent, wxWindowID id, const wxString& label, const wxPoint& pos, const wxSize& size, const wxColour& bgColor, const wxColour& labelColor)
: wxButton(parent, id, label, pos, size), m_bgColor(bgColor), m_labelColor(labelColor)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
Bind(wxEVT_PAINT, &RoundedButton::OnPaint, this);
}
void RoundedButton::OnPaint(wxPaintEvent& event)
{
wxAutoBufferedPaintDC dc(this);
wxSize size = GetSize();
wxRect rect(0, 0, size.x, size.y);
dc.SetBrush(wxBrush(m_bgColor));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawRoundedRectangle(rect, 10); // 角の半径を10に設定
dc.SetTextForeground(m_labelColor); // ラベルの色を設定
dc.DrawLabel(GetLabel(), rect, wxALIGN_CENTER);
}
// AUTOボタン
new RoundedButton(bottomPanel, wxID_ANY, "AUTO", wxPoint(100, 5), wxSize(60, 30), wxColour("#00FFFF"), wxColour("#C0C0C0")); // シアン背景、白ラベル
// OFFボタン
new RoundedButton(bottomPanel, wxID_ANY, "OFF", wxPoint(180, 5), wxSize(60, 30), wxColour("#FF00FF"), wxColour("#FFFFFF")); // マゼンタ背景、白ラベル
[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]
wxWidgetsのコードはFLTKとはまた違った独特のクセがあります。
ChatGPTにGUIのコードを書いてもらいました。Swiftと遜色ない洗練された外観です。
やはりGUIは座標を使うのが楽です。SwiftUIやJavaのSwingは書きにくくて苦手です。
#include <wx/wx.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/choice.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <wx/image.h>
#include <wx/bitmap.h>
#include "functions.h"
class MyFrame : public wxFrame
{
public:
MyFrame() : wxFrame(NULL, wxID_ANY, "SwitchBot Manager", wxDefaultPosition, wxSize(600, 400))
{
// パネルを左右に分割
wxPanel* leftPanel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(300, 400));
wxPanel* rightPanel = new wxPanel(this, wxID_ANY, wxPoint(300, 0), wxSize(300, 400));
// 背景色の設定
leftPanel->SetBackgroundColour(wxColour("#00CED1"));
rightPanel->SetBackgroundColour(wxColour("#4B0082"));
// 下部のパネルを追加して背景色を設定
wxPanel* bottomPanel = new wxPanel(leftPanel, wxID_ANY, wxPoint(0, 100), wxSize(300, 300));
bottomPanel->SetBackgroundColour(wxColour("#008B8B"));
// CSVからのデータ読み込み
std::vector<double> data = ReadCsvData();
// 室温と湿度
new wxStaticText(leftPanel, wxID_ANY, "室温", wxPoint(10, 10), wxSize(80, 20));
new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.1f", data[0]), wxPoint(100, 10), wxSize(80, 20));
new wxStaticText(leftPanel, wxID_ANY, "湿度", wxPoint(10, 40), wxSize(80, 20));
new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[1]), wxPoint(100, 40), wxSize(80, 20));
// 不快指数
new wxStaticText(leftPanel, wxID_ANY, "不快指数", wxPoint(10, 70), wxSize(80, 20));
new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[2]), wxPoint(100, 70), wxSize(80, 20));
// エアコン
new wxStaticText(bottomPanel, wxID_ANY, "エアコン", wxPoint(10, 10), wxSize(80, 20));
// AUTOボタン
new wxButton(bottomPanel, wxID_ANY, "AUTO", wxPoint(100, 5), wxSize(60, 30));
// OFFボタン
new wxButton(bottomPanel, wxID_ANY, "OFF", wxPoint(180, 5), wxSize(60, 30));
// 動作温度
new wxStaticText(bottomPanel, wxID_ANY, "動作温度", wxPoint(10, 51), wxSize(72, 18));
wxSlider* operationTempSlider = new wxSlider(bottomPanel, wxID_ANY, 26, 26, 28, wxPoint(110, 50), wxSize(120, 20), wxSL_HORIZONTAL);
wxStaticText* operationTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "26", wxPoint(240, 50), wxSize(40, 20));
// 温度幅
new wxStaticText(bottomPanel, wxID_ANY, "温度幅", wxPoint(10, 81), wxSize(54, 18));
wxSlider* tempRangeSlider = new wxSlider(bottomPanel, wxID_ANY, 3, 1, 5, wxPoint(110, 80), wxSize(120, 20), wxSL_HORIZONTAL);
wxStaticText* tempRangeDisplay = new wxStaticText(bottomPanel, wxID_ANY, "0.3", wxPoint(240, 81), wxSize(40, 20));
// 設定温度
new wxStaticText(bottomPanel, wxID_ANY, "設定温度", wxPoint(10, 111), wxSize(72, 18));
wxSlider* setTempSlider = new wxSlider(bottomPanel, wxID_ANY, 22, 20, 25, wxPoint(110, 110), wxSize(120, 20), wxSL_HORIZONTAL);
wxStaticText* setTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "22", wxPoint(240, 110), wxSize(40, 20));
// ファン
new wxStaticText(bottomPanel, wxID_ANY, "ファン", wxPoint(10, 141), wxSize(53, 18));
wxArrayString fanChoices;
fanChoices.Add("Auto");
fanChoices.Add("Low");
fanChoices.Add("Medium");
fanChoices.Add("High");
new wxChoice(bottomPanel, wxID_ANY, wxPoint(110, 140), wxSize(80, 20), fanChoices);
// Event bindings
operationTempSlider->Bind(wxEVT_SLIDER, [operationTempDisplay](wxCommandEvent& event) {
operationTempDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
});
tempRangeSlider->Bind(wxEVT_SLIDER, [tempRangeDisplay](wxCommandEvent& event) {
tempRangeDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
});
setTempSlider->Bind(wxEVT_SLIDER, [setTempDisplay](wxCommandEvent& event) {
setTempDisplay->SetLabel(wxString::Format("%d", event.GetInt()));
});
}
};
class MyApp : public wxApp
{
public:
virtual bool OnInit()
{
MyFrame* frame = new MyFrame();
frame->Show(true);
return true;
}
};
wxIMPLEMENT_APP(MyApp);
[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]
wxWidgetsのHomebrewでの最新バージョンが3.2.3と遅れているのでGitHub最新の3.2.5を自分でビルドしました。zipではビルドできません。tar.bz2をダウンロードします。
# --with-libtiff=builtinを追加しないとエラーになる
mkdir build-cocoa-debug
cd build-cocoa-debug
../configure --enable-debug --with-libtiff=builtin
make
ビルドしたライブラリとインクルードは/usr/localにコピーしました。アプリのビルド時にwx/setup.hがないというエラーになりましたが、以下のパスにあったのでインクルードフォルダにコピーしました。
/wxWidgets-3.2.5/build-cocoa-debug/lib/wx/include/osx_cocoa-unicode-3.2/wx/setup.h
wxWidgetsを広く普及するためにも、せめてHomebrewに最新版を登録して欲しいところですが、人が足りないのかな。
Makefileは以下の通りです。
# コンパイラ
COMPILER = clang++
DEBUG = -g
# フラグ
CPPFLAGS = -D_FILE_OFFSET_BITS=64 -DWXUSINGDLL -D__WXMAC__ -D__WXOSX__ -D__WXOSX_COCOA__ -std=c++17
LDFLAGS = -framework IOKit -framework Carbon -framework Cocoa -framework QuartzCore -framework AudioToolbox -framework System -framework OpenGL -lwx_osx_cocoau_xrc-3.2 -lwx_osx_cocoau_html-3.2 -lwx_osx_cocoau_qa-3.2 -lwx_osx_cocoau_core-3.2 -lwx_baseu_xml-3.2 -lwx_baseu_net-3.2 -lwx_baseu-3.2 -lc++
# includeパス(-I)
INCLUDE = -I./include -I/Volumes/DATA_m1/code/cpp/mylib/include -I/usr/local/include/wxWidgets
# ライブラリ(-l)
LIBRARY0 =
# ライブラリパス(-L)
LIBRARY = -L/usr/local/lib/wxWidgets
# ソースファイル
SRCDIR = ./src
SRCS = $(shell find $(SRCDIR) -type f)
# オブジェクトファイル
OBJDIR = ./obj
OBJS = $(addprefix $(OBJDIR), $(patsubst ./src/%.cpp,/%.o,$(SRCS)))
# 実行ファイル
TARGETDIR = ./bin
TARGET = SwitchBotManager
# cppファイルからoファイル作成 $<:依存ファイル
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
$(COMPILER) $(CPPFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<
# oファイルから実行ファイルとappファイル作成
$(TARGET): $(OBJS)
$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LIBRARY0) $(LDFLAGS) $(LIBRARY)
mkdir -p $(TARGET).app/Contents/MacOS
mkdir -p $(TARGET).app/Contents/Resources
cp $(TARGETDIR)/$(TARGET) $(TARGET).app/Contents/MacOS/$(TARGET)
cp ./images/$(TARGET).icns $(TARGET).app/Contents/Resources
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" > $(TARGET).app/Contents/Info.plist
echo "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" >> $(TARGET).app/Contents/Info.plist
echo "<plist version=\"1.0\">" >> $(TARGET).app/Contents/Info.plist
echo "<dict>" >> $(TARGET).app/Contents/Info.plist
echo " <key>CFBundleExecutable</key>" >> $(TARGET).app/Contents/Info.plist
echo " <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
echo " <key>CFBundleIconFile</key>" >> $(TARGET).app/Contents/Info.plist
echo " <string>$(TARGET).icns</string>" >> $(TARGET).app/Contents/Info.plist
echo " <key>CFBundleIdentifier</key>" >> $(TARGET).app/Contents/Info.plist
echo " <string>com.yourcompany.$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
echo " <key>CFBundleName</key>" >> $(TARGET).app/Contents/Info.plist
echo " <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
echo " <key>CFBundleVersion</key>" >> $(TARGET).app/Contents/Info.plist
echo " <string>1.0</string>" >> $(TARGET).app/Contents/Info.plist
echo "</dict>" >> $(TARGET).app/Contents/Info.plist
echo "</plist>" >> $(TARGET).app/Contents/Info.plist
# 全ソース強制コンパイル
.PHONY:all
all: clean $(TARGET)
# 全ファイル削除
.PHONY:clean
clean:
rm -rf $(OBJS) $(TARGETDIR)/$(TARGET) $(TARGET).app
[Mac M2 Pro 12CPU, Sonoma 14.5]
SwiftUIではNSOpenPanelやfileImporterでファイル選択しないとローカルファイルを読み込めないことが判明したため、C++に戻りwxWidgetsに挑戦することにしました。
以前wxWidgetsを扱った時は実行ファイルは作成できてもappファイル作成ができなかったのですが、今回はChatGPTに手伝ってもらいあっさりMakefileを完成させました。
ウィジェットのデザインはOS依存ですから、Swiftアプリ風の見た目になります。
次の記事
[Mac M2 Pro 12CPU, Sonoma 14.5]
開発経過をGUIで記録します。
[Mac M2 Pro 12CPU, Sonoma 14.5]
FLTKのスライダーが機能不足なのでSwiftUIに変えました。
デスクトップではウィジェットの位置を座標で決めていたため、SwiftUIでVStackやHStackを使って位置決めするのはとてもやりずらいです。iOSやwatchOSでは違和感がないのですが。
[Mac M2 Pro 12CPU, Sonoma 14.5]
SwitchBot管理アプリMac版の製作に着手しました。手順は以下の通りです。
1.Adobe XDでGUIをデザイン。座標とサイズを自作プラグインで抽出
2.ChatGPT用プロンプトを作成しレスポンスを得る
3.手直ししたC++コードをビルド
このコードを土台に肉付けしていきます。
FLTKアプリを作成します
C++コードを考えてください
ウィンドウのサイズは600*400
各ウィジェットの座標は以下の通り
{
"エアコングラフ": [15, 200, 270, 180],
"ファン選択": [110, 140, 60, 20],
"ファン": [10, 141, 53, 18],
"温度幅表示": [240, 81, 40, 20],
"温度幅スライダー": [110, 80, 120, 20],
"温度幅": [10, 81, 54, 18],
"動作温度表示": [240, 50, 40, 20],
"動作温度スライダー": [110, 50, 120, 20],
"動作温度": [10, 51, 72, 18],
"OFFボタン": [190, 10, 60, 30],
"AUTOボタン": [110, 10, 60, 30],
"エアコン": [10, 15, 80, 20],
"設定温度表示": [240, 110, 40, 20],
"設定温度スライダー": [110, 110, 120, 20],
"設定温度": [10, 111, 72, 18]
}
エアコン:Fl_Boxに"エアコン"を表示
AUTOボタン:ラベルが"AUTO"のFl_Button
OFFボタン:ラベルが"OFF"のFl_Button
動作温度:Fl_Boxに"動作温度"を表示
動作温度スライダー:Fl_Sliderを表示 26から28まで0.1きざみ
動作温度表示:Fl_Boxに動作温度スライダーの値を表示。
温度幅:Fl_Boxに"温度幅"を表示
温度幅スライダー:Fl_Sliderを表示 0.1から0.5まで0.1きざみ
温度幅表示:Fl_Boxに温度幅スライダーの値を表示。
設定温度:Fl_Boxに"設定温度"を表示
設定温度スライダー:Fl_Sliderを表示 20から25まで1.0きざみ
設定温度表示:Fl_Boxに設定温度スライダーの値を表示。
ファン:Fl_Boxに"ファン"を表示
ファン選択:ブルダウンで選択 # fan speed includes 1 (auto), 2 (low), 3 (medium), 4 (high);
エアコングラフ:pngファイルを表示