Almacenamiento de Datos de Proceso con SQLite en Proyectos C++ de PLCnext
Este artículo describe cómo el motor de base de datos SQLite ya instalado en Controladores PLCnext podría usarse para almacenar datos proporcionados a través del Espacio Global de Datos (GDS). La base de datos permite el almacenamiento de datos de proceso de forma estandarizada y podría exportarse a otros sistemas con SFTP.
Por favor asegúrate de que la versión de la herramienta plcncli coincida con la versión de firmware de tu controlador.
Crear un Proyecto C++ en Eclipse
Crea un nuevo proyecto C++ en Eclipse, siguiendo las instrucciones del PLCnext Info Center con las siguientes propiedades:
- nombre del proyecto: CppDB
- nombre del componente: DBComponent
- nombre del programa: DBProgram
- namespace del proyecto: CppDB
Otros nombres también estarían bien, sin embargo, un nombre común simplifica el tutorial.
Crea una nueva carpeta (misma jerarquía que la carpeta src) en el proyecto y nómbrala 'cmake'. Dentro de la carpeta crea un archivo y nómbralo 'FindSqlite.cmake' e inserta el siguiente contenido en él.
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()
Reemplaza el contenido de los archivos DBComponent.cpp y DBComponent.hpp con lo siguiente:
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 <sqlite3.h>
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<DBComponent> {
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 en PLCnext Engineer
//#port
//#attributes(Input)
float32 floatArray[10] {}; // REAL en 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
Posteriormente, construye el proyecto. La librería PLCnext creada puede encontrarse en el directorio del proyecto (C:\Users\eclipse-workspace\CppDB\bin).
Explicación
En este enfoque se usa un WorkerThread para manejar la operación de escritura. Este es un hilo de baja prioridad que repite la ejecución del código enhebrado hasta que Stop() es llamado. En el hilo verificaremos si hay nuevos datos para la base de datos disponibles y almacenaremos los datos. Después de la ejecución, el WorkerThread espera un tiempo especificado (aquí: 10 ms).
Con la ayuda del puerto 'control', podemos disparar diferentes operaciones de base de datos. Los datos que deben almacenarse se proporcionan con los puertos 'intArray' y 'floatArray'.
Creemos un programa IEC simple:
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;
Finalmente, tenemos que conectar los puertos:
Ahora, podemos compilar el proyecto y enviarlo a un PLC conectado. En el 'modo en vivo', podemos interactuar con la base de datos, asignando diferentes valores a la variable 'iControl'.
Una base de datos 'database.db' se crea en el directorio del controlador /opt/plcnext. Podemos acceder a ella con herramientas como WinSCP. Podemos verificar el contenido de la base de datos con la herramienta DB Browser (SQLite):
Información Adicional
Sentencias pragma de SQLite
Tecnología Industrial
- Cómo convertirse en un campeón digital en la fabricación: Guía práctica
- Convertir proyectos de analítica de datos en éxito real: De la planificación al despliegue
- Dominar la inteligencia de negocio: Crear una estrategia ganadora para el éxito basado en datos
- Garantizando datos confiables en la cadena de suministro: Una estrategia probada
- Cómo la IA resuelve los desafíos de datos "sucios" en las cadenas de suministro
- Factores clave que predicen el éxito de proyectos Big Data
- Integrar Alibaba Cloud Connector con PLCnext: Guía paso a paso
- Consolidación Data Center: Reduciendo Costes, Mejorando Seguridad y Eficiencia
- Entendiendo los Tipos de Datos en C++: Fundamentos, Modificadores y Mejores Prácticas
- Dominando la Abstracción de Datos en C++: Simplifica el Diseño con Separación Interfaz-Implementación
- Encapsulamiento de Datos en C++: Protege tu Código