PLCnext C++プロジェクトでSQLiteを用いたプロセスデータ保存
この記事では、PLCnextコントローラーにプリインストールされているSQLiteデータベースエンジンを用いて、グローバルデータスペース(GDS)経由で提供されるデータを保存する方法を説明します。データベースによりプロセスデータを標準化された形で保存でき、SFTPで他システムへエクスポート可能です。
plcncliツールバージョンがコントローラーファームウェアバージョンと一致することを確認してください。
Eclipse C++プロジェクト作成
PLCnext Info Centerの手順に従い、以下プロパティでEclipse新規C++プロジェクト作成:
- プロジェクト名: CppDB
- コンポーネント名: DBComponent
- プログラム名: DBProgram
- プロジェクト名前空間: CppDB
他の名前でも構いませんが、共通名だとチュートリアルが簡単になります。
プロジェクトにsrcフォルダと同階層で新規フォルダ作成、名前をcmakeとする。フォルダ内にファイル作成、名前をFindSqlite.cmakeとし、以下内容挿入。
FindSqlite.cmake
# Copyright (c) 2018 PHOENIX CONTACT GmbH & Co. KG
# Created by Björn sauer
#
# - Find Sqlite
# Find the Sqlite headers and libraries.
#
# Defined Variables:
# Sqlite_INCLUDE_DIRS - Where to find sqlite3.h.
# Sqlite_LIBRARIES - The sqlite library.
# Sqlite_FOUND - True if sqlite found.
#
# Defined Targets:
# Sqlite::Sqlite
find_path(Sqlite_INCLUDE_DIR NAMES sqlite3.h)
find_library(Sqlite_LIBRARY NAMES sqlite3)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Sqlite DEFAULT_MSG Sqlite_LIBRARY Sqlite_INCLUDE_DIR)
if(Sqlite_FOUND)
set(Sqlite_INCLUDE_DIRS "${Sqlite_INCLUDE_DIR}")
set(Sqlite_LIBRARIES "${Sqlite_LIBRARY}")
mark_as_advanced(Sqlite_INCLUDE_DIRS Sqlite_LIBRARIES)
if(NOT TARGET Sqlite::Sqlite)
add_library(Sqlite::Sqlite UNKNOWN IMPORTED)
set_target_properties(Sqlite::Sqlite PROPERTIES IMPORTED_LOCATION "${Sqlite_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${Sqlite_INCLUDE_DIRS}")
endif()
endif()
DBComponent.cppとDBComponent.hppの内容を以下で置き換え:
DBComponent.hpp
#pragma once
#include "Arp/System/Core/Arp.h"
#include "Arp/System/Acf/ComponentBase.hpp"
#include "Arp/System/Acf/IApplication.hpp"
#include "Arp/Plc/Commons/Esm/ProgramComponentBase.hpp"
#include "DBComponentProgramProvider.hpp"
#include "Arp/Plc/Commons/Meta/MetaLibraryBase.hpp"
#include "Arp/System/Commons/Logging.h"
#include "CppDBLibrary.hpp"
#include "Arp/System/Acf/IControllerComponent.hpp"
#include "Arp/System/Commons/Threading/WorkerThread.hpp"
#include
namespace CppDB {
using namespace Arp;
using namespace Arp::System::Acf;
using namespace Arp::Plc::Commons::Esm;
using namespace Arp::Plc::Commons::Meta;
//#component
class DBComponent : public ComponentBase, public IControllerComponent, public ProgramComponentBase, private Loggable {
public:
// typedefs
public:
// construction/destruction
DBComponent(IApplication& application, const String& name);
virtual ~DBComponent() = default;
public:
// IComponent operations
void Initialize() override;
void LoadConfig() override;
void SetupConfig() override;
void ResetConfig() override;
void PowerDown() override;
public:
// IControllerComponent operations
void Start(void) override;
void Stop(void) override;
public:
// ProgramComponentBase operations
void RegisterComponentPorts() override;
void WriteToDB();
private:
// methods
DBComponent(const DBComponent& arg) = delete;
DBComponent& operator= (const DBComponent& arg) = delete;
public:
// static factory operations
static IComponent::Ptr Create(Arp::System::Acf::IApplication& application, const String& name);
private:
// fields
DBComponentProgramProvider programProvider;
WorkerThread workerThread;
private:
// static fields
static const int workerThreadIdleTimeWrite = 10; // 10 ms
public:
// Ports
//#port
//#attributes(Input)
int16 control = 0;
//#port
//#attributes(Input)
int16 intArray[10] {}; // INT in PLCnext Engineer
//#port
//#attributes(Input)
float32 floatArray[10] {}; // REAL in PLCnext Engineer
//#port
//#attributes(Output)
int16 status = 0;
};
// inline methods of class DBComponent
inline DBComponent::DBComponent(IApplication& application, const String& name)
: ComponentBase(application, ::CppDB::CppDBLibrary::GetInstance(), name, ComponentCategory::Custom)
, programProvider(*this)
, workerThread(make_delegate(this, &DBComponent::WriteToDB), workerThreadIdleTimeWrite, "CppDB.WriteToDatabase") // WorkerThread
, ProgramComponentBase(::CppDB::CppDBLibrary::GetInstance().GetNamespace(), programProvider)
{
}
inline IComponent::Ptr DBComponent::Create(Arp::System::Acf::IApplication& application, const String& name) {
return IComponent::Ptr(new DBComponent(application, name));
}
} // end of namespace CppDB
DBComponent.cpp
#include "DBComponent.hpp"
#include "Arp/Plc/Commons/Esm/ProgramComponentBase.hpp"
namespace CppDB {
sqlite3 *db = nullptr; // pointer to the database
sqlite3_stmt * stmt = nullptr; // needed to prepare
std::string sql = ""; // sqlite statement
int rc = 0; // for error codes of the database
void DBComponent::Initialize() {
// never remove next line
ProgramComponentBase::Initialize();
// subscribe events from the event system (Nm) here
}
void DBComponent::LoadConfig() {
// load project config here
}
void DBComponent::SetupConfig() {
// never remove next line
ProgramComponentBase::SetupConfig();
// setup project config here
}
void DBComponent::ResetConfig() {
// never remove next line
ProgramComponentBase::ResetConfig();
// implement this inverse to SetupConfig() and LoadConfig()
}
#pragma region IControllerComponent operations
void DBComponent::Start() {
// start your threads here accessing any Arp components or services
// open the database connection
// the database path (/opt/plcnext/) and name (database) could be modified
rc = sqlite3_open("/opt/plcnext/database.db", &db);
if( rc ) {
Log::Error("DB - 1 - {}", sqlite3_errmsg(db));
status = 1;
return;
} else{
// modify the database behaviour with pragma statements
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, NULL);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, NULL);
sqlite3_exec(db, "PRAGMA temp_store = MEMORY", NULL, NULL, NULL);
// create tables
sql = "CREATE TABLE IF NOT EXISTS tb0 ("
"_id INTEGER PRIMARY KEY, "
"value1 INTEGER DEFAULT 0, "
"value2 REAL DEFAULT 0.0 );";
// execute the sql-statement
rc = sqlite3_exec(db, sql.c_str(), 0, 0, 0);
if(rc) {
Log::Error("DB - 3 - {}", sqlite3_errmsg(db));
status = 3;
}
}
// prepare sql-statement
sql = "INSERT INTO tb0 (value1, value2) VALUES (?,?)";
rc = sqlite3_prepare_v2(db, sql.c_str(), strlen(sql.c_str()), &stmt, nullptr);
if(rc) {
Log::Error("DB - 4 - {}", sqlite3_errmsg(db));
status = 4;
}
// start the WorkerThread
this->workerThread.Start();
}
void DBComponent::Stop() {
// stop your threads here accessing any Arp components or services
// delete the prepared sqlite statements
rc = sqlite3_finalize(stmt);
{ Log::Error("DB - 1 - {}", sqlite3_errmsg(db)); status = 1; }
// close the database connection
rc = sqlite3_close(db);
{ Log::Error("DB - 1 - {}", sqlite3_errmsg(db)); status = 1; }
// stop the WorkerThread
this->workerThread.Stop();
}
#pragma endregion
void DBComponent::PowerDown() {
// implement this only if data must be retained even on power down event
// Available with 2021.6 FW
}
void DBComponent::WriteToDB() {
// store data in the database
if(control == 1) {
// start transaction
rc = sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
if(rc) { Log::Error("DB - 5 - {}", sqlite3_errmsg(db)); status = 5; }
// iterate over the arrays
for(int i = 0; i < 10; i++) {
// bind values to the prepared statement
rc = sqlite3_bind_int(stmt, 1, intArray[i]);
if(rc) { Log::Error("DB - 6 - {}", sqlite3_errmsg(db)); status = 6; }
rc = sqlite3_bind_double(stmt, 2, floatArray[i]);
if(rc) { Log::Error("DB - 6 - {}", sqlite3_errmsg(db)); status = 6; }
// execute the sqlite statement and reset the prepared statement
rc = sqlite3_step(stmt);
rc = sqlite3_clear_bindings(stmt);
if(rc) { Log::Error("DB - 6 - {}", sqlite3_errmsg(db)); status = 6; }
rc = sqlite3_reset(stmt);
if(rc) { Log::Error("DB - 6 - {}", sqlite3_errmsg(db)); status = 6; }
}
// end transaction
rc = sqlite3_exec(db, "END TRANSACTION", NULL, NULL, NULL);
if(rc) { Log::Error("DB - 5 - {}", sqlite3_errmsg(db)); status = 5; }
}
// delete the database entries
if(control == 2) {
// begin transaction
rc = sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
if(rc) { Log::Error("DB - 5 - {}", sqlite3_errmsg(db)); status = 5; }
rc = sqlite3_exec(db, "DELETE FROM tb0", 0, 0, 0);
if(rc) { Log::Error("DB - 7 - {}", sqlite3_errmsg(db)); status = 7; }
// end transaction
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, NULL);
if(rc) { Log::Error("DB - 5 - {}", sqlite3_errmsg(db)); status = 5; }
// release the used memory
rc = sqlite3_exec(db, "VACUUM", 0, 0, 0);
if(rc) { Log::Error("DB - 8 - {}", sqlite3_errmsg(db)); status = 8; }
}
}
} // end of namespace CppDB
その後、プロジェクトをビルド。作成されたPLCnextライブラリはプロジェクトディレクトリ(C:\Users\eclipse-workspace\CppDB\bin)にあります。
解説
このアプローチではWorkerThreadを使用して書き込み操作を処理します。これはStop()が呼ばれるまで、指定時間(ここでは10 ms)待機しながらスレッドコード実行を繰り返す低優先度スレッドです。スレッド内でデータベースへの新規データ有無をチェックし、データを保存します。
controlポートで異なるデータベース操作をトリガーできます。保存すべきデータはintArrayとfloatArrayポートで提供されます。
シンプルなIECプログラムを作成:
IF iControl = 1 THEN iControl := 0; END_IF;
IF xWrite THEN
arrInt[0] := 0; arrInt[1] := 1; arrInt[2] := 2; arrInt[3] := 3; arrInt[4] := 4;
arrInt[5] := 5; arrInt[6] := 6; arrInt[7] := 7; arrInt[8] := 8; arrInt[9] := 9;
arrReal[0] := 9.0; arrReal[1] := 8.0; arrReal[2] := 7.0; arrReal[3] := 6.0; arrReal[4] := 5.0;
arrReal[5] := 4.0; arrReal[6] := 3.0; arrReal[7] := 2.0; arrReal[8] := 1.0; arrReal[9] := 0.0;
iControl := 1; xWrite := FALSE;
END_IF;
最後にポート接続:
プロジェクトをコンパイルし、接続されたPLCへ送信。ライブモードでiControl変数に異なる値を割り当てることでデータベースと相互作用可能。
データベースdatabase.dbがコントローラーの/opt/plcnextディレクトリに作成されます。WinSCP等のツールでアクセス可能。DB Browser (SQLite)等のツールでデータベース内容確認:
追加情報
SQLite pragmaステートメント
産業技術
- 製造業で「デジタル・チャンピオン」になるための実践ガイド
- データ分析プロジェクトを実成功に導く:計画から導入まで
- ビジネスインテリジェンスの習得:データ駆動型成功のための勝利戦略の策定
- 信頼できるサプライチェーンデータを確保する実証済み戦略
- AIが解決するサプライチェーンの「汚いデータ」問題
- ビッグデータプロジェクト成功を予測する重要要因
- PLCnextとAlibaba Cloud Connectorの連携:ステップバイステップガイド
- データセンター統合:コスト削減、セキュリティ強化、効率向上
- C++データ型完全ガイド:基本型、修飾子、ベストプラクティス
- C++データ抽象化完全マスター:インターフェースと実装の分離
- C++データカプセル化:コードを守る仕組みと実装例