PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
association.h
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
19#ifndef PACS_NETWORK_ASSOCIATION_HPP
20#define PACS_NETWORK_ASSOCIATION_HPP
21
25
26#include <chrono>
27#include <cstdint>
28#include <functional>
29#include <map>
30#include <memory>
31#include <mutex>
32#include <optional>
33#include <string>
34#include <string_view>
35#include <variant>
36#include <vector>
37
38#include <kcenon/thread/lockfree/lockfree_queue.h>
39
41
42namespace kcenon::pacs::network {
43
44// =============================================================================
45// Forward Declarations
46// =============================================================================
47
48class association;
49
50// =============================================================================
51// Result Type
52// =============================================================================
53
55template <typename T>
57
59using VoidResult = kcenon::pacs::VoidResult;
60
61// =============================================================================
62// Association State
63// =============================================================================
64
80
86[[nodiscard]] constexpr const char* to_string(association_state state) noexcept {
87 switch (state) {
88 case association_state::idle: return "Idle (Sta1)";
89 case association_state::awaiting_associate_ac: return "Awaiting A-ASSOCIATE-AC (Sta5)";
90 case association_state::awaiting_associate_rq: return "Awaiting A-ASSOCIATE-RQ (Sta2)";
91 case association_state::established: return "Established (Sta6)";
92 case association_state::awaiting_release_rp: return "Awaiting A-RELEASE-RP (Sta7)";
93 case association_state::awaiting_release_rq: return "Awaiting A-RELEASE-RQ (Sta8)";
94 case association_state::released: return "Released";
95 case association_state::aborted: return "Aborted";
96 default: return "Unknown";
97 }
98}
99
100// =============================================================================
101// Association Error
102// =============================================================================
103
125
129[[nodiscard]] constexpr std::string_view to_string(association_error err) noexcept {
130 switch (err) {
131 case association_error::success: return "Success";
132 case association_error::connection_failed: return "Connection failed";
133 case association_error::connection_timeout: return "Connection timeout";
134 case association_error::association_rejected: return "Association rejected";
135 case association_error::association_aborted: return "Association aborted";
136 case association_error::invalid_state: return "Invalid state for operation";
137 case association_error::negotiation_failed: return "Negotiation failed";
138 case association_error::no_acceptable_context: return "No acceptable presentation context";
139 case association_error::pdu_encoding_error: return "PDU encoding error";
140 case association_error::pdu_decoding_error: return "PDU decoding error";
141 case association_error::send_failed: return "Send failed";
142 case association_error::receive_failed: return "Receive failed";
143 case association_error::receive_timeout: return "Receive timeout";
144 case association_error::protocol_error: return "Protocol error";
145 case association_error::release_failed: return "Release failed";
146 case association_error::already_released: return "Already released";
147 default: return "Unknown error";
148 }
149}
150
151// =============================================================================
152// Association Configuration
153// =============================================================================
154
159 uint8_t id;
160 std::string abstract_syntax;
161 std::vector<std::string> transfer_syntaxes;
162
164 proposed_presentation_context(uint8_t ctx_id, std::string abs_syntax,
165 std::vector<std::string> ts_list)
166 : id(ctx_id)
167 , abstract_syntax(std::move(abs_syntax))
168 , transfer_syntaxes(std::move(ts_list)) {}
169};
170
175 uint8_t id;
176 std::string abstract_syntax;
177 std::string transfer_syntax;
179
182 accepted_presentation_context(uint8_t ctx_id, std::string abs_syntax,
183 std::string ts, presentation_context_result res)
184 : id(ctx_id)
185 , abstract_syntax(std::move(abs_syntax))
186 , transfer_syntax(std::move(ts))
187 , result(res) {}
188
189 [[nodiscard]] bool is_accepted() const noexcept {
191 }
192};
193
198 std::string calling_ae_title;
199 std::string called_ae_title;
200 std::vector<proposed_presentation_context> proposed_contexts;
204
206};
207
212 std::string ae_title;
213 std::vector<std::string> accepted_ae_titles;
214 std::vector<std::string> supported_abstract_syntaxes;
215 std::vector<std::string> supported_transfer_syntaxes;
219
220 scp_config() = default;
221};
222
223// =============================================================================
224// Association Rejection Info
225// =============================================================================
226
232 uint8_t source;
233 uint8_t reason;
234 std::string description;
235
240
241 rejection_info(reject_result res, uint8_t src, uint8_t rsn)
242 : result(res), source(src), reason(rsn) {
244 }
245
246private:
247 void build_description();
248};
249
250// =============================================================================
251// Association Class
252// =============================================================================
253
280public:
281 // =========================================================================
282 // Type Aliases
283 // =========================================================================
284
285 using clock = std::chrono::steady_clock;
286 using duration = std::chrono::milliseconds;
287 using time_point = clock::time_point;
288
290 static constexpr duration default_timeout{30000}; // 30 seconds
291
292 // =========================================================================
293 // Construction / Destruction
294 // =========================================================================
295
300
304 association(association&& other) noexcept;
305
309 association& operator=(association&& other) noexcept;
310
314 ~association();
315
316 // Disable copy
317 association(const association&) = delete;
319
320 // =========================================================================
321 // Factory Methods
322 // =========================================================================
323
333 [[nodiscard]] static Result<association> connect(
334 const std::string& host,
335 uint16_t port,
336 const association_config& config,
337 duration timeout = default_timeout);
338
346 [[nodiscard]] static association accept(
347 const associate_rq& rq,
348 const scp_config& config);
349
358 [[nodiscard]] static associate_rj reject(
359 reject_result result,
360 uint8_t source,
361 uint8_t reason);
362
363 // =========================================================================
364 // State Queries
365 // =========================================================================
366
370 [[nodiscard]] association_state state() const noexcept;
371
375 [[nodiscard]] bool is_established() const noexcept;
376
380 [[nodiscard]] bool is_closed() const noexcept;
381
382 // =========================================================================
383 // Negotiated Parameters
384 // =========================================================================
385
389 [[nodiscard]] std::string_view calling_ae() const noexcept;
390
394 [[nodiscard]] std::string_view called_ae() const noexcept;
395
399 [[nodiscard]] uint32_t max_pdu_size() const noexcept;
400
404 [[nodiscard]] std::string_view remote_implementation_class() const noexcept;
405
409 [[nodiscard]] std::string_view remote_implementation_version() const noexcept;
410
411 // =========================================================================
412 // Presentation Context Management
413 // =========================================================================
414
418 [[nodiscard]] bool has_accepted_context(std::string_view abstract_syntax) const;
419
424 [[nodiscard]] std::optional<uint8_t> accepted_context_id(
425 std::string_view abstract_syntax) const;
426
432 [[nodiscard]] Result<encoding::transfer_syntax> context_transfer_syntax(
433 uint8_t pc_id) const;
434
438 [[nodiscard]] const std::vector<accepted_presentation_context>&
439 accepted_contexts() const noexcept;
440
441 // =========================================================================
442 // DIMSE Operations
443 // =========================================================================
444
452 [[nodiscard]] Result<std::monostate> send_dimse(
453 uint8_t context_id,
454 const dimse::dimse_message& msg);
455
462 [[nodiscard]] Result<std::pair<uint8_t, dimse::dimse_message>> receive_dimse(
463 duration timeout = default_timeout);
464
465 // =========================================================================
466 // PDU Access (for network layer integration)
467 // =========================================================================
468
472 [[nodiscard]] associate_rq build_associate_rq() const;
473
477 [[nodiscard]] associate_ac build_associate_ac() const;
478
483 bool process_associate_ac(const associate_ac& ac);
484
488 void process_associate_rj(const associate_rj& rj);
489
493 [[nodiscard]] std::optional<rejection_info> get_rejection_info() const;
494
495 // =========================================================================
496 // Lifecycle Management
497 // =========================================================================
498
507 [[nodiscard]] Result<std::monostate> release(duration timeout = default_timeout);
508
513 void process_release_rq();
514
518 void process_release_rp();
519
526 void abort(uint8_t source = 0, uint8_t reason = 0);
527
531 void process_abort(const abort_source& source, const abort_reason& reason);
532
533 // =========================================================================
534 // Internal State Management (for testing/debugging)
535 // =========================================================================
536
541 void set_state(association_state new_state);
542
546 void set_peer(association* peer);
547
551 void enqueue_message(uint8_t context_id, dimse::dimse_message msg);
552
556 void update_peer(association* old_peer, association* new_peer);
557
558private:
559 // =========================================================================
560 // Private Implementation
561 // =========================================================================
562
564 [[nodiscard]] bool can_transition_to(association_state new_state) const;
565
567 void transition(association_state new_state);
568
570 void build_context_map();
571
573 void negotiate_contexts(const associate_rq& rq, const scp_config& config);
574
575 // =========================================================================
576 // Member Variables
577 // =========================================================================
578
581
583 std::string calling_ae_;
584
586 std::string called_ae_;
587
589 std::string our_ae_;
590
593
596
599
602
605
607 std::vector<proposed_presentation_context> proposed_contexts_;
608
610 std::vector<accepted_presentation_context> accepted_contexts_;
611
613 std::map<std::string, uint8_t> abstract_syntax_to_context_;
614
616 std::map<uint8_t, encoding::transfer_syntax> context_to_transfer_syntax_;
617
619 std::optional<rejection_info> rejection_info_;
620
622 uint8_t abort_source_{0};
623
625 uint8_t abort_reason_{0};
626
628 mutable std::mutex mutex_;
629
631 bool is_scu_{true};
632
635
637 using message_type = std::pair<uint8_t, dimse::dimse_message>;
638 using message_queue_type = kcenon::thread::detail::concurrent_queue<message_type>;
639 mutable std::unique_ptr<message_queue_type> incoming_queue_{
640 std::make_unique<message_queue_type>()};
641};
642
643} // namespace kcenon::pacs::network
644
645#endif // PACS_NETWORK_ASSOCIATION_HPP
std::map< std::string, uint8_t > abstract_syntax_to_context_
Map from abstract syntax to accepted context ID.
std::string_view remote_implementation_version() const noexcept
Get remote implementation version name.
kcenon::thread::detail::concurrent_queue< message_type > message_queue_type
std::chrono::steady_clock clock
std::unique_ptr< message_queue_type > incoming_queue_
static association accept(const associate_rq &rq, const scp_config &config)
Accept an incoming SCP association.
std::string remote_implementation_class_
Remote implementation class UID.
std::vector< proposed_presentation_context > proposed_contexts_
Proposed presentation contexts (SCU)
uint8_t abort_reason_
Abort reason (if aborted)
bool process_associate_ac(const associate_ac &ac)
Process received A-ASSOCIATE-AC PDU.
void enqueue_message(uint8_t context_id, dimse::dimse_message msg)
Enqueue message from peer (for in-memory testing).
association()
Default constructor (creates idle association).
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.
std::optional< rejection_info > get_rejection_info() const
Get rejection info if association was rejected.
std::string called_ae_
Called AE Title.
std::string our_implementation_class_
Our implementation class UID.
std::string our_ae_
Our AE Title (may be calling or called depending on role)
~association()
Destructor (aborts if still established).
void process_release_rp()
Process received A-RELEASE-RP.
void build_context_map()
Build presentation context map from accepted contexts.
bool can_transition_to(association_state new_state) const
Validate state transition.
void set_state(association_state new_state)
Force state transition (for testing).
const std::vector< accepted_presentation_context > & accepted_contexts() const noexcept
Get all accepted presentation contexts.
bool is_established() const noexcept
Check if association is established and ready for DIMSE.
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
std::string_view calling_ae() const noexcept
Get calling AE title.
std::string_view called_ae() const noexcept
Get called AE title.
void update_peer(association *old_peer, association *new_peer)
Update peer pointer (for in-memory testing).
uint32_t max_pdu_size() const noexcept
Get negotiated maximum PDU size.
void process_release_rq()
Process received A-RELEASE-RQ.
bool is_scu_
Is this an SCU (true) or SCP (false)?
std::mutex mutex_
Thread safety mutex.
association & operator=(association &&other) noexcept
Move assignment operator.
associate_ac build_associate_ac() const
Build A-ASSOCIATE-AC PDU for sending.
std::string calling_ae_
Calling AE Title.
association_state state_
Current state.
associate_rq build_associate_rq() const
Build A-ASSOCIATE-RQ PDU for sending.
association_state state() const noexcept
Get current association state.
std::string our_implementation_version_
Our implementation version name.
Result< encoding::transfer_syntax > context_transfer_syntax(uint8_t pc_id) const
Get the transfer syntax for an accepted context.
void transition(association_state new_state)
Perform state transition.
void set_peer(association *peer)
Set peer association for in-memory testing.
std::chrono::milliseconds duration
std::map< uint8_t, encoding::transfer_syntax > context_to_transfer_syntax_
Map from context ID to transfer syntax.
Result< std::monostate > release(duration timeout=default_timeout)
Gracefully release the association.
association * peer_
Peer association for in-memory testing.
bool has_accepted_context(std::string_view abstract_syntax) const
Check if a presentation context for the abstract syntax was accepted.
uint8_t abort_source_
Abort source (if aborted)
association(const association &)=delete
Result< std::pair< uint8_t, dimse::dimse_message > > receive_dimse(duration timeout=default_timeout)
Receive a DIMSE message.
uint32_t max_pdu_size_
Negotiated maximum PDU size.
bool is_closed() const noexcept
Check if association has been released or aborted.
association & operator=(const association &)=delete
std::pair< uint8_t, dimse::dimse_message > message_type
Incoming message queue for in-memory testing (thread-safe)
std::string remote_implementation_version_
Remote implementation version name.
static constexpr duration default_timeout
Default timeout for operations.
std::vector< accepted_presentation_context > accepted_contexts_
Accepted presentation contexts.
void process_associate_rj(const associate_rj &rj)
Process received A-ASSOCIATE-RJ PDU.
void negotiate_contexts(const associate_rq &rq, const scp_config &config)
Negotiate presentation contexts for SCP.
static associate_rj reject(reject_result result, uint8_t source, uint8_t reason)
Reject an incoming association request.
std::optional< rejection_info > rejection_info_
Rejection information (if rejected)
std::string_view remote_implementation_class() const noexcept
Get remote implementation class UID.
void process_abort(const abort_source &source, const abort_reason &reason)
Process received A-ABORT PDU.
std::optional< uint8_t > accepted_context_id(std::string_view abstract_syntax) const
Get the presentation context ID for an abstract syntax.
DIMSE message encoding and decoding.
reject_result
Reject result values.
Definition pdu_types.h:92
@ rejected_permanent
Rejected-permanent.
@ abstract_syntax
Abstract Syntax Sub-item.
abort_reason
Abort reason values when source is service-provider.
Definition pdu_types.h:79
constexpr uint32_t DEFAULT_MAX_PDU_LENGTH
Maximum PDU length recommended by DICOM (16384 bytes)
Definition pdu_types.h:276
constexpr const char * to_string(association_state state) noexcept
Convert association_state to string representation.
Definition association.h:86
kcenon::pacs::VoidResult VoidResult
VoidResult type alias for operations without return value.
Definition association.h:59
association_error
Error codes for association operations.
abort_source
Abort source values.
Definition pdu_types.h:70
presentation_context_result
Result values for A-ASSOCIATE-AC presentation context.
Definition pdu_types.h:59
association_state
DICOM Association state machine states per PS3.8.
Definition association.h:70
@ released
Association gracefully released.
@ awaiting_release_rp
Sta7: Awaiting A-RELEASE response (initiator)
@ awaiting_associate_ac
Sta5: Awaiting A-ASSOCIATE response (SCU)
@ established
Sta6: Association established, ready for DIMSE.
@ aborted
Association aborted (error condition)
@ awaiting_release_rq
Sta8: Awaiting potential A-RELEASE request.
@ awaiting_associate_rq
Sta2: Awaiting A-ASSOCIATE request (SCP)
@ idle
Sta1: No TCP connection, waiting for transport.
Transfer Syntax UIDs.
Definition main.cpp:78
Result<T> type aliases and helpers for PACS system.
Accepted presentation context after negotiation.
accepted_presentation_context(uint8_t ctx_id, std::string abs_syntax, std::string ts, presentation_context_result res)
presentation_context_result result
Negotiation result.
std::string transfer_syntax
Accepted Transfer Syntax UID.
std::string abstract_syntax
Abstract Syntax UID.
A-ASSOCIATE-AC PDU data.
Definition pdu_types.h:218
A-ASSOCIATE-RJ PDU data.
Definition pdu_types.h:231
A-ASSOCIATE-RQ PDU data.
Definition pdu_types.h:205
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
Proposed presentation context for SCU association request.
std::string abstract_syntax
Abstract Syntax UID (SOP Class)
uint8_t id
Presentation Context ID (odd 1-255)
std::vector< std::string > transfer_syntaxes
Proposed Transfer Syntaxes.
proposed_presentation_context(uint8_t ctx_id, std::string abs_syntax, std::vector< std::string > ts_list)
Information about an association rejection.
rejection_info(reject_result res, uint8_t src, uint8_t rsn)
Configuration for SCP to accept associations.
std::vector< std::string > accepted_ae_titles
Allowed calling AE titles (empty = all)
std::string ae_title
Our AE Title.
std::vector< std::string > supported_abstract_syntaxes
std::vector< std::string > supported_transfer_syntaxes