File transfer
File transfer lets a master retrieve files from an outstation, such as logs and disturbance records, and push files to it, such as configuration. The master API supports these operations:
- Get file info - Retrieve information on an individual file or directory
- Read file - Read a remote file from the outstation asynchronously
- Read directory listing - Read the contents of a directory from the outstation
- Write file - Write a file to the outstation, one block at a time
Get file info
Information about a single file or directory may be retrieved from outstation using a single method invocation.
- Rust
- C
- C++
- Java
- C#
let info = self.association.get_file_info(".").await?;
print_file_info(info);
// Callback interface
void on_file_info_complete(dnp3_file_info_t info, void *context)
{
print_file_info(info);
printf("%s: success\n", (const char *)context);
}
void on_file_failure(dnp3_file_error_t error, void *context) { printf("%s: %s\n", (const char *)context, dnp3_file_error_to_string(error)); }
// asynchronously start the operation
dnp3_file_info_callback_t cb = {
.on_complete = &on_file_info_complete,
.on_failure = &on_file_failure,
.on_destroy = NULL,
.ctx = "gfi",
};
dnp3_param_error_t err = dnp3_master_channel_get_file_info(channel, association_id, ".", cb);
// Callback interface
class FileInfoCallback : public dnp3::FileInfoCallback {
public:
explicit FileInfoCallback(const char *cmd) : cmd(cmd) {}
void on_complete(const dnp3::FileInfo &info) override
{
print_file_info(info);
std::cout << cmd << ": success" << std::endl;
}
void on_failure(dnp3::FileError error) override { std::cout << cmd << ": " << dnp3::to_string(error) << std::endl; }
private:
const char *cmd;
};
std::unique_ptr<dnp3::FileInfoCallback> create_file_info_callback(const char *cmd)
{
return std::make_unique<FileInfoCallback>(cmd);
}
// asynchronously start the operation
channel.get_file_info(assoc, ".", create_file_info_callback("gfi"));
FileInfo info = channel.getFileInfo(association, ".").toCompletableFuture().get();
printFileInfo(info);
var info = await channel.GetFileInfo(association, ".");
PrintFileInfo(info);
Read a file
File data may be asynchronously read from the outstation using a callback interface. Reading a file consists of multiple DNP3 requests. Obtaining an authentication key from the outstation is optional, depending on outstation requirements.
The FileReader interface is used to receive data from the outstation as it is read. Your code is responsible for doing
something with this data, e.g. writing it to a local file.
- Rust
- C
- C++
- Java
- C#
struct FileLogger;
impl FileReader for FileLogger {
fn opened(&mut self, size: u32) -> FileAction {
tracing::info!("File opened - size: {size}");
FileAction::Continue
}
fn block_received(&mut self, block_num: u32, data: &[u8]) -> MaybeAsync<FileAction> {
tracing::info!("Received block {block_num} with size: {}", data.len());
MaybeAsync::ready(FileAction::Continue)
}
fn aborted(&mut self, err: FileError) {
println!("rf: aborted: {err}");
}
fn completed(&mut self) {
println!("rf: completed");
}
}
// asynchronously start the transfer
self.association
.read_file(
".", // this reads the root "directory" file but doesn't parse it
FileReadConfig::default(),
Box::new(FileLogger),
None,
)
.await?;
// Callback interface
bool on_file_opened(uint32_t size, void *context)
{
printf("File opened - size: %u\n", size);
return true; // continue
}
bool on_file_block_received(uint32_t block_num, dnp3_byte_iterator_t *data, void *context)
{
uint32_t size = 0;
while (dnp3_byte_iterator_next(data)) {
++size;
}
printf("Received block %u with size: %u\n", block_num, size);
return true; // continue
}
void on_file_aborted(dnp3_file_error_t error, void *context) { printf("rf: aborted: %s\n", dnp3_file_error_to_string(error)); }
void on_file_completed(void *context) { printf("rf: completed\n"); }
dnp3_file_reader_t get_file_reader(void)
{
return (dnp3_file_reader_t){
.opened = &on_file_opened,
.block_received = &on_file_block_received,
.aborted = &on_file_aborted,
.completed = &on_file_completed,
.on_destroy = NULL,
.ctx = NULL,
};
}
// asynchronously start the operation
dnp3_param_error_t err = dnp3_master_channel_read_file(
channel,
association_id,
".", // this reads the root "directory" file but doesn't parse it
dnp3_file_read_config_defaults(),
get_file_reader()
);
// Callback interface
class FileReader : public dnp3::FileReader {
bool opened(uint32_t size) override
{
std::cout << "File opened - size: " << size << std::endl;
return true; // continue
}
bool block_received(uint32_t block_num, dnp3::ByteIterator &data) override
{
uint32_t size = 0;
while (data.next()) {
++size;
}
std::cout << "Received block " << block_num << " with size: " << size << std::endl;
return true; // continue
}
void aborted(dnp3::FileError error) override { std::cout << "rf: aborted: " << dnp3::to_string(error) << std::endl; }
void completed() override { std::cout << "rf: completed" << std::endl; }
};
std::unique_ptr<dnp3::FileReader> create_file_reader()
{
return std::make_unique<FileReader>();
}
// asynchronously start the operation
// this reads the root "directory" file but doesn't parse it
channel.read_file(assoc, ".", dnp3::FileReadConfig::defaults(), create_file_reader());
class FileLogger implements FileReader {
@Override
public boolean opened(UInteger size) {
System.out.println("File opened - size: " + size);
return true;
}
@Override
public boolean blockReceived(UInteger blockNum, List<UByte> data) {
System.out.println("Received block " + blockNum + " with size: " + data.size());
return true;
}
@Override
public void aborted(FileError error) {
System.out.println("rf: aborted: " + error);
}
@Override
public void completed() {
System.out.println("rf: completed");
}
}
// asynchronously start the operation
// this reads the root "directory" file but doesn't parse it
channel.readFile(association, ".", FileReadConfig.defaults(), new FileLogger());
class FileLogger : IFileReader
{
public bool Opened(uint size)
{
Console.WriteLine($"File opened - size: {size}");
return true;
}
public bool BlockReceived(uint blockNum, ICollection<byte> data)
{
Console.WriteLine($"Received block {blockNum} with size: {data.Count}");
return true;
}
public void Aborted(FileError error)
{
Console.WriteLine($"rf: aborted: {error}");
}
public void Completed()
{
Console.WriteLine("rf: completed");
}
}
// asynchronously start the operation
// this reads the root "directory" file but doesn't parse it
channel.ReadFile(association, ".", FileReadConfig.Defaults(), new FileLogger());
The master closes the file when a read completes, or when your FileReader aborts the transfer in opened. If the
transfer stops for any other reason, such as an abort in block_received, a timeout or an error, the file is left for the
outstation to clean up.
Read a directory
A directory listing in DNP3 is just a special kind of file with a special encoding. We implement this feature by starting
a background file transfer, accumulating the file data over possibly multiple blocks, and then parsing it into a collection
of FileInfo instances.
The master limits the size of a directory listing it accumulates. Raise the maximum size in DirReadConfig if listings are
large.
- Rust
- C
- C++
- Java
- C#
let items = self
.association
.read_directory(".", DirReadConfig::default(), None)
.await?;
for info in items {
print_file_info(info);
}
// Callback interface
void on_read_directory_complete(dnp3_file_info_iterator_t *iter, void *context)
{
dnp3_file_info_t *info = NULL;
while ((info = dnp3_file_info_iterator_next(iter))) {
print_file_info(*info);
}
printf("%s: success\n", (const char *)context);
}
// asynchronously start the operation
dnp3_read_directory_callback_t cb = {
.on_complete = &on_read_directory_complete,
.on_failure = &on_file_failure,
.on_destroy = NULL,
.ctx = "rd",
};
dnp3_param_error_t err = dnp3_master_channel_read_directory(channel, association_id, ".", dnp3_dir_read_config_defaults(), cb);
// Callback interface
class ReadDirectoryCallback : public dnp3::ReadDirectoryCallback {
public:
explicit ReadDirectoryCallback(const char *cmd) : cmd(cmd) {}
void on_complete(dnp3::FileInfoIterator &iter) override
{
while (iter.next()) {
print_file_info(iter.get());
}
std::cout << cmd << ": success" << std::endl;
}
void on_failure(dnp3::FileError error) override { std::cout << cmd << ": " << dnp3::to_string(error) << std::endl; }
private:
const char *cmd;
};
std::unique_ptr<dnp3::ReadDirectoryCallback> create_read_directory_callback(const char *cmd)
{
return std::make_unique<ReadDirectoryCallback>(cmd);
}
// asynchronously start the operation
channel.read_directory(assoc, ".", dnp3::DirReadConfig::defaults(), create_read_directory_callback("rd"));
List<FileInfo> items = channel
.readDirectory(association, ".", DirReadConfig.defaults())
.toCompletableFuture().get();
for (FileInfo info : items) {
printFileInfo(info);
}
var items = await channel.ReadDirectory(association, ".", DirReadConfig.Defaults());
foreach (var info in items)
{
PrintFileInfo(info);
}
Write a file
Writing a file opens it for writing, writes its data one block at a time, and closes it. The last block is marked as the
last one, so that the outstation knows the file is complete. The examples write two lines of text to hello_world.txt.
- Rust
- C
- C++
- Java
- C#
let line = "hello world\n".as_bytes();
let file = self
.association
.open_file(
"hello_world.txt",
AuthKey::none(),
Permissions::default(),
0xFFFFFFFF, // indicate that we don't know the size
FileMode::Write,
512,
)
.await?;
// write the 'hello world' line to the file twice
let mut block = BlockNumber::default();
self.association
.write_file_block(file.file_handle, block, line.to_vec())
.await?;
block.increment()?;
block.set_last();
self.association
.write_file_block(file.file_handle, block, line.to_vec())
.await?;
self.association.close_file(file.file_handle).await?;
// what each step of the write needs: each step's callback owns a copy and frees it
typedef struct write_file_t {
dnp3_master_channel_t *channel;
dnp3_association_id_t association_id;
uint32_t handle;
} write_file_t;
write_file_t *new_write_file(dnp3_master_channel_t *channel, dnp3_association_id_t association_id, uint32_t handle)
{
write_file_t *state = malloc(sizeof(write_file_t));
if (state == NULL) {
printf("wf: out of memory\n");
return NULL;
}
state->channel = channel;
state->association_id = association_id;
state->handle = handle;
return state;
}
void on_write_file_failure(dnp3_file_error_t error, void *context) { printf("wf: %s\n", dnp3_file_error_to_string(error)); }
void on_write_file_closed(dnp3_nothing_t nothing, void *context) { printf("wf: success\n"); }
void print_write_file_error(dnp3_param_error_t err)
{
if (err) {
printf("wf: %s\n", dnp3_param_error_to_string(err));
}
}
void on_last_block_written(dnp3_nothing_t nothing, void *context)
{
write_file_t *state = (write_file_t *)context;
dnp3_file_operation_callback_t cb = {
.on_complete = &on_write_file_closed,
.on_failure = &on_write_file_failure,
.on_destroy = NULL,
.ctx = NULL,
};
print_write_file_error(dnp3_master_channel_close_file(state->channel, state->association_id, state->handle, cb));
}
// write the 'hello world' line as one block of the file
void write_hello_world_block(write_file_t *state, uint32_t block, bool last, void (*on_written)(dnp3_nothing_t, void *))
{
write_file_t *next = new_write_file(state->channel, state->association_id, state->handle);
if (next == NULL) {
return;
}
const char *line = "hello world\n";
dnp3_byte_collection_t *data = dnp3_byte_collection_create((uint32_t)strlen(line));
for (size_t i = 0; i < strlen(line); ++i) {
dnp3_byte_collection_add(data, (uint8_t)line[i]);
}
dnp3_file_operation_callback_t cb = {
.on_complete = on_written,
.on_failure = &on_write_file_failure,
.on_destroy = &free,
.ctx = next,
};
print_write_file_error(dnp3_master_channel_write_file_block(state->channel, state->association_id, state->handle, block, last, data, cb));
dnp3_byte_collection_destroy(data);
}
void on_first_block_written(dnp3_nothing_t nothing, void *context)
{
write_hello_world_block((write_file_t *)context, 1, true, &on_last_block_written);
}
void on_write_file_opened(dnp3_open_file_t file, void *context)
{
write_file_t *state = (write_file_t *)context;
state->handle = file.file_handle;
// write the 'hello world' line to the file twice
write_hello_world_block(state, 0, false, &on_first_block_written);
}
// open hello_world.txt for writing: the callbacks above write it and close it
void start_write_file(dnp3_master_channel_t *channel, dnp3_association_id_t association_id)
{
write_file_t *state = new_write_file(channel, association_id, 0);
if (state == NULL) {
return;
}
dnp3_file_open_callback_t cb = {
.on_complete = &on_write_file_opened,
.on_failure = &on_write_file_failure,
.on_destroy = &free,
.ctx = state,
};
dnp3_param_error_t err = dnp3_master_channel_open_file(
channel,
association_id,
"hello_world.txt",
0, // no authentication key
dnp3_permissions_none(),
0xFFFFFFFF, // indicate that we don't know the size
DNP3_FILE_MODE_WRITE,
512, // maximum block size
cb
);
print_write_file_error(err);
}
void write_hello_world_block(dnp3::MasterChannel &channel, dnp3::AssociationId assoc, uint32_t handle, uint32_t block);
// each step of the write starts the next one from its callback
class WriteFileClosed : public dnp3::FileOperationCallback {
void on_complete(dnp3::Nothing result) override { std::cout << "wf: success" << std::endl; }
void on_failure(dnp3::FileError error) override { std::cout << "wf: " << dnp3::to_string(error) << std::endl; }
};
class WriteFileBlockWritten : public dnp3::FileOperationCallback {
public:
WriteFileBlockWritten(dnp3::MasterChannel &channel, dnp3::AssociationId assoc, uint32_t handle, uint32_t block)
: channel(channel), assoc(assoc), handle(handle), block(block)
{
}
void on_complete(dnp3::Nothing result) override
{
try {
if (block == 0) {
write_hello_world_block(channel, assoc, handle, 1);
}
else {
channel.close_file(assoc, handle, std::make_unique<WriteFileClosed>());
}
}
catch (const std::exception &ex) {
std::cout << "wf: " << ex.what() << std::endl;
}
}
void on_failure(dnp3::FileError error) override { std::cout << "wf: " << dnp3::to_string(error) << std::endl; }
private:
dnp3::MasterChannel &channel;
dnp3::AssociationId assoc;
uint32_t handle;
uint32_t block;
};
class WriteFileOpened : public dnp3::FileOpenCallback {
public:
WriteFileOpened(dnp3::MasterChannel &channel, dnp3::AssociationId assoc) : channel(channel), assoc(assoc) {}
void on_complete(const dnp3::OpenFile &file) override
{
try {
write_hello_world_block(channel, assoc, file.file_handle, 0);
}
catch (const std::exception &ex) {
std::cout << "wf: " << ex.what() << std::endl;
}
}
void on_failure(dnp3::FileError error) override { std::cout << "wf: " << dnp3::to_string(error) << std::endl; }
private:
dnp3::MasterChannel &channel;
dnp3::AssociationId assoc;
};
// write the 'hello world' line as block 0, then again as block 1, the last one
void write_hello_world_block(dnp3::MasterChannel &channel, dnp3::AssociationId assoc, uint32_t handle, uint32_t block)
{
const std::string line = "hello world\n";
const std::vector<uint8_t> data(line.begin(), line.end());
channel.write_file_block(assoc, handle, block, block == 1, data, std::make_unique<WriteFileBlockWritten>(channel, assoc, handle, block));
}
void start_write_file(dnp3::MasterChannel &channel, dnp3::AssociationId assoc)
{
channel.open_file(
assoc,
"hello_world.txt",
0, // no authentication key
dnp3::Permissions::none(),
0xFFFFFFFF, // indicate that we don't know the size
dnp3::FileMode::write,
512, // maximum block size
std::make_unique<WriteFileOpened>(channel, assoc)
);
}
OpenFile file = channel.openFile(
association,
"hello_world.txt",
uint(0), // no authentication key
Permissions.none(),
uint(0xFFFFFFFFL), // indicate that we don't know the size
FileMode.WRITE,
ushort(512)
).toCompletableFuture().get();
List<UByte> line = new ArrayList<>();
for (byte b : "hello world\n".getBytes(StandardCharsets.UTF_8)) {
line.add(ubyte(b));
}
// write the 'hello world' line to the file twice
channel.writeFileBlock(association, file.fileHandle, uint(0), false, line).toCompletableFuture().get();
channel.writeFileBlock(association, file.fileHandle, uint(1), true, line).toCompletableFuture().get();
channel.closeFile(association, file.fileHandle).toCompletableFuture().get();
var line = System.Text.Encoding.UTF8.GetBytes("hello world\n");
var file = await channel.OpenFile(
association,
"hello_world.txt",
0, // no authentication key
Permissions.None(),
0xFFFFFFFF, // indicate that we don't know the size
FileMode.Write,
512
);
// write the 'hello world' line to the file twice
await channel.WriteFileBlock(association, file.FileHandle, 0, false, line);
await channel.WriteFileBlock(association, file.FileHandle, 1, true, line);
await channel.CloseFile(association, file.FileHandle);