• Skip to main content
  • Skip to header right navigation
  • Skip to site footer
CDP Studio logo

CDP Studio

The no-code and full-code software development tool for distributed control systems and HMI

  • Why CDP
    • Software developers
    • Automation engineers
    • Managers
  • Product
    • Design UI
    • Develop
    • Analyze and test
    • Deploy
    • Framework and toolbox
    • Compatibility
  • Services
  • Use cases
  • Pricing
  • Try CDP

CDP Studio Documentation

  • Framework - CDP Core
  • Application
  • 4.11.14

Application Class

CDP Application main component. Creates and starts all other CDP components, and performs periodic maintenance tasks. More...

Header: #include <CDPSystem/Application/Application.h>
Inherits: CDPComponent
  • List of all members, including inherited members

Public Functions

Application(int argc = 0, char **argv = 0)
virtual ~Application() override
virtual void AddOneShotWorkerFunction(const char *pzProcessName, const std::function<void() > &function)
virtual void AddOneShotWorkerProcess(const char *pzProcessName, CDPComponent *pComp, CDPCOMPONENT_STATEPROCESS pProc)
virtual bool CDPClockSyncEnabled()
void CDPTimeChangedHandler(double secondsChanged, const std::string &oldDateTime, const std::string &newDateTime)
virtual void Configure()
virtual void Create()
virtual void CreateXMLConfigurationDirectory(std::string &src, const std::string &shortName)
char **GetArguments() const
int GetArgumentsCount() const
double GetMemUsedRelative()
void HandleKeyboard()
bool IsConfigurationSaveBlocked()
bool IsStarted(void)
virtual bool IsTimeServer()
void ListComponentThreads()
void ListObjectStates()
void ListObjects(CDPObject *pObject, int level)
virtual bool OSTimeClockSyncEnabled()
virtual unsigned int OStimeInterval()
void RealSuspend()
void ReceiveKey(char key)
virtual void SetClockSyncType(const std::string &newClockSyncType)
void SetDiskAlarm(std::string strAlarmText)
virtual void SetIsTimeServer(bool newIsTimeServer)
virtual void SetOStimeInterval(unsigned int newOStimeInterval)
int Start()
virtual std::string &XMLConfigurationDirectory()

Reimplemented Public Functions

virtual void Destroy() override
virtual void FillNodeChildren(CDP::StudioAPI::NodeStream &stream) const override
virtual unsigned int GetNodeID() const override
virtual CDP::StudioAPI::CDPNodeType GetNodeType() const override
virtual std::string GetProperty(std::string propertyName) override
virtual XMLElementEx *GetXMLElement() override
virtual void SetProperty(std::string propertyName, std::string propertyValue) override
virtual void SetPropertyHandler(CDPPropertyBase *pProp) override
virtual void Suspend() override
virtual bool ValidatePropertyHandler(CDP::StudioAPI::CDPVariantValue &newValue, CDPPropertyBase *property) override
  • 90 public functions inherited from CDPComponent
  • 37 public functions inherited from CDPObject
  • 42 public functions inherited from CDPBaseObject
  • 26 public functions inherited from CDP::StudioAPI::CDPNode
  • 22 public functions inherited from CDP::StudioAPI::ICDPNode

Static Public Members

void AddKeyboardHandler(KeyboardHandlerFunction myKeyboardHandler)
void DeferredNodeDelete(CDP::StudioAPI::CDPNode *property)
void EnableKeyboardHandling(bool bEnable)
std::string GetArgumentValue(const std::string &argument)
Application *GetInstance()
bool GetNotRootEnabled()
std::set<std::string> GetRemoteFullNameByHandle(unsigned int objectHandle)
std::string GetRemoteFullNameByHandleParentCDPObject(unsigned int objectHandle)
bool HasArgument(const std::string &argument)
void PrintCommandLineArgumentList()
void RunInMainThread(std::function<void() > command, double delayInSeconds = 0.0)
bool Running()
void SetGlobalDebugLevel(int value)
void SetNotRootEnabled(bool enable = true)
void SetReturnStatus(int status)
void Stop()
bool Stopped()
  • 2 static public members inherited from CDPComponent
  • 6 static public members inherited from CDPObject
  • 1 static public member inherited from CDPBaseObject

Protected Functions

virtual int MessageSendKeyToKeyboard(void *message)
virtual int MessageSetDiskAlarmAndText(void *message)
virtual void ProcessRunning()
virtual bool TransitionNullToRunning()
virtual void WorkerTask(void)

Reimplemented Protected Functions

virtual void CreateModel() override
virtual int ReceiveMessage(void *message) override
  • 12 protected functions inherited from CDPComponent
  • 13 protected functions inherited from CDPObject
  • 1 protected function inherited from CDP::StudioAPI::CDPNode

Static Protected Members

OSAPITASK WorkerTaskWrapper(LPVOID pClass)
  • 1 static protected member inherited from CDPObject

Additional Inherited Members

  • 31 protected variables inherited from CDPComponent
  • 7 protected variables inherited from CDPObject
  • 9 protected variables inherited from CDPBaseObject

Detailed Description

CDP Application main component. Creates and starts all other CDP components, and performs periodic maintenance tasks.

Usage

Only one instance of Application can exist in the application space. This application instance is created automatically by CDP. The application instance can be found using the Application::GetInstance() method, which returns a pointer to the application instance.

The application is configured from the Application.xml file.

When running with text console display and keyboard, application functions for monitoring and fault finding can be accessed using the keyboard. Press 'h' to get the keyboard menu.

Perodic process

The Application periodic process performs many system maintenance tasks.

  • Calls CDPConnection::UpdateConnections() to update connections.
  • Application CPU load, memory usage and network failures are monitored at slow periodic interval, typically 500 ms - 1 s.
  • Alarms are enabled delayed after startup if 'AlarmEnableDelay' is specified in the Application configuration.

How to Backup a CDP Application

CDP Applications can be backed up or copied by external tools such as xcopy, rsync or other dedicated backup tools. To avoid a corrupt backup, make sure not to write to files that are in the process of being backed up. The Application component has a boolean property DelayConfigurationSave that temporarily prevents writing to the CDP configuration files. The parameter MaxConfigurationSaveDelay defines the maximum number of seconds that DelayConfigurationSave can block the configuration write. We recommend setting DelayConfigurationSave before taking a backup and that you reset DelayConfigurationSave after the backup is complete. For further information, see SR 7.3 – Control system backup for more information about recommended backup guidelines.

Messages

The application re-routes some messages to other system components. Some system messages are handled both by messenger and application.

MessageDescription
CM_STOPStop the controller application.
CM_DEBUGMESSAGESONSwitch on console debug messages.
CM_DEBUGMESSAGESOFFSwitch off console debug messages.
CM_NOTIFYNotify reply from component.
CM_REQUESTHANDLECDPObject/CDPComponent handle request from remote, send notify reply if object/component found.
CM_STATUSReceiving status reply from component.
CM_STATUSUPDATECDPComponent or CDPObject status info.
CM_STATEINFO_MULTICDPComponent multi states info.
CM_DISKALARMTEXTSets the alarm "Disk Alarm" and updates the alarm text.
CM_TEXTCOMMAND"SendKeyToKeyboard" : The specified key/character will be sent to Application. Argument (one character): Key

Application properties

Property NameDescription
InstanceHelpApplication instance help description
HandleApplication handle. Automatically changed if already occupied.
AlarmEnableDelaySet delay time (s) before alarms are enabled on startup.
FlushWritesFlushes data to disk immediately after writes to configuration XML files. This reduces the risk of file corruption after a power loss but can have poor performance some on file systems (e.g. ext3 in "data=ordered" mode).
WebURLDuring runtime this will show the URL with the web address, if 'index.html' is found in Application-folder and WebURL is not already configured in the Application.
WebFilesToServeFolders and files seperated with ';' that CDP web server allows to access. All paths must be relative to application root directory. Dot (.) means allow everything. 'Models' directory is essential for CDP Studio to work properly.
NewCDPTimeWhen this property is changed, the CDP application will adjust to this new time. NewCDPTime must be set in this format: "MM/DD/YY HH:MM:SS.ZZZ" (yy: will be interpreted as year in range [1970..2069], e.g. yy="10" means 2010)
ClockSyncChoose between 'None', 'CDPClockSync' and 'OS_Time'. If 'CDPClockSync' is chosen, CDP ClockSync will be enabled: One or several CDP applications can act as TimeServers. The other CDP applications will adjust it's time, to get the same time as the Server. If 'OS_Time' is chosen, the CDP application will read time from OS (which may be updated by NTP or similar) regularly, and adjust to that time.
IsTimeServerThis application will act as ClockSync Time server if set to 1 (if ClockSync property is set to 'CDPClockSync').
OSTimeIntervalInterval (in seconds) of reading OS time, and possibly adjust CDP time accordingly (if ClockSync property is set to 'OS_Time').
DomainCDP applications with the same Domain can connect and exchange information. By setting the same Domain on several systems, applications from the various systems can interconnect like they do in a single system. By default, all applications in a system have the same Domain. Long Domain names are truncated to 31 characters.
PathThe location of the executable in the filesystem. To protect confidentiality it is advised to use StudioAPI encryption and not route this property to other applications. You can find more information in Security Manual.
DelayConfigurationSaveSet to true to temporarily disable configuration writes. Temporarily disabling configuration write (i.e. changes to be written into application and all its components XML configuration files) is needed to ensure that the Application does not corrupt a file when a backup is in progress. DelayConfigurationSave only postpones writing of the in-memory configuration data to the disk. Note that delaying configuration save does specifically not cause buffering of unwritten data (such as a growing list, for instance) that could lead to an out-of-memory condition; it only delays the writing of the already updated in-memory configuration. See also MaxConfigurationSaveDelay for more information on the delay mechanism.

Message Log

The recommended way to add debug printouts to your application is to use the printf-like CDPMessage function. See also Debugging.

if (error)
  CDPMessage("Error in %s\n", name.c_str());

The following properties can be used to configure CDPMessage functionality.

NameDescription
LogFileNameFile name to store local messages. Leave it empty to create no log file.
LogFileSizeLimitMaximum size of the log file in megabytes, use "0" for no limit. When limit is reached, "-old" suffix is appended to file name and a new file is created.
LogHideEnable to suppress printing CDPMessages to stdout.
LogIncludeTimestampEnable to add timestamp in front of each message.
LogLinesInBufferNumber of lines to keep in memory before wrapping to beginning
LogTimestampDecimalsNumber of decimals in the timestamp to put in front of messages (0..20).
LogURLDuring runtime this will show the URL where message log can be downloaded from.

NetworkInterface

Sets up a network media access controller with the following properties:

NameThe name to identify this networkinterface with.
MACThe MAC-address this network interface is bound to (can be ommited; if so, the network interfaces are assigned in the order they are found)
IPAddressThe IPAddress of this networkinterface
SubnetMaskThe Subnet mask for this network interface.
GatewayThe Gateway for this network interface.

Subcomponents

Contains all the components that this application should start up. Those components' subcomponents and so on will also be started. The subcomponent CDP contains CDP infrastructure components. Required.

Signals

System info signals available for system monitoring.

Signal NameDescription
CPULoadCurrent CPU load [busy/idle, 0..1] on the physical computer that this application is running.
MemTotalTotal amount of memory installed [KiB] on the physical computer that this application is running.
MemUsedTotal amount of memory used [KiB] now on the physical computer that this application is running.
MemUsedRelativeRelative amount of memory [used/total, 0..1] used on the physical computer that this application is running.
MemBuffCacheMemory in buffers and cache [KiB] (only relevant if Linux).

Parameters

The following Parameters can be set in the Application component:

Parameter NameDescription
MaxConfigurationSaveDelayThe maximum amount of time that DelayConfigurationSave can block the configuration writing. If DelayConfigurationSave is active for longer than this time, configuration saving will be allowed again, and the DelayConfigurationSaveAborted alarm will trigger.

Alarms

The Alarms below are available in the Application component. Note that most alarms are hidden by Internal DisplayHint. See

Alarm NameDescription
Disk AlarmThis alarm is set when something is wrong with the disk or files on the disk.
InvalidLicenseOne or more licensed features (libraries) are missing a valid license.
DelayConfigurationSaveAbortedConfiguration save delay period timed out. The configuration saving has been forcibly enabled, even though DelayConfigurationSave is true. Consider increasing MaxConfigurationSaveDelay.

Handling keyboard keypresses

Functions that will be called when a key is pressed on the keyboard can be registered by calling AddKeyboardHandler(). The keyboard handler should return true to indicate that the keypress has been handled and that no further processing of the key is required. As such, some commands could return false even if handled, like the 'h' key, allowing all registered keyboard handlers to print help text about keys handled.

Many keyboard handlers may be registered. The handler registered last is called first, allowing to override the default behaviour.

Usage Define the handler like this:

bool MyKeyboardHandler(char ch)
{
  switch (ch)
  {
    case 'h':
    {
      CDPMessage("Keys supported my MyLib:\n");
      CDPMessage(" b: Print an info message.\n");
      // Special case for 'h': return false to allow all handlers to print help
      return false;
    }
    case 'b':
    {
      CDPMessage("'b' pressed!\n");
      // Normally, return true to indicate theat the key has been handled
      return true;
    }
  }
  return false;
}

Call AddKeyboardHandler() to register the new keyboard handler.

Application::AddKeyboardHandler( MyKeyboardHandler );

Make sure that each handler is registered only once per model, not for every instance created of a component. A good place to do this is in the component's CreateModel(), as long as the function handles keypresses for that specific model only.

Member Function Documentation

Application::Application(int argc = 0, char **argv = 0)

Default constructs an instance of Application.

[override virtual] Application::~Application()

Destroys the instance of Application. The destructor is virtual.

[static] void Application::AddKeyboardHandler(KeyboardHandlerFunction myKeyboardHandler)

Registers a function that will be called when a key is pressed on the keyboard. The keyboard handler returns true to indicate that no further processing of the keyboard event should occur, or false to allow other keyboard-handlers to handle the event. The handler registered last is called first, allowing override of the default behaviour.

[virtual] void Application::AddOneShotWorkerFunction(const char *pzProcessName, const std::function<void() > &function)

[virtual] void Application::AddOneShotWorkerProcess(const char *pzProcessName, CDPComponent *pComp, CDPCOMPONENT_STATEPROCESS pProc)

Adds a component function to be executed once at low priority in worker thread.

The following function adds a (possibly lengthy) process function to be executed to the Application Worker thread execution fifo queue. The function must exit after some time to allow other worker-threads to run. The function takes as input a name and a (CDPCOMPONENT_PROCESS) type (i.e. a void MyComponent::ProcessFunction(void) ) function. The Process function may change the priority of the workerprocess, but it will be reset to the initial low priority after it exits. Exceptions are caught and printed through CDPMessage.

[virtual] bool Application::CDPClockSyncEnabled()

Returns true if CDP ClockSync is enabled (property m_strClockSync has value "CDPClockSync").

void Application::CDPTimeChangedHandler(double secondsChanged, const std::string &oldDateTime, const std::string &newDateTime)

Will generate an extended event if secondsChanged is larger than 1s. This function has been registered into CDPTime::AddGlobalTimeChangedHandler(), and is called if CDPTime has changed, typically by clocksynch.

[virtual] void Application::Configure()

Configures Application. CDP System components are configured first. Then all other components are created and configured.

[virtual] void Application::Create()

Creates Application component and CDP system components

[override virtual protected] void Application::CreateModel()

Reimplemented from CDPBaseObject::CreateModel().

Creates the Application model.

[virtual] void Application::CreateXMLConfigurationDirectory(std::string &src, const std::string &shortName)

Figures out where the subcomponent configuration is, and updates src.

[static] void Application::DeferredNodeDelete(CDP::StudioAPI::CDPNode *property)

[override virtual] void Application::Destroy()

Reimplemented from CDPBaseObject::Destroy().

Suspends and destroys the application and all subcomponents.

[static] void Application::EnableKeyboardHandling(bool bEnable)

Set if keyboard handling is enabled or not.

[override virtual] void Application::FillNodeChildren(CDP::StudioAPI::NodeStream &stream) const

Reimplemented from CDPNode::FillNodeChildren().

Fills the nodestream with information. Calls baseclass implementation.

[static] std::string Application::GetArgumentValue(const std::string &argument)

Returns the corresponding value for argument (e.g. my-argument my_value)

char **Application::GetArguments() const

Returns argument pointer this application was invoked with

int Application::GetArgumentsCount() const

Returns number of arguments this application was invoked with

[static] Application *Application::GetInstance()

Returns the only Application instance

double Application::GetMemUsedRelative()

Returns memory used by the system, relative to available memory. Relative memory used will be returned in range [0.0 .. 1.0].

[override virtual] unsigned int Application::GetNodeID() const

Reimplemented from ICDPNode::GetNodeID().

Returns the application handle

[override virtual] CDP::StudioAPI::CDPNodeType Application::GetNodeType() const

Reimplemented from ICDPNode::GetNodeType().

Returns CDPNode type

[static] bool Application::GetNotRootEnabled()

Returns true if executing CDP as non root is enabled.

[override virtual] std::string Application::GetProperty(std::string propertyName)

Reimplemented from CDPBaseObject::GetProperty().

Returns the property value for a named property. May return a base property if propertyName is not found in this instance. Supports "GlobalTime"

[static] std::set<std::string> Application::GetRemoteFullNameByHandle(unsigned int objectHandle)

Returns a set of connection-names (remote fullnames), if found in CDPConnection's connectionsByHandle. Usually, only one connection is found for one handle, but if there are several connections having CDPNodes at the end of the remote names, after the same CDPComponent/CDPObject, there will be several connections for one handle. E.g.if there are two connections named 'App.CDPComponent.CDPNode1' and 'App.CDPComponent.CDPNode2', both will have same handle. Both these names will be returned by this function.

See also GetRemoteFullNameByHandleParentCDPObject().

[static] std::string Application::GetRemoteFullNameByHandleParentCDPObject(unsigned int objectHandle)

Returns remote fullname to nearest CDPObject identified by name of the connection, if connected and found in CDPConnection connectionsByHandle.

If e.g. the name of the connection is 'App.CDPComponent.CDPObject', this function will return 'App.CDPComponent.CDPObject'.

If e.g. the name of the connection is 'App.CDPComponent', this function will return 'App.CDPComponent'.

If e.g. there are several connections for same handle, like 'App.CDPComponent.CDPObject.CDPNode1.CDPNode2'and 'App.CDPComponent.CDPObject.CDPNode1.CDPNode3', this function will return 'App.CDPComponent.CDPObject'.

See also GetRemoteFullNameByHandle().

[override virtual] XMLElementEx *Application::GetXMLElement()

Reimplemented from CDPObject::GetXMLElement().

Get the XMLElement pointing to <Application>

void Application::HandleKeyboard()

Checks keyboard input and calls all registered keyboard handlers.

[static] bool Application::HasArgument(const std::string &argument)

Returns true if argument is found in the application argument list.

bool Application::IsConfigurationSaveBlocked()

Returns true if DelayConfigurationSave is set and time spent delaying save is less than MaxConfigurationSaveDelay. If not it returns false. If DelayConfigurationSave is set and time spent delaying save is equal or more than MaxConfigurationSaveDelay, the DelayConfigurationSaveAborted alarm is set.

bool Application::IsStarted(void)

Returns true when Application::Configure() is finished, meaning all components are configured and ready to start.

[virtual] bool Application::IsTimeServer()

Returns true if this Application shall act as a Time server for CDPClockSync. Only relevant if clocksynch is "CDPClockSync".

void Application::ListComponentThreads()

Prints all thread id's in this application space to CDPMessages console.

void Application::ListObjectStates()

Prints all object states in this application space to CDPMessages console.

void Application::ListObjects(CDPObject *pObject, int level)

Prints all objects in this application space to CDPMessages console.

[virtual protected] int Application::MessageSendKeyToKeyboard(void *message)

Messagehandler that forwards specified key/char to Keyboard handler. message must be a MessageTextCommandWithParameterSend-pointer. Character found in MessageTextCommandWithParameterSend's parameters[0] will be used as argument when calling ReceiveKey().

[virtual protected] int Application::MessageSetDiskAlarmAndText(void *message)

Messagehandler for the alarm "Disk Alarm". message must be a MessageDiskAlarmText-pointer. The alarm will be Set with updated alarm-text.

[virtual] bool Application::OSTimeClockSyncEnabled()

Returns true if OS Time ClockSync is enabled (property m_strClockSync has value "OS_Time").

[virtual] unsigned int Application::OStimeInterval()

Returns value of Interval (in s) of reading OS time. Only relevant if clocksynch is "OS_Time".

[static] void Application::PrintCommandLineArgumentList()

Prints supported command line arguments

[virtual protected] void Application::ProcessRunning()

Runs periodic state process. Updates CDPConnections, handles clock sync and updates CPU info etc.

void Application::RealSuspend()

Stops the application by suspending all components and stopping cdp infrastructure

void Application::ReceiveKey(char key)

Inserts key into keyboard input buffer.

[override virtual protected] int Application::ReceiveMessage(void *message)

Reimplemented from CDPBaseObject::ReceiveMessage().

Processes application messages. Will call CDPComponent::ReceiveMessage() if not handled here.

[static] void Application::RunInMainThread(std::function<void() > command, double delayInSeconds = 0.0)

Executes a callback command in the thread were `int main()` is defined.

Usually this is the only thread were functions are allowed to access GUI components. If optional parameter delayInSeconds is set, the command is run after specified delay.

Application::RunInMainThread([=] () { m_label.setText("New text"); }, 0.5);

[static] bool Application::Running()

Returns true after Start is called and before RealSuspend is executed

[virtual] void Application::SetClockSyncType(const std::string &newClockSyncType)

Specifies which clocksync to use. The property m_strClockSync gets value "CDPClockSync", "OS_Time" or "None".

void Application::SetDiskAlarm(std::string strAlarmText)

Sets the Disk Alarm in the application in a thread-safe manner. The strAlarmText should include information such as filename and type of error (read/write/seek etc).

[static] void Application::SetGlobalDebugLevel(int value)

Set the global debugEx variable (also adjusted by keyboard input 'Z' and 'z')

[virtual] void Application::SetIsTimeServer(bool newIsTimeServer)

Sets if this Application shall act as a Time server or not. Only relevant if clocksynch is "CDPClockSync". Default value is false.

[static] void Application::SetNotRootEnabled(bool enable = true)

Sets value of flag enabling executing CDP as non root (not real time) or not.

[virtual] void Application::SetOStimeInterval(unsigned int newOStimeInterval)

Sets new value of Interval (in s) of reading OS time. Only relevant if clocksynch is "OS_Time".

[override virtual] void Application::SetProperty(std::string propertyName, std::string propertyValue)

Reimplemented from CDPBaseObject::SetProperty().

Sets a property in the object to a specified value. Called by MessageSetProperty after property name and value has been figured out. If propery is not found, CDPComponent::SetProperty() is called. Supported Properties: "ApplicationHandle".

[override virtual] void Application::SetPropertyHandler(CDPPropertyBase *pProp)

Reimplemented from CDPObject::SetPropertyHandler().

Catches property 'Set' events

[static] void Application::SetReturnStatus(int status)

Set the application exit status.

int Application::Start()

Starts system components. Will not return before application is stopped. Returns application exit status.

[static] void Application::Stop()

Stops execution of all components and system services in this application space.

[static] bool Application::Stopped()

Returns true when RealSuspend has started or finished executing

[override virtual] void Application::Suspend()

Reimplemented from CDPBaseObject::Suspend().

Suspends the application and all other components.

[virtual protected] bool Application::TransitionNullToRunning()

Changes to Running state if requestedState == 'Running'

[override virtual] bool Application::ValidatePropertyHandler(CDP::StudioAPI::CDPVariantValue &newValue, CDPPropertyBase *property)

Reimplemented from CDPObject::ValidatePropertyHandler().

Validates properties before they are set to a new value. Return true to allow change, or false to disallow.

[virtual protected] void Application::WorkerTask(void)

This is the he WorkerTask which processes list of worker processes, and executes them in sequence. Restores initial Process priority after each run.

[static protected] OSAPITASK Application::WorkerTaskWrapper(LPVOID pClass)

Starts the worker task

[virtual] std::string &Application::XMLConfigurationDirectory()

Returns the XML configuration directory where subcomponent configuration is. If directory is not set, this will call CreateXMLConfigurationDirectory().

The content of this document is confidential information not to be published without the consent of CDP Technologies AS.

CDP Technologies AS, www.cdpstudio.com

Get started with CDP Studio today

Let us help you take your great ideas and turn them into the products your customer will love.

Try CDP Studio for free
Why CDP Studio?

CDP Technologies AS
Hundsværgata 8,
P.O. Box 144
6001 Ålesund, Norway

Tel: +47 990 80 900
E-mail: info@cdptech.com

Company

About CDP

Contact us

Services

Partners

Blog

Developers

Get started

User manuals

Support

Document download

Release notes

Follow CDP

  • LinkedIn
  • YouTube
  • GitHub

    © Copyright 2022 CDP Technologies. Privacy and cookie policy.

    Return to top