Skip to main content

TCP server

Some outstations, such as devices on cellular modems or behind NAT, connect to the master instead of waiting for it to connect. A master TCP server accepts these connections and gives you a MasterChannel for each one. Your ConnectionHandler decides which connections to accept and sets up each channel:

CallbackCalled whenWhat your code does
acceptAn outstation connectsAccepts or rejects it by remote address, or asks to identify it first
acceptWithLinkIdThe outstation's first link frame arrivesAccepts or rejects it by source and destination address
start, startWithLinkIdThe channel is createdAdds an association and enables the channel

In Rust, acceptWithLinkId is named accept_link_id.

Creating the server​

Create the server with the local address to listen on, a LinkIdConfig and your ConnectionHandler. Creating it fails if the address can't be bound, for example because another process is listening on it.

let _server = spawn_master_tcp_server(
"127.0.0.1:20000".parse()?,
LinkIdConfig::default().decode_level(PhysDecodeLevel::Data),
ConnectionHandler {
channels: Default::default(),
},
)
.await?;

Accepting connections​

The server calls accept with the remote address of each new connection. Your code can:

In the bindings, accept receives an AcceptHandler: call its accept(..) or getLinkIdentity() method, or neither to reject the connection. In Rust, accept returns an AcceptAction, or Err(Reject).

Accepting by remote address works when each outstation connects from an address you know in advance.

Identifying the outstation​

Outstations on cellular networks or behind NAT often don't have a fixed or unique address. Instead, the server can wait for the outstation's first link frame, usually an unsolicited response, and call acceptWithLinkId with its source and destination. The source is the outstation's DNP3 address, and the destination is the master address the outstation expects, which you can use as the channel's master address.

You accept or reject the connection the same way, and the server then calls startWithLinkId. The frame isn't lost: the new channel processes it. LinkIdConfig sets how long the server waits for the frame, after which it closes the connection, and how many connections can wait at once.

impl dnp3::tcp::ConnectionHandler for ConnectionHandler {
async fn accept(&mut self, _: SocketAddr) -> Result<AcceptAction, Reject> {
Ok(AcceptAction::GetLinkIdentity)
}

async fn start(&mut self, _: MasterChannel, _: SocketAddr) {
//
}

async fn accept_link_id(
&mut self,
addr: SocketAddr,
source: u16,
_destination: u16,
) -> Result<AcceptConfig, Reject> {
tracing::info!("accepted from {addr:?}:{source}");
let mut decode_level = DecodeLevel::nothing();
decode_level.application = AppDecodeLevel::ObjectValues;
let config = AcceptConfig {
error_mode: LinkErrorMode::Close,
config: MasterChannelConfig {
master_address: EndpointAddress::try_new(1).unwrap(),
decode_level,
tx_buffer_size: Default::default(),
rx_buffer_size: Default::default(),
},
};
Ok(config)
}

async fn start_with_link_id(
&mut self,
mut channel: MasterChannel,
_addr: SocketAddr,
source: u16,
destination: u16,
) {
tracing::info!("start with source = {source} dest = {destination}");

match Self::setup_channel(&mut channel, source).await {
Ok(_) => {
self.channels.insert(source, channel);
}
Err(err) => {
tracing::warn!("channel setup failed: {err}");
}
}
}
}

Starting the channel​

start and startWithLinkId hand you the new MasterChannel. Like any channel, it doesn't communicate until it's enabled: add an association for the outstation, then call enable.

In Rust, these callbacks are async, so you can do this directly in them. In the bindings, adding an association and enabling a channel can't be called from a callback, so pass the channel to another thread. The Java example puts it on a queue that a starter thread reads.

The server waits for each callback to return before it handles the next connection, so return quickly. If the Java example's queue is full, its callback shuts the channel down rather than wait.

async fn setup_channel(
channel: &mut MasterChannel,
source: u16,
) -> Result<AssociationHandle, Box<dyn std::error::Error>> {
let assoc = channel
.add_association(
EndpointAddress::try_new(source)?,
AssociationConfig::new(
EventClasses::all(),
EventClasses::all(),
Classes::all(),
EventClasses::none(),
),
Box::new(NullReadHandler),
Box::new(NullAssociationHandler),
Box::new(NullAssociationInformation),
)
.await?;
channel.enable().await?;
Ok(assoc)
}

Keep the channel for as long as you want to talk to the outstation. In Rust, dropping the MasterChannel and its AssociationHandles ends the session; in the bindings, shut the channel down when you're done with it.

The server doesn't track the channels it has created. When an outstation reconnects, it gets a new channel, so shut down the old one. Both examples keep their channels in a map keyed by the outstation's address and replace the old entry.

Shutting down​

In Rust, dropping the ServerHandle stops the server. In the bindings, shut down the MasterServer: close() in Java, Dispose() in C#, its destructor in C++, or dnp3_master_server_destroy() in C.

The server then stops accepting connections. Channels it has already created keep running until you shut them down.