FTPSclientOPTA is a general-purpose FTPS client library for Arduino Opta devices.
The goal is to provide a reusable, Opta-first FTPS library built on the board's Ethernet and Mbed networking stack, with initial validation targeted against a WD My Cloud PR4100, vsftpd, and FileZilla Server, and designed to support other standards-compliant FTPS servers over time.
Note
This library has been validated on real Arduino Opta hardware with all core operations (connect, mkdir, upload, size, download, quit) passing against a pyftpdlib FTPS server. Treat it as early-release — broader server interoperability testing is still in progress.
- Provide a small Arduino-style FTPS client for Arduino Opta.
- Ship Explicit FTPS (
AUTH TLS) over Ethernet first. - Support protected passive-mode transfers (
PBSZ 0,PROT P) for upload and download. - Keep certificate handling practical for field deployments.
- Keep the session and transport model flexible enough to add Implicit FTPS and optional active mode later.
- Stay general enough to support servers beyond the initial reference NAS.
- Explicit FTPS only
- Passive mode only
- Binary transfers only
- Upload and download primitives
- Remote directory creation for nested application paths
- Remote size preflight for variable-size downloads
- SHA-256 fingerprint pinning
- Imported PEM certificate trust
- FTPS-specific error reporting suitable for field diagnostics
- Implicit FTPS
- Active mode
- Recursive sync
- Delete, rename, or broader file-management helpers beyond the current integration set
- Product-specific UI, config storage, or backup policy
- Broad multi-board support claims before hardware validation
- Explicit and Implicit FTPS modes under a common client API
- Optional active-mode transfers in addition to passive mode
- Capability discovery and directory helpers such as
FEAT,PWD,MLSD, andNLST - File-management helpers such as
DELE,RNFR/RNTO, andRMD - Stream-oriented transfer APIs in addition to buffer-based helpers
- Broader certificate and trust-management options
- Expanded interoperability testing across more FTPS servers
- Possible support for additional Mbed-based Arduino targets after Opta support is stable
Active mode is worth keeping on the v2 backlog, but it should stay behind Implicit FTPS and passive-mode hardening. It adds firewall and NAT complexity, and it is less likely to matter for the initial Opta-to-NAS deployment patterns this library is targeting.
- Arduino Opta only for v1
- Intended minimum core:
arduino:mbed_opta4.5.0 or later - Initial network path: Ethernet via
PortentaEthernetandEthernet
The v1 validation targets are:
- WD My Cloud PR4100 — NAS with built-in FTPS support
- vsftpd — widely deployed Linux FTPS server
- FileZilla Server — cross-platform FTPS server with GUI management
These are reference servers, not a guarantee of universal FTPS compatibility. Broader compatibility claims should only be made after real interop testing.
When the first implementation lands, the library is expected to rely on:
- Arduino Opta board support package
PortentaEthernetandEthernet- Mbed networking interfaces exposed by the Opta core, including
NetworkInterface,TCPSocket, andTLSSocketWrapper
FTPSclientOPTA is structured for Arduino Library Manager distribution.
Until the indexer has picked up the latest tagged release, you can also install it as a local library checkout.
The public API is intentionally small and Arduino-style. The current implementation direction looks like this:
// Illustrative usage.
FtpsServerConfig config;
config.host = "192.168.1.100";
config.port = 21;
config.user = "user";
config.password = "pass";
config.tlsServerName = "nas.local";
config.trustMode = FtpsTrustMode::Fingerprint;
config.fingerprint = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
// Current implementation is fail-closed and requires certificate validation.
config.validateServerCert = true;
FtpsClient client;
char error[96];
if (!client.begin(Ethernet.getNetwork(), error, sizeof(error))) {
Serial.println(error);
return;
}
if (client.connect(config, error, sizeof(error))) {
client.mkd("/backup", error, sizeof(error));
const uint8_t payload[] = "{}";
client.store(
"/backup/config.json",
payload,
sizeof(payload) - 1,
error,
sizeof(error));
size_t remoteBytes = 0;
client.size(
"/backup/config.json",
remoteBytes,
error,
sizeof(error));
uint8_t buffer[64] = {};
size_t bytesRead = 0;
client.retrieve(
"/backup/config.json",
buffer,
sizeof(buffer),
bytesRead,
error,
sizeof(error));
client.quit();
}The public API surface is begin(), connect(), setTraceCallback(), lastPhase(), mkd(), size(), list(), dele(), rmd(), rename(), store(), retrieve(), quit(), lastError(), and lastNsapiError(). begin() initializes the transport layer with the Mbed NetworkInterface and must be called once before connect(). lastError() returns an FtpsError enum for programmatic error handling alongside the human-readable char* error buffer. lastNsapiError() exposes the most recent socket-layer code (for example -3005 when LWIP socket-pool pressure causes a transient data-channel open failure). setTraceCallback() registers an optional callback invoked at each protocol phase for diagnostics or watchdog integration.
FtpsErrors.h also provides two sketch-level classifier helpers used by multi-file retry loops:
ftpsIsSessionDead(err)ftpsIsTransferRetriable(err, nsapiCode)
These helpers standardize error classification across projects while keeping retry/backoff policy in the application layer.
The v1 public config intentionally does not expose securityMode or passiveMode toggles. Until additional modes are implemented, the library surface is fixed to Explicit FTPS plus protected passive transfers so sketches cannot accidentally rely on unsupported options.
Static IP vs DHCP: On some Opta boards and network configurations, Ethernet.begin(mac) (DHCP) can hang or take a very long time. If your sketch appears stuck at Ethernet init, use a static IP instead:
IPAddress ip(192, 168, 1, 50);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
Ethernet.begin(mac, ip, dns, gateway, subnet);Watchdog and long operations: TLS handshakes on the Opta can take several seconds each. If your application uses a hardware watchdog, kick it from the trace callback to prevent resets during connect/transfer:
#include <mbed.h>
static void ftpsTraceCallback(const char *phase) {
Watchdog::get_instance().kick();
Serial.print("[FTPS] ");
Serial.println(phase);
}
ftps.setTraceCallback(ftpsTraceCallback);For general Arduino applications that need multi-file backup, restore, manifest, or archive workflows on top of this library, the next items are:
- broader server interoperability testing (FileZilla Server, vsftpd, WD My Cloud PR4100)
- documentation for buffer-owned transfer semantics, variable-size download guidance, and the provisional one-client-at-a-time assumption
- streaming transfer APIs only if measured payload sizes exceed safe RAM ceilings for the supported Opta target
These requirements are tracked as integration follow-up work, not as product-specific behavior inside the library.
- SHA-256 leaf-certificate fingerprint pinning
- Imported PEM certificate trust
- Certificate validation required (fail-closed)
- No silent fallback from FTPS to plaintext FTP
The repository currently includes:
examples/BasicUpload/BasicUpload.inoexamples/BasicDownload/BasicDownload.inoexamples/RetryUpload/RetryUpload.inoexamples/FileZillaLiveTest/FileZillaLiveTest.inoexamples/PyftpdlibLiveTest/PyftpdlibLiveTest.inoexamples/WDMyCloudLiveTest/WDMyCloudLiveTest.inoexamples/WebHarnessLiveTest/WebHarnessLiveTest.inoexamples/WebFileManagerLiveTest/WebFileManagerLiveTest.inoFtpsSpikeTest/FtpsSpikeTest.ino
The upload/download examples and the FileZilla live-test sketch emit structured serial diagnostics so Opta-side runs are easy to inspect in the serial monitor. The PyftpdlibLiveTest example bundles a self-contained Python FTPS server (ftps_server.py) and certificate generator (gen_cert.py) so you can test the library without installing any third-party server software — just pip install pyftpdlib pyOpenSSL and run the scripts. The WDMyCloudLiveTest example targets WD My Cloud NAS devices running My Cloud OS 5 and includes a setup guide for enabling FTPS on the NAS and extracting the certificate fingerprint. The web harness sketch hosts a lightweight LAN page for updating FTPS settings and running small connect/upload/download/quit tests without reflashing, uses token-gated actions behind a small passcode unlock step, and supports downloadable plain-text test reports. The web file manager sketch builds on that harness and adds authenticated /fs routes for remote list, mkdir, delete, move, and demo copy workflows (copy remains buffer-limited in the example). On arduino:mbed_opta 4.5.0, local directory listing is currently disabled in this example build because <dirent.h> is not available in the shipped toolchain.
Planned follow-up examples after broader transport/client validation lands:
- Trust-mode-focused fingerprint validation example
- Trust-mode-focused imported PEM validation example
- PR4100 reference setup
- vsftpd reference setup
- Additional FileZilla Server reference variants
The library has been validated on real Arduino Opta hardware:
- All core operations pass: connect, mkd, store, size, retrieve, quit
- SHA-256 fingerprint pinning verified on-device
- TLS session reuse hinting implemented for data-channel handshakes
- Socket lifecycle hardened: ordered tear-down (TCP close → TLS delete
→ TCP delete) prevents Mbed OS hard faults and the historical
delete tlshang - TLS close timeouts and
SO_LINGER(where supported) prevent indefinite hangs during shutdown - Multi-file backup verified end-to-end against a pyftpdlib FTPS server: 8 uploads, 0 failed in a single session (2026-04-17)
- Web file manager live validation passed on real Opta against local
pyftpdlib FTPS (
2026-04-18): connect, remote list, upload, download, copy, move, delete, auth-gate enforcement, bad-fingerprint rejection, bad-password rejection, and reconnect-after-server-restart - Trace callback support for diagnostics and watchdog integration,
including new
xport:cleanup:*,xport:linger-*, andxport:open-failed:*/xport:connect-failed:*traces for fine-grained socket-lifecycle observability
Remaining work:
- Broader server interoperability testing (FileZilla Server, vsftpd, WD My Cloud PR4100)
- Imported PEM certificate trust validation on hardware
- Broader example compile validation across the included sketches
- Arduino Opta is the only supported board for v1
- Explicit FTPS only
- Passive mode only
- Buffer-based transfers only (no streaming API yet)
- Compatibility beyond pyftpdlib is not yet validated on hardware
- Servers that enforce strict TLS session reuse may require additional transport work
- DHCP may hang on some Opta/network configurations; static IP is recommended
examples/WebFileManagerLiveTestuses safer placeholder defaults and requires explicit runtime configuration for host credentials and fingerprint before live FTPS actions
The FTPS library itself is platform-neutral, but Arduino Opta integrators
should be aware of two hard limits in the LWIP build shipped inside the
precompiled libmbed.a archive:
- Socket pool size is fixed at 4.
MBED_CONF_LWIP_SOCKET_MAXandMBED_CONF_LWIP_TCP_SOCKET_MAXare baked into the precompiled archive, so editingvariants/OPTA/mbed_config.hhas no effect at link time. An Opta application can have at most ~4 simultaneous TCP PCBs across the entire device. SO_LINGERis not implemented.setsockopt(SO_LINGER, {l_onoff=1, l_linger=0})returnsNSAPI_ERROR_UNSUPPORTED(-3002). Every closed TCP socket therefore sits inTIME_WAITfor ~60 s instead of being hard-reset. The transport now tracesxport:linger-unsupported:-3002so this is visible at runtime.
For a single FTPS transfer (control + data) these limits are not visible. For multi-file backup workflows on Opta, the practical consequences are:
- An idle
EthernetServerconsumes 1 of the 4 PCB slots for the LISTEN socket. If the application also keeps an accepted browser session active, only 2 slots remain for FTPS — not enough for a control channel plus a fresh PASV data channel while the previous data socket is still inTIME_WAIT. - Pacing matters. Opening a new PASV data socket within ~60 s of
closing the previous one will fail with
NSAPI_ERROR_NO_SOCKET(-3005). The transport surfaces this viaxport:open-failed:-3005so integrators can distinguish pool exhaustion from network errors.
A recipe for working around both constraints in an application that also
hosts a web UI is documented in the integrating External App Server
project (see FTPS repository docs:
CODE REVIEW/FTPS_RETRY_PROPOSALS_SUMMARY_04172026.md
and CODE REVIEW/SOCKET_CLOSE_HANG_ANALYSIS_04172026.md).
- Implementation note
- Implementation checklist
- Application integration requirements
- Hardware and follow-up checklist
- Repository creation plan
- Phase 0 spike plan
- Serial monitor output guide
- Web harness API reference
This repository uses the CC0 1.0 license. See LICENSE.