address_space_internal.cpp
Go to the documentation of this file.
00001 
00002 
00003 
00004 
00005 
00006 
00007 
00008 
00009 
00010 
00011 #include "address_space_internal.h"
00012 
00013 
00014 namespace OpcUa
00015 {
00016   namespace Internal
00017   {
00018     typedef std::map <IntegerId, std::shared_ptr<InternalSubscription>> SubscriptionsIdMap; // Map SubscptioinId, SubscriptionData
00019 
00020     //store subscription for one attribute
00021     struct AttSubscription
00022     {
00023       IntegerId SubscriptionId;
00024       IntegerId MonitoredItemId;
00025       MonitoringParameters Parameters;
00026     };
00027 
00028     AddressSpaceInMemory::AddressSpaceInMemory(bool debug)
00029         : Debug(debug)
00030         , DataChangeCallbackHandle(0)
00031     {
00032       /*
00033       ObjectAttributes attrs;
00034       attrs.Description = LocalizedText(OpcUa::Names::Root);
00035       attrs.DisplayName = LocalizedText(OpcUa::Names::Root);
00036 
00037       AddNodesItem rootNode;
00038       rootNode.BrowseName = QualifiedName(0, OpcUa::Names::Root);
00039       rootNode.Class = NodeClass::Object;
00040       rootNode.RequestedNewNodeId = ObjectId::RootFolder;
00041       rootNode.TypeDefinition = ObjectId::FolderType;
00042       rootNode.Attributes = attrs;
00043       AddNode(rootNode);
00044       */
00045     }
00046 
00047     AddressSpaceInMemory::~AddressSpaceInMemory()
00048     {
00049     }
00050 
00051     std::vector<AddNodesResult> AddressSpaceInMemory::AddNodes(const std::vector<AddNodesItem>& items)
00052     {
00053       boost::unique_lock<boost::shared_mutex> lock(DbMutex);
00054 
00055       std::vector<AddNodesResult> results;
00056       for (const AddNodesItem& item: items)
00057       {
00058         results.push_back(AddNode(item));
00059       }
00060       return results;
00061     }
00062 
00063     std::vector<StatusCode> AddressSpaceInMemory::AddReferences(const std::vector<AddReferencesItem>& items)
00064     {
00065       boost::unique_lock<boost::shared_mutex> lock(DbMutex);
00066 
00067       std::vector<StatusCode> results;
00068       for (const auto& item : items)
00069       {
00070         results.push_back(AddReference(item));
00071       }
00072       return results;
00073     }
00074 
00075     std::vector<BrowsePathResult> AddressSpaceInMemory::TranslateBrowsePathsToNodeIds(const TranslateBrowsePathsParameters& params) const
00076     {
00077       boost::shared_lock<boost::shared_mutex> lock(DbMutex);
00078 
00079       std::vector<BrowsePathResult> results;
00080       for (BrowsePath browsepath : params.BrowsePaths )
00081       {
00082         BrowsePathResult result = TranslateBrowsePath(browsepath);
00083         results.push_back(result);
00084       }
00085       return results;
00086     }
00087 
00088     std::vector<BrowseResult> AddressSpaceInMemory::Browse(const OpcUa::NodesQuery& query) const
00089     {
00090       boost::shared_lock<boost::shared_mutex> lock(DbMutex);
00091 
00092       if (Debug) std::cout << "AddressSpaceInternal | Browsing." << std::endl;
00093       std::vector<BrowseResult> results;
00094       for ( BrowseDescription browseDescription: query.NodesToBrowse)
00095       {
00096         BrowseResult result;
00097         if(Debug)
00098         {
00099           std::cout << "AddressSpaceInternal | Browsing ";
00100           std::cout << " NodeId: '" << browseDescription.NodeToBrowse << "'";
00101           std::cout << ", ReferenceId: '" << browseDescription.ReferenceTypeId << "'";
00102           std::cout << ", Direction: " << browseDescription.Direction;
00103           std::cout << ", NodeClasses: 0x" << std::hex << (unsigned)browseDescription.NodeClasses;
00104           std::cout << ", ResultMask: '0x" << std::hex << (unsigned)browseDescription.ResultMask << std::endl;
00105         }
00106 
00107         NodesMap::const_iterator node_it = Nodes.find(browseDescription.NodeToBrowse);
00108         if ( node_it == Nodes.end() )
00109         {
00110           if (Debug) std::cout << "AddressSpaceInternal | Node '" << OpcUa::ToString(browseDescription.NodeToBrowse) << "' not found in the address space." << std::endl;
00111           continue;
00112         }
00113 
00114         std::copy_if(node_it->second.References.begin(), node_it->second.References.end(), std::back_inserter(result.Referencies),
00115             std::bind(&AddressSpaceInMemory::IsSuitableReference, this, std::cref(browseDescription), std::placeholders::_1)
00116         );
00117         results.push_back(result);
00118       }
00119       return results;
00120     }
00121 
00122     std::vector<BrowseResult> AddressSpaceInMemory::BrowseNext() const
00123     {
00124       boost::shared_lock<boost::shared_mutex> lock(DbMutex);
00125 
00126       return std::vector<BrowseResult>();
00127     }
00128 
00129         std::vector<NodeId> AddressSpaceInMemory::RegisterNodes(const std::vector<NodeId>& params) const
00130         {
00131                 boost::shared_lock<boost::shared_mutex> lock(DbMutex);
00132 
00133                 return params;
00134         }
00135 
00136         void AddressSpaceInMemory::UnregisterNodes(const std::vector<NodeId>& params) const
00137         {
00138                 boost::shared_lock<boost::shared_mutex> lock(DbMutex);
00139 
00140                 return;
00141         }
00142 
00143     std::vector<DataValue> AddressSpaceInMemory::Read(const ReadParameters& params) const
00144     {
00145       boost::shared_lock<boost::shared_mutex> lock(DbMutex);
00146 
00147       std::vector<DataValue> values;
00148       for (const ReadValueId& attribute : params.AttributesToRead)
00149       {
00150         values.push_back(GetValue(attribute.NodeId, attribute.AttributeId));
00151       }
00152       return values;
00153     }
00154 
00155     std::vector<StatusCode> AddressSpaceInMemory::Write(const std::vector<OpcUa::WriteValue>& values)
00156     {
00157       boost::unique_lock<boost::shared_mutex> lock(DbMutex);
00158 
00159       std::vector<StatusCode> statuses;
00160       for (WriteValue value : values)
00161       {
00162         if (value.Value.Encoding & DATA_VALUE)
00163         {
00164           statuses.push_back(SetValue(value.NodeId, value.AttributeId, value.Value));
00165           continue;
00166         }
00167         statuses.push_back(StatusCode::BadNotWritable);
00168       }
00169       return statuses;
00170     }
00171 
00172     std::tuple<bool, NodeId> AddressSpaceInMemory::FindElementInNode(const NodeId& nodeid, const RelativePathElement& element) const
00173     {
00174       NodesMap::const_iterator nodeit = Nodes.find(nodeid);
00175       if ( nodeit != Nodes.end() )
00176       {
00177         for (auto reference : nodeit->second.References)
00178         {
00179           //if (reference.first == current) { std::cout <<   reference.second.BrowseName.NamespaceIndex << reference.second.BrowseName.Name << " to " << element.TargetName.NamespaceIndex << element.TargetName.Name <<std::endl; }
00180           if (reference.BrowseName == element.TargetName)
00181           {
00182             return std::make_tuple(true, reference.TargetNodeId);
00183           }
00184         }
00185       }
00186       return std::make_tuple(false, NodeId());
00187     }
00188 
00189     BrowsePathResult AddressSpaceInMemory::TranslateBrowsePath(const BrowsePath& browsepath) const
00190     {
00191       NodeId current = browsepath.StartingNode;
00192       BrowsePathResult result;
00193 
00194       for (RelativePathElement element : browsepath.Path.Elements)
00195       {
00196         auto res = FindElementInNode(current, element);
00197         if ( std::get<0>(res) == false )
00198         {
00199           result.Status = OpcUa::StatusCode::BadNoMatch;
00200           return result;
00201         }
00202         current = std::get<1>(res);
00203       }
00204 
00205       result.Status = OpcUa::StatusCode::Good;
00206       std::vector<BrowsePathTarget> targets;
00207       BrowsePathTarget target;
00208       target.Node = current;
00209       target.RemainingPathIndex = std::numeric_limits<uint32_t>::max();
00210       targets.push_back(target);
00211       result.Targets = targets;
00212       return result;
00213     }
00214 
00215     DataValue AddressSpaceInMemory::GetValue(const NodeId& node, AttributeId attribute) const
00216     {
00217       NodesMap::const_iterator nodeit = Nodes.find(node);
00218       if ( nodeit == Nodes.end() )
00219       {
00220         if (Debug) std::cout << "AddressSpaceInternal | Bad node not found: " << node << std::endl;
00221       }
00222       else
00223       {
00224         AttributesMap::const_iterator attrit = nodeit->second.Attributes.find(attribute);
00225         if ( attrit == nodeit->second.Attributes.end() )
00226         {
00227           if (Debug) std::cout << "AddressSpaceInternal | node " << node << " has not attribute: " << (uint32_t)attribute << std::endl;
00228         }
00229         else
00230         {
00231           if ( attrit->second.GetValueCallback )
00232           {
00233             if (Debug) std::cout << "AddressSpaceInternal | A callback is set for this value, calling callback" << std::endl;
00234             return attrit->second.GetValueCallback();
00235           }
00236           if (Debug) std::cout << "AddressSpaceInternal | No callback is set for this value returning stored value" << std::endl;
00237           return attrit->second.Value;
00238         }
00239       }
00240       DataValue value;
00241       value.Encoding = DATA_VALUE_STATUS_CODE;
00242       value.Status = StatusCode::BadNotReadable;
00243       return value;
00244     }
00245 
00246     uint32_t AddressSpaceInMemory::AddDataChangeCallback(const NodeId& node, AttributeId attribute, std::function<Server::DataChangeCallback> callback)
00247     {
00248       if (Debug) std::cout << "AddressSpaceInternal| Set data changes callback for node " << node
00249          << " and attribute " << (unsigned)attribute <<  std::endl;
00250       NodesMap::iterator it = Nodes.find(node);
00251       if ( it == Nodes.end() )
00252       {
00253         if (Debug) std::cout << "AddressSpaceInternal| Node '" << node << "' not found." << std::endl;
00254         throw std::runtime_error("AddressSpaceInternal | NodeId not found");
00255       }
00256       AttributesMap::iterator ait = it->second.Attributes.find(attribute);
00257       if ( ait == it->second.Attributes.end() )
00258       {
00259         if (Debug) std::cout << "address_space| Attribute " << (unsigned)attribute << " of node '" << node << "' not found." << std::endl;
00260         throw std::runtime_error("Attribute not found");
00261       }
00262 
00263       uint32_t handle = ++DataChangeCallbackHandle;
00264       DataChangeCallbackData data;
00265       data.Callback = callback;
00266       ait->second.DataChangeCallbacks[handle] = data;
00267       ClientIdToAttributeMap[handle] = NodeAttribute(node, attribute);
00268       return handle;
00269     }
00270 
00271     void AddressSpaceInMemory::DeleteDataChangeCallback(uint32_t serverhandle )
00272     {
00273       if (Debug) std::cout << "AddressSpaceInternal | Deleting callback with client id. " << serverhandle << std::endl;
00274 
00275       ClientIdToAttributeMapType::iterator it = ClientIdToAttributeMap.find(serverhandle);
00276       if ( it == ClientIdToAttributeMap.end() )
00277       {
00278         std::cout << "AddressSpaceInternal | Error, request to delete a callback using unknown handle: " << serverhandle << std::endl;
00279         return;
00280       }
00281 
00282       NodesMap::iterator nodeit = Nodes.find(it->second.Node);
00283       if ( nodeit != Nodes.end() )
00284       {
00285         AttributesMap::iterator ait = nodeit->second.Attributes.find(it->second.Attribute);
00286         if ( ait != nodeit->second.Attributes.end() )
00287         {
00288           size_t nb = ait->second.DataChangeCallbacks.erase(serverhandle);
00289           if (Debug) std::cout << "AddressSpaceInternal | deleted " << nb << " callbacks" << std::endl;
00290           ClientIdToAttributeMap.erase(serverhandle);
00291           return;
00292         }
00293       }
00294       throw std::runtime_error("AddressSpaceInternal | NodeId or attribute nor found");
00295     }
00296 
00297     StatusCode AddressSpaceInMemory::SetValueCallback(const NodeId& node, AttributeId attribute, std::function<DataValue(void)> callback)
00298     {
00299       NodesMap::iterator it = Nodes.find(node);
00300       if ( it != Nodes.end() )
00301       {
00302         AttributesMap::iterator ait = it->second.Attributes.find(attribute);
00303         if ( ait != it->second.Attributes.end() )
00304         {
00305           ait->second.GetValueCallback = callback;
00306           return StatusCode::Good;
00307         }
00308       }
00309       return StatusCode::BadAttributeIdInvalid;
00310     }
00311 
00312     void AddressSpaceInMemory::SetMethod(const NodeId& node, std::function<std::vector<OpcUa::Variant> (std::vector<OpcUa::Variant> arguments)> callback)
00313     {
00314       NodesMap::iterator it = Nodes.find(node);
00315       if ( it != Nodes.end() )
00316       {
00317         it->second.Method = callback;
00318       }
00319       throw std::runtime_error("While setting node callback: node does not exist.");
00320     }
00321 
00322     std::vector<OpcUa::CallMethodResult> AddressSpaceInMemory::Call(std::vector<OpcUa::CallMethodRequest> methodsToCall)
00323     {
00324       std::vector<OpcUa::CallMethodResult>  results;
00325       for (auto method : methodsToCall)
00326       {
00327         results.push_back(CallMethod(method));
00328       }
00329       return results;
00330     }
00331 
00332     CallMethodResult AddressSpaceInMemory::CallMethod(CallMethodRequest request)
00333     {
00334       boost::shared_lock<boost::shared_mutex> lock(DbMutex);
00335 
00336       CallMethodResult result;
00337       NodesMap::iterator node_it = Nodes.find(request.ObjectId);
00338       if ( node_it == Nodes.end() )
00339       {
00340         result.Status = StatusCode::BadNodeIdUnknown;
00341         return result;
00342       }
00343       NodesMap::iterator method_it = Nodes.find(request.MethodId);
00344       if ( method_it == Nodes.end() )
00345       {
00346         result.Status = StatusCode::BadNodeIdUnknown;
00347         return result;
00348       }
00349       if ( ! method_it->second.Method )
00350       {
00351         result.Status = StatusCode::BadNothingToDo;
00352         return result;
00353       }
00354       //FIXME: find a way to return more information about failure to client
00355       try
00356       {
00357         result.OutputArguments = method_it->second.Method(request.InputArguments);
00358       }
00359       catch (std::exception& ex)
00360       {
00361         std::cout << "Exception whil calling method" << request.MethodId << ":  " << ex.what() << std::endl;
00362         result.Status = StatusCode::BadUnexpectedError;
00363         return result;
00364       }
00365       for (auto var : request.InputArguments)
00366       {
00367         result.InputArgumentResults.push_back(StatusCode::Good);
00368       }
00369       return result;
00370     }
00371 
00372     StatusCode AddressSpaceInMemory::SetValue(const NodeId& node, AttributeId attribute, const DataValue& data)
00373     {
00374       NodesMap::iterator it = Nodes.find(node);
00375       if ( it != Nodes.end() )
00376       {
00377         AttributesMap::iterator ait = it->second.Attributes.find(attribute);
00378         if ( ait != it->second.Attributes.end() )
00379         {
00380           DataValue value(data);
00381           value.SetServerTimestamp(DateTime::Current());
00382           ait->second.Value = value;
00383           //call registered callback
00384           for (auto pair : ait->second.DataChangeCallbacks)
00385           {
00386             pair.second.Callback(it->first, ait->first, ait->second.Value);
00387           }
00388           return StatusCode::Good;
00389         }
00390       }
00391       return StatusCode::BadAttributeIdInvalid;
00392     }
00393 
00394     bool AddressSpaceInMemory::IsSuitableReference(const BrowseDescription& desc, const ReferenceDescription& reference) const
00395     {
00396       if (Debug) std::cout << "AddressSpaceInternal | Checking reference '" << reference.ReferenceTypeId << "' to the node '" << reference.TargetNodeId << "' (" << reference.BrowseName << ") which must fit ref: " << desc.ReferenceTypeId << " with include subtype: " << desc.IncludeSubtypes << std::endl;
00397 
00398       if ((desc.Direction == BrowseDirection::Forward && !reference.IsForward) || (desc.Direction == BrowseDirection::Inverse && reference.IsForward))
00399       {
00400         if (Debug) std::cout << "AddressSpaceInternal | Reference in different direction." << std::endl;
00401         return false;
00402       }
00403       if (desc.ReferenceTypeId != ObjectId::Null && !IsSuitableReferenceType(reference, desc.ReferenceTypeId, desc.IncludeSubtypes))
00404       {
00405         if (Debug) std::cout << "AddressSpaceInternal | Reference has wrong type." << std::endl;
00406         return false;
00407       }
00408       if (desc.NodeClasses != NodeClass::Unspecified && (desc.NodeClasses & reference.TargetNodeClass) == NodeClass::Unspecified)
00409       {
00410         if (Debug) std::cout << "AddressSpaceInternal | Reference has wrong class." << std::endl;
00411         return false;
00412       }
00413       if (Debug) std::cout << "AddressSpaceInternal | Reference suitable." << std::endl;
00414       return true;
00415     }
00416 
00417     bool AddressSpaceInMemory::IsSuitableReferenceType(const ReferenceDescription& reference, const NodeId& typeId, bool includeSubtypes) const
00418     {
00419       if (!includeSubtypes)
00420       {
00421         return reference.ReferenceTypeId == typeId;
00422       }
00423       const std::vector<NodeId> suitableTypes = SelectNodesHierarchy(std::vector<NodeId>(1, typeId));
00424       const auto resultIt = std::find(suitableTypes.begin(), suitableTypes.end(), reference.ReferenceTypeId);\
00425       return resultIt != suitableTypes.end();
00426     }
00427 
00428     std::vector<NodeId> AddressSpaceInMemory::SelectNodesHierarchy(std::vector<NodeId> sourceNodes) const
00429     {
00430       std::vector<NodeId> subNodes;
00431       for ( NodeId nodeid: sourceNodes )
00432       {
00433           NodesMap::const_iterator node_it = Nodes.find(nodeid);
00434           if ( node_it != Nodes.end() )
00435           {
00436             for (auto& ref:  node_it->second.References )
00437             {
00438               subNodes.push_back(ref.TargetNodeId);
00439           }
00440         }
00441       }
00442       if (subNodes.empty())
00443       {
00444         return sourceNodes;
00445       }
00446 
00447       const std::vector<NodeId> allChilds = SelectNodesHierarchy(subNodes);
00448       sourceNodes.insert(sourceNodes.end(), allChilds.begin(), allChilds.end());
00449       return sourceNodes;
00450     }
00451 
00452     AddNodesResult AddressSpaceInMemory::AddNode( const AddNodesItem& item )
00453     {
00454       AddNodesResult result;
00455       if (Debug) std::cout << "AddressSpaceInternal | address_space| Adding new node id='" << item.RequestedNewNodeId << "' name=" << item.BrowseName.Name << std::endl;
00456 
00457       const NodeId resultId = GetNewNodeId(item.RequestedNewNodeId);
00458 
00459       if (!Nodes.empty() && resultId != ObjectId::Null && Nodes.find(resultId) != Nodes.end())
00460       {
00461         std::cerr << "AddressSpaceInternal | Error: NodeId '"<< resultId << "' allready exist: " << std::endl;
00462         result.Status = StatusCode::BadNodeIdExists;
00463         return result;
00464       }
00465 
00466       NodesMap::iterator parent_node_it = Nodes.end();
00467       if (item.ParentNodeId != NodeId())
00468       {
00469         parent_node_it = Nodes.find(item.ParentNodeId);
00470         if ( parent_node_it == Nodes.end() )
00471         {
00472           if (Debug) std::cout << "AddressSpaceInternal | Error: Parent node '"<< item.ParentNodeId << "'does not exist" << std::endl;
00473           result.Status = StatusCode::BadParentNodeIdInvalid;
00474           return result;
00475         }
00476       }
00477 
00478       NodeStruct nodestruct;
00479       //Add Common attributes
00480       nodestruct.Attributes[AttributeId::NodeId].Value = resultId;
00481       nodestruct.Attributes[AttributeId::BrowseName].Value = item.BrowseName;
00482       nodestruct.Attributes[AttributeId::NodeClass].Value = static_cast<int32_t>(item.Class);
00483 
00484       // Add requested attributes
00485       for (const auto& attr: item.Attributes.Attributes)
00486       {
00487         AttributeValue attval;
00488         attval.Value = attr.second;
00489 
00490         nodestruct.Attributes.insert(std::make_pair(attr.first, attval));
00491       }
00492 
00493       Nodes.insert(std::make_pair(resultId, nodestruct));
00494 
00495       if (parent_node_it != Nodes.end())
00496       {
00497         // Link to parent
00498         ReferenceDescription desc;
00499         desc.ReferenceTypeId = item.ReferenceTypeId;
00500         desc.TargetNodeId = resultId;
00501         desc.TargetNodeClass = item.Class;
00502         desc.BrowseName = item.BrowseName;
00503         desc.DisplayName = LocalizedText(item.BrowseName.Name);
00504         desc.TargetNodeTypeDefinition = item.TypeDefinition;
00505         desc.IsForward = true; // should this be in constructor?
00506 
00507         parent_node_it->second.References.push_back(desc);
00508       }
00509 
00510       if (item.TypeDefinition != ObjectId::Null)
00511       {
00512         // Link to parent
00513         AddReferencesItem typeRef;
00514         typeRef.SourceNodeId = resultId;
00515         typeRef.IsForward = true;
00516         typeRef.ReferenceTypeId = ObjectId::HasTypeDefinition;
00517         typeRef.TargetNodeId = item.TypeDefinition;
00518         typeRef.TargetNodeClass = NodeClass::DataType;
00519         AddReference(typeRef);
00520       }
00521 
00522       result.Status = StatusCode::Good;
00523       result.AddedNodeId = resultId;
00524       if (Debug) std::cout << "AddressSpaceInternal | node added." << std::endl;
00525       return result;
00526     }
00527 
00528     StatusCode AddressSpaceInMemory::AddReference(const AddReferencesItem& item)
00529     {
00530       NodesMap::iterator node_it = Nodes.find(item.SourceNodeId);
00531       if ( node_it == Nodes.end() )
00532       {
00533         return StatusCode::BadSourceNodeIdInvalid;
00534       }
00535       NodesMap::iterator targetnode_it = Nodes.find(item.TargetNodeId);
00536       if ( targetnode_it == Nodes.end() )
00537       {
00538         return StatusCode::BadTargetNodeIdInvalid;
00539       }
00540       ReferenceDescription desc;
00541       desc.ReferenceTypeId = item.ReferenceTypeId;
00542       desc.IsForward = item.IsForward;
00543       desc.TargetNodeId = item.TargetNodeId;
00544       desc.TargetNodeClass = item.TargetNodeClass;
00545       DataValue dv = GetValue(item.TargetNodeId, AttributeId::BrowseName);
00546       if (dv.Status == StatusCode::Good)
00547       {
00548         desc.BrowseName = dv.Value.As<QualifiedName>();
00549       }
00550       else
00551       {
00552         desc.BrowseName = QualifiedName("NONAME", 0);
00553       }
00554       dv = GetValue(item.TargetNodeId, AttributeId::DisplayName);
00555       if (dv.Status == StatusCode::Good)
00556       {
00557         desc.DisplayName = dv.Value.As<LocalizedText>();
00558       }
00559       else
00560       {
00561         desc.DisplayName = LocalizedText(desc.BrowseName.Name);
00562       }
00563       node_it->second.References.push_back(desc);
00564       return StatusCode::Good;
00565     }
00566 
00567     NodeId AddressSpaceInMemory::GetNewNodeId(const NodeId& id)
00568     {
00569       if (id == ObjectId::Null || id.IsNull())
00570       {
00571         return OpcUa::NumericNodeId(++MaxNodeIdNum, DefaultIdx);
00572       }
00573 
00574       if (id.HasNullIdentifier())
00575       {
00576         return OpcUa::NumericNodeId(++MaxNodeIdNum, id.GetNamespaceIndex());
00577       }
00578 
00579       return id;
00580     }
00581   }
00582 
00583   namespace Server
00584   {
00585     AddressSpace::UniquePtr CreateAddressSpace(bool debug)
00586     {
00587       return AddressSpace::UniquePtr(new Internal::AddressSpaceInMemory(debug));
00588     }
00589   }
00590 }


ros_opcua_impl_freeopcua
Author(s): Denis Štogl
autogenerated on Sat Jun 8 2019 18:24:39