PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
remote_node_manager.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
15
16#include <atomic>
17#include <condition_variable>
18#include <deque>
19#include <future>
20#include <mutex>
21#include <thread>
22#include <unordered_map>
23
24namespace kcenon::pacs::client {
25
26// =============================================================================
27// Constants
28// =============================================================================
29
30namespace {
31
33constexpr std::string_view verification_sop_class_uid = "1.2.840.10008.1.1";
34
36const std::vector<std::string> default_transfer_syntaxes = {
37 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
38 "1.2.840.10008.1.2" // Implicit VR Little Endian
39};
40
41} // namespace
42
43// =============================================================================
44// Implementation Structure
45// =============================================================================
46
48 // Repository for persistence
49 std::shared_ptr<storage::node_repository> repo;
50
51 // Logger
52 std::shared_ptr<di::ILogger> logger;
53
54 // Configuration
56
57 // Status callback
59 std::mutex callback_mutex;
60
61 // In-memory node cache
62 std::unordered_map<std::string, remote_node> node_cache;
63 mutable std::mutex cache_mutex;
64
65 // Statistics per node
66 std::unordered_map<std::string, node_statistics> statistics;
67 mutable std::mutex stats_mutex;
68
69 // Connection pool (node_id -> available associations)
71 std::unique_ptr<network::association> assoc;
72 std::chrono::steady_clock::time_point acquired_at;
73 };
74 std::unordered_map<std::string, std::deque<pooled_association>> connection_pool;
75 mutable std::mutex pool_mutex;
76
77 // Health check control
78 std::atomic<bool> health_check_running{false};
80 std::condition_variable health_check_cv;
82
83 // =========================================================================
84 // Helper Methods
85 // =========================================================================
86
87 void notify_status_change(std::string_view node_id, node_status status) {
88 std::lock_guard<std::mutex> lock(callback_mutex);
89 if (status_callback) {
90 status_callback(node_id, status);
91 }
92 }
93
94 void update_node_status(const std::string& node_id, node_status status,
95 const std::string& error_msg = "") {
96 // Update cache
97 {
98 std::lock_guard<std::mutex> lock(cache_mutex);
99 auto it = node_cache.find(node_id);
100 if (it != node_cache.end()) {
101 auto old_status = it->second.status;
102 it->second.status = status;
103 if (status == node_status::online) {
104 it->second.last_verified = std::chrono::system_clock::now();
105 } else if (status == node_status::error || status == node_status::offline) {
106 it->second.last_error = std::chrono::system_clock::now();
107 it->second.last_error_message = error_msg;
108 }
109
110 // Notify if status changed
111 if (old_status != status) {
112 notify_status_change(node_id, status);
113 }
114 }
115 }
116
117 // Update repository (ignore result as this is best-effort)
118 if (repo) {
119 [[maybe_unused]] auto result = repo->update_status(node_id, status, error_msg);
120 }
121 }
122
123 kcenon::pacs::VoidResult perform_echo(const remote_node& node) {
124 using namespace network;
125
126 // Build association config
127 association_config assoc_config;
128 assoc_config.calling_ae_title = config.local_ae_title;
129 assoc_config.called_ae_title = node.ae_title;
130 assoc_config.proposed_contexts.push_back({
131 1,
132 std::string(verification_sop_class_uid),
133 default_transfer_syntaxes
134 });
135
136 // Connect
137 auto connect_result = association::connect(
138 node.host,
139 node.port,
140 assoc_config,
141 std::chrono::duration_cast<association::duration>(node.connection_timeout)
142 );
143
144 if (connect_result.is_err()) {
147 "Failed to connect to node: " + node.node_id,
148 connect_result.error().message);
149 }
150
151 auto& assoc = connect_result.value();
152
153 // Get accepted context
154 auto context_id = assoc.accepted_context_id(verification_sop_class_uid);
155 if (!context_id) {
156 assoc.abort();
159 "Verification SOP Class not accepted by " + node.node_id);
160 }
161
162 // Build C-ECHO-RQ
163 auto echo_rq = dimse::make_c_echo_rq(1);
164
165 // Send request
166 auto send_result = assoc.send_dimse(*context_id, echo_rq);
167 if (send_result.is_err()) {
168 assoc.abort();
171 "Failed to send C-ECHO-RQ to " + node.node_id);
172 }
173
174 // Receive response
175 auto recv_result = assoc.receive_dimse(
176 std::chrono::duration_cast<association::duration>(node.dimse_timeout)
177 );
178
179 if (recv_result.is_err()) {
180 assoc.abort();
183 "Failed to receive C-ECHO-RSP from " + node.node_id);
184 }
185
186 const auto& [recv_context_id, response] = recv_result.value();
187
188 // Verify response
189 if (response.command() != dimse::command_field::c_echo_rsp) {
190 assoc.abort();
193 "Unexpected response from " + node.node_id);
194 }
195
196 // Check status (status_success is a constexpr, not enum member)
197 if (response.status() != dimse::status_success) {
198 [[maybe_unused]] auto release_result = assoc.release();
201 "C-ECHO failed with status: " +
202 std::to_string(static_cast<uint16_t>(response.status())));
203 }
204
205 // Release association
206 [[maybe_unused]] auto release_result = assoc.release();
207
208 return kcenon::pacs::ok();
209 }
210
212 while (health_check_running.load()) {
213 // Verify all nodes
214 std::vector<std::string> node_ids;
215 {
216 std::lock_guard<std::mutex> lock(cache_mutex);
217 for (const auto& [id, _] : node_cache) {
218 node_ids.push_back(id);
219 }
220 }
221
222 for (const auto& id : node_ids) {
223 if (!health_check_running.load()) break;
224
225 std::optional<remote_node> node;
226 {
227 std::lock_guard<std::mutex> lock(cache_mutex);
228 auto it = node_cache.find(id);
229 if (it != node_cache.end()) {
230 node = it->second;
231 }
232 }
233
234 if (node) {
236
237 auto result = perform_echo(*node);
238 if (result.is_ok()) {
240
241 // Update statistics
242 std::lock_guard<std::mutex> lock(stats_mutex);
243 statistics[id].successful_operations++;
244 statistics[id].last_activity = std::chrono::system_clock::now();
245 } else {
246 update_node_status(id, node_status::offline, result.error().message);
247
248 // Update statistics
249 std::lock_guard<std::mutex> lock(stats_mutex);
250 statistics[id].failed_operations++;
251 }
252 }
253 }
254
255 // Wait for next interval
256 std::unique_lock<std::mutex> lock(health_check_mutex);
257 health_check_cv.wait_for(lock, config.health_check_interval, [this] {
258 return !health_check_running.load();
259 });
260 }
261 }
262
264 if (!repo) return;
265
266#ifdef PACS_WITH_DATABASE_SYSTEM
267 auto nodes_result = repo->find_all();
268 if (nodes_result.is_err()) return;
269
270 std::lock_guard<std::mutex> lock(cache_mutex);
271 for (auto& node : nodes_result.value()) {
272 node_cache[node.node_id] = std::move(node);
273 }
274#else
275 auto nodes = repo->find_all();
276 std::lock_guard<std::mutex> lock(cache_mutex);
277 for (auto& node : nodes) {
278 node_cache[node.node_id] = std::move(node);
279 }
280#endif
281 }
282};
283
284// =============================================================================
285// Construction / Destruction
286// =============================================================================
287
289 std::shared_ptr<storage::node_repository> repo,
290 node_manager_config config,
291 std::shared_ptr<di::ILogger> logger)
292 : impl_(std::make_unique<impl>()) {
293
294 impl_->repo = std::move(repo);
295 impl_->config = std::move(config);
296 impl_->logger = logger ? std::move(logger) : di::null_logger();
297
298 // Load existing nodes from repository
300
301 // Auto-start health check if configured
304 }
305}
306
309
310 // Clear connection pool
311 {
312 std::lock_guard<std::mutex> lock(impl_->pool_mutex);
313 impl_->connection_pool.clear();
314 }
315}
316
317// =============================================================================
318// Node CRUD Operations
319// =============================================================================
320
321kcenon::pacs::VoidResult remote_node_manager::add_node(const remote_node& node) {
322 if (node.node_id.empty()) {
324 kcenon::pacs::error_codes::invalid_argument,
325 "Node ID cannot be empty");
326 }
327
328 if (node.ae_title.empty()) {
330 kcenon::pacs::error_codes::invalid_argument,
331 "AE Title cannot be empty");
332 }
333
334 if (node.host.empty()) {
336 kcenon::pacs::error_codes::invalid_argument,
337 "Host cannot be empty");
338 }
339
340 // Check for duplicate
341 {
342 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
343 if (impl_->node_cache.find(node.node_id) != impl_->node_cache.end()) {
345 kcenon::pacs::error_codes::already_exists,
346 "Node with ID already exists: " + node.node_id);
347 }
348 }
349
350 // Persist to repository
351 if (impl_->repo) {
352#ifdef PACS_WITH_DATABASE_SYSTEM
353 auto result = impl_->repo->save(node);
354 if (result.is_err()) {
356 result.error().code,
357 "Failed to persist node: " + result.error().message);
358 }
359#else
360 auto result = impl_->repo->upsert(node);
361 if (result.is_err()) {
363 result.error().code,
364 "Failed to persist node: " + result.error().message);
365 }
366#endif
367 }
368
369 // Add to cache
370 {
371 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
372 impl_->node_cache[node.node_id] = node;
373 }
374
375 impl_->logger->info_fmt("Added remote node: {} ({}:{})",
376 node.node_id, node.host, node.port);
377
378 return kcenon::pacs::ok();
379}
380
381kcenon::pacs::VoidResult remote_node_manager::update_node(const remote_node& node) {
382 // Check if exists
383 {
384 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
385 if (impl_->node_cache.find(node.node_id) == impl_->node_cache.end()) {
387 kcenon::pacs::error_codes::not_found,
388 "Node not found: " + node.node_id);
389 }
390 }
391
392 // Update repository
393 if (impl_->repo) {
394#ifdef PACS_WITH_DATABASE_SYSTEM
395 auto result = impl_->repo->save(node);
396 if (result.is_err()) {
398 result.error().code,
399 "Failed to update node: " + result.error().message);
400 }
401#else
402 auto result = impl_->repo->upsert(node);
403 if (result.is_err()) {
405 result.error().code,
406 "Failed to update node: " + result.error().message);
407 }
408#endif
409 }
410
411 // Update cache
412 {
413 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
414 impl_->node_cache[node.node_id] = node;
415 }
416
417 impl_->logger->info_fmt("Updated remote node: {}", node.node_id);
418
419 return kcenon::pacs::ok();
420}
421
422kcenon::pacs::VoidResult remote_node_manager::remove_node(std::string_view node_id) {
423 std::string id_str(node_id);
424
425 // Check if exists
426 {
427 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
428 if (impl_->node_cache.find(id_str) == impl_->node_cache.end()) {
430 kcenon::pacs::error_codes::not_found,
431 "Node not found: " + id_str);
432 }
433 }
434
435 // Remove from repository
436 if (impl_->repo) {
437 auto result = impl_->repo->remove(std::string(node_id));
438 if (result.is_err()) {
439 return result;
440 }
441 }
442
443 // Remove from cache
444 {
445 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
446 impl_->node_cache.erase(id_str);
447 }
448
449 // Remove from connection pool
450 {
451 std::lock_guard<std::mutex> lock(impl_->pool_mutex);
452 impl_->connection_pool.erase(id_str);
453 }
454
455 // Remove statistics
456 {
457 std::lock_guard<std::mutex> lock(impl_->stats_mutex);
458 impl_->statistics.erase(id_str);
459 }
460
461 impl_->logger->info_fmt("Removed remote node: {}", id_str);
462
463 return kcenon::pacs::ok();
464}
465
466std::optional<remote_node> remote_node_manager::get_node(std::string_view node_id) const {
467 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
468 auto it = impl_->node_cache.find(std::string(node_id));
469 if (it != impl_->node_cache.end()) {
470 return it->second;
471 }
472 return std::nullopt;
473}
474
475std::vector<remote_node> remote_node_manager::list_nodes() const {
476 std::vector<remote_node> result;
477 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
478 result.reserve(impl_->node_cache.size());
479 for (const auto& [_, node] : impl_->node_cache) {
480 result.push_back(node);
481 }
482 return result;
483}
484
485std::vector<remote_node> remote_node_manager::list_nodes_by_status(node_status status) const {
486 std::vector<remote_node> result;
487 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
488 for (const auto& [_, node] : impl_->node_cache) {
489 if (node.status == status) {
490 result.push_back(node);
491 }
492 }
493 return result;
494}
495
496// =============================================================================
497// Connection Verification
498// =============================================================================
499
500kcenon::pacs::VoidResult remote_node_manager::verify_node(std::string_view node_id) {
501 std::optional<remote_node> node;
502 {
503 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
504 auto it = impl_->node_cache.find(std::string(node_id));
505 if (it != impl_->node_cache.end()) {
506 node = it->second;
507 }
508 }
509
510 if (!node) {
512 kcenon::pacs::error_codes::not_found,
513 "Node not found: " + std::string(node_id));
514 }
515
516 impl_->update_node_status(std::string(node_id), node_status::verifying);
517
518 auto result = impl_->perform_echo(*node);
519
520 if (result.is_ok()) {
521 impl_->update_node_status(std::string(node_id), node_status::online);
522 if (impl_->repo) {
523 [[maybe_unused]] auto update_result = impl_->repo->update_last_verified(node_id);
524 }
525 } else {
526 impl_->update_node_status(std::string(node_id), node_status::offline,
527 result.error().message);
528 }
529
530 return result;
531}
532
533std::future<kcenon::pacs::VoidResult> remote_node_manager::verify_node_async(
534 std::string_view node_id) {
535
536 std::string id_str(node_id);
537
538 return std::async(std::launch::async, [this, id_str]() {
539 return verify_node(id_str);
540 });
541}
542
544 std::vector<std::string> node_ids;
545 {
546 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
547 for (const auto& [id, _] : impl_->node_cache) {
548 node_ids.push_back(id);
549 }
550 }
551
552 for (const auto& id : node_ids) {
553 std::thread([this, id]() {
554 [[maybe_unused]] auto result = verify_node(id);
555 }).detach();
556 }
557}
558
559// =============================================================================
560// Association Pool Management
561// =============================================================================
562
564 std::string_view node_id,
565 std::span<const std::string> sop_classes) {
566
567 std::string id_str(node_id);
568 std::optional<remote_node> node;
569
570 {
571 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
572 auto it = impl_->node_cache.find(id_str);
573 if (it != impl_->node_cache.end()) {
574 node = it->second;
575 }
576 }
577
578 if (!node) {
579 return kcenon::pacs::pacs_error<std::unique_ptr<network::association>>(
580 kcenon::pacs::error_codes::not_found,
581 "Node not found: " + id_str);
582 }
583
584 // Try to get from pool
585 {
586 std::lock_guard<std::mutex> lock(impl_->pool_mutex);
587 auto it = impl_->connection_pool.find(id_str);
588 if (it != impl_->connection_pool.end() && !it->second.empty()) {
589 auto pooled = std::move(it->second.front());
590 it->second.pop_front();
591
592 // Check if connection is still valid and not expired
593 auto age = std::chrono::steady_clock::now() - pooled.acquired_at;
594 if (age < impl_->config.pool_connection_ttl &&
595 pooled.assoc && pooled.assoc->is_established()) {
596 impl_->logger->debug_fmt("Reusing pooled association for {}", id_str);
597 return kcenon::pacs::ok(std::move(pooled.assoc));
598 }
599 }
600 }
601
602 // Create new association
603 network::association_config assoc_config;
605 assoc_config.called_ae_title = node->ae_title;
606
607 uint8_t context_id = 1;
608 for (const auto& sop_class : sop_classes) {
609 assoc_config.proposed_contexts.push_back({
610 context_id,
611 sop_class,
612 default_transfer_syntaxes
613 });
614 context_id += 2; // Context IDs must be odd
615 }
616
617 auto connect_result = network::association::connect(
618 node->host,
619 node->port,
620 assoc_config,
621 std::chrono::duration_cast<network::association::duration>(node->connection_timeout)
622 );
623
624 if (connect_result.is_err()) {
626 connect_result.error().message);
627 return connect_result.error();
628 }
629
630 // Update statistics
631 {
632 std::lock_guard<std::mutex> lock(impl_->stats_mutex);
633 impl_->statistics[id_str].total_connections++;
634 impl_->statistics[id_str].active_connections++;
635 }
636
637 return kcenon::pacs::ok(std::make_unique<network::association>(std::move(connect_result.value())));
638}
639
641 std::string_view node_id,
642 std::unique_ptr<network::association> assoc) {
643
644 std::string id_str(node_id);
645
646 // Update statistics
647 {
648 std::lock_guard<std::mutex> lock(impl_->stats_mutex);
649 auto it = impl_->statistics.find(id_str);
650 if (it != impl_->statistics.end() && it->second.active_connections > 0) {
651 it->second.active_connections--;
652 }
653 }
654
655 if (!assoc || !assoc->is_established()) {
656 return;
657 }
658
659 // Check pool capacity
660 {
661 std::lock_guard<std::mutex> lock(impl_->pool_mutex);
662 auto& pool = impl_->connection_pool[id_str];
663 if (pool.size() < impl_->config.max_pool_connections_per_node) {
665 pooled.assoc = std::move(assoc);
666 pooled.acquired_at = std::chrono::steady_clock::now();
667 pool.push_back(std::move(pooled));
668 impl_->logger->debug_fmt("Returned association to pool for {}", id_str);
669 return;
670 }
671 }
672
673 // Pool is full, release the association
674 [[maybe_unused]] auto release_result = assoc->release();
675}
676
677// =============================================================================
678// Health Check Scheduler
679// =============================================================================
680
682 if (impl_->health_check_running.load()) {
683 return;
684 }
685
686 impl_->health_check_running.store(true);
687 impl_->health_check_thread = std::thread([this]() {
689 });
690
691 impl_->logger->info("Started health check scheduler");
692}
693
695 if (!impl_->health_check_running.load()) {
696 return;
697 }
698
699 impl_->health_check_running.store(false);
700 impl_->health_check_cv.notify_all();
701
702 if (impl_->health_check_thread.joinable()) {
704 }
705
706 impl_->logger->info("Stopped health check scheduler");
707}
708
710 return impl_->health_check_running.load();
711}
712
713// =============================================================================
714// Status Monitoring
715// =============================================================================
716
717node_status remote_node_manager::get_status(std::string_view node_id) const {
718 std::lock_guard<std::mutex> lock(impl_->cache_mutex);
719 auto it = impl_->node_cache.find(std::string(node_id));
720 if (it != impl_->node_cache.end()) {
721 return it->second.status;
722 }
724}
725
727 std::lock_guard<std::mutex> lock(impl_->callback_mutex);
728 impl_->status_callback = std::move(callback);
729}
730
731// =============================================================================
732// Statistics
733// =============================================================================
734
736 std::lock_guard<std::mutex> lock(impl_->stats_mutex);
737 auto it = impl_->statistics.find(std::string(node_id));
738 if (it != impl_->statistics.end()) {
739 return it->second;
740 }
741 return {};
742}
743
744void remote_node_manager::reset_statistics(std::string_view node_id) {
745 std::lock_guard<std::mutex> lock(impl_->stats_mutex);
746 if (node_id.empty()) {
747 impl_->statistics.clear();
748 } else {
749 impl_->statistics.erase(std::string(node_id));
750 }
751}
752
753// =============================================================================
754// Configuration
755// =============================================================================
756
758 return impl_->config;
759}
760
762 impl_->config = std::move(new_config);
763}
764
765} // namespace kcenon::pacs::client
DICOM Association management per PS3.8.
void start_health_check()
Start the automatic health check scheduler.
auto is_health_check_running() const noexcept -> bool
Check if health check is running.
auto get_statistics(std::string_view node_id) const -> node_statistics
Get statistics for a node.
auto update_node(const remote_node &node) -> kcenon::pacs::VoidResult
Update an existing remote node.
auto get_node(std::string_view node_id) const -> std::optional< remote_node >
Get a node by ID.
auto get_status(std::string_view node_id) const -> node_status
Get the current status of a node.
~remote_node_manager()
Destructor - stops health check if running.
void verify_all_nodes_async()
Verify all nodes asynchronously.
void stop_health_check()
Stop the automatic health check scheduler.
remote_node_manager(std::shared_ptr< storage::node_repository > repo, node_manager_config config={}, std::shared_ptr< di::ILogger > logger=nullptr)
Construct a remote node manager.
auto verify_node(std::string_view node_id) -> kcenon::pacs::VoidResult
Verify a node's connectivity synchronously.
auto list_nodes_by_status(node_status status) const -> std::vector< remote_node >
List nodes filtered by status.
auto remove_node(std::string_view node_id) -> kcenon::pacs::VoidResult
Remove a remote node.
auto list_nodes() const -> std::vector< remote_node >
List all registered nodes.
auto acquire_association(std::string_view node_id, std::span< const std::string > sop_classes) -> kcenon::pacs::Result< std::unique_ptr< network::association > >
Acquire an association from the pool.
void reset_statistics(std::string_view node_id="")
Reset statistics for a node.
auto config() const noexcept -> const node_manager_config &
Get the current configuration.
void set_config(node_manager_config new_config)
Update the configuration.
void set_status_callback(node_status_callback callback)
Set the status change callback.
auto add_node(const remote_node &node) -> kcenon::pacs::VoidResult
Add a new remote node.
void release_association(std::string_view node_id, std::unique_ptr< network::association > assoc)
Release an association back to the pool.
auto verify_node_async(std::string_view node_id) -> std::future< kcenon::pacs::VoidResult >
Verify a node's connectivity asynchronously.
static Result< association > connect(const std::string &host, uint16_t port, const association_config &config, duration timeout=default_timeout)
Initiate an SCU association to a remote SCP.
DIMSE message encoding and decoding.
node_status
Status of a remote PACS node.
Definition remote_node.h:51
@ verifying
Verification in progress.
@ offline
Node is not responding.
@ online
Node is responding to C-ECHO.
@ unknown
Status not yet determined.
@ error
Node returned an error.
std::function< void(std::string_view node_id, node_status status)> node_status_callback
Callback function type for node status changes.
std::shared_ptr< ILogger > null_logger()
Get a shared null logger instance.
Definition ilogger.h:271
constexpr int no_acceptable_context
Definition result.h:103
constexpr int receive_failed
Definition result.h:97
constexpr int send_failed
Definition result.h:96
constexpr int connection_failed
Definition result.h:94
constexpr int dimse_error
Definition result.h:90
constexpr std::string_view verification_sop_class_uid
Verification SOP Class UID (1.2.840.10008.1.1)
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Definition result.h:249
Repository for remote PACS node persistence using base_repository pattern.
Remote PACS node manager for client operations.
DIMSE status codes.
Configuration for the remote node manager.
size_t max_pool_connections_per_node
Maximum pooled connections per node.
std::string local_ae_title
Our AE Title for outgoing associations.
std::chrono::seconds health_check_interval
Interval between automatic health checks.
bool auto_start_health_check
Start health check automatically on construction.
Statistics for a remote node.
void update_node_status(const std::string &node_id, node_status status, const std::string &error_msg="")
kcenon::pacs::VoidResult perform_echo(const remote_node &node)
void notify_status_change(std::string_view node_id, node_status status)
std::unordered_map< std::string, node_statistics > statistics
std::unordered_map< std::string, remote_node > node_cache
std::unordered_map< std::string, std::deque< pooled_association > > connection_pool
std::shared_ptr< storage::node_repository > repo
std::string ae_title
DICOM Application Entity Title.
uint16_t port
DICOM port (default: 104)
std::string node_id
Unique identifier for this node.
std::chrono::seconds dimse_timeout
DIMSE operation timeout.
std::string host
IP address or hostname.
std::chrono::seconds connection_timeout
TCP connection timeout.
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts