工業製造
産業用モノのインターネット | 工業材料 | 機器のメンテナンスと修理 | 産業プログラミング |
home  MfgRobots >> 工業製造 >  >> Manufacturing Technology >> 産業技術

PLCnext C++プロジェクトでSQLiteを用いたプロセスデータ保存

この記事では、PLCnextコントローラーにプリインストールされているSQLiteデータベースエンジンを用いて、グローバルデータスペース(GDS)経由で提供されるデータを保存する方法を説明します。データベースによりプロセスデータを標準化された形で保存でき、SFTPで他システムへエクスポート可能です。

plcncliツールバージョンがコントローラーファームウェアバージョンと一致することを確認してください。

Eclipse C++プロジェクト作成

PLCnext Info Centerの手順に従い、以下プロパティでEclipse新規C++プロジェクト作成:

他の名前でも構いませんが、共通名だとチュートリアルが簡単になります。

プロジェクトに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ポートで提供されます。

PLCnext C++プロジェクトでSQLiteを用いたプロセスデータ保存

シンプルな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;
PLCnext C++プロジェクトでSQLiteを用いたプロセスデータ保存

最後にポート接続:

PLCnext C++プロジェクトでSQLiteを用いたプロセスデータ保存

プロジェクトをコンパイルし、接続されたPLCへ送信。ライブモードでiControl変数に異なる値を割り当てることでデータベースと相互作用可能。

データベースdatabase.dbがコントローラーの/opt/plcnextディレクトリに作成されます。WinSCP等のツールでアクセス可能。DB Browser (SQLite)等のツールでデータベース内容確認:

PLCnext C++プロジェクトでSQLiteを用いたプロセスデータ保存

追加情報

SQLite pragmaステートメント

産業技術

  1. 製造業で「デジタル・チャンピオン」になるための実践ガイド
  2. データ分析プロジェクトを実成功に導く:計画から導入まで
  3. ビジネスインテリジェンスの習得:データ駆動型成功のための勝利戦略の策定
  4. 信頼できるサプライチェーンデータを確保する実証済み戦略
  5. AIが解決するサプライチェーンの「汚いデータ」問題
  6. ビッグデータプロジェクト成功を予測する重要要因
  7. PLCnextとAlibaba Cloud Connectorの連携:ステップバイステップガイド
  8. データセンター統合:コスト削減、セキュリティ強化、効率向上
  9. C++データ型完全ガイド:基本型、修飾子、ベストプラクティス
  10. C++データ抽象化完全マスター:インターフェースと実装の分離
  11. C++データカプセル化:コードを守る仕組みと実装例