I ran two different tests, one using the Photon (running Device OS 1.1.1) as the UDP server (receives packets) and an the Argon (running Device OS 1.3.0-rc.1) as the UDP client (transmitting packets), and another in the reverse direction.
In addition to being on the same Wi-Fi network, the device’s D2 pins and GND were connected.
When the transmitter was about to UDP.sendPacket, it would asset D2 high, transmit, the drop D2.
The receiver monitors the time D2 was asserted, and the latency until the time the packet was received.
In my test, the packet was always 64 bytes, with the first 4 bytes being a sequence number, so I know if packets were lost.
Packets were sent every 100 milliseconds (10 per second, or 100 per 10 second bucket).
The client uses publish and subscribe to find the server address.
The server aggregates the data over 10 second periods and reports the minimum, mean, maximum latency, and also the count of packets and lost packets.
Server Code
#include "Particle.h"
SYSTEM_THREAD(ENABLED);
SerialLogHandler logHandler(LOG_LEVEL_INFO, {
{ "comm", LOG_LEVEL_ERROR } // Don't log comm INFO or TRACE messages
});
const int UDP_PORT = 5123;
const int SIGNAL_PIN = D2;
const unsigned long REPORT_PERIOD_MS = 10000;
void interruptHandler(void);
void subscribeHandler(const char *event, const char *data);
UDP udp;
bool wifiReady = false;
unsigned long signalTime = 0;
uint8_t buffer[512];
bool publishAddr = false;
uint32_t lastCounter = 0;
unsigned long minLatency = ULONG_MAX;
unsigned long maxLatency = 0;
unsigned long totalLatency = 0;
size_t samples = 0;
size_t lost = 0;
size_t maxPerLoop = 0;
unsigned long lastReport = 0;
void setup() {
pinMode(SIGNAL_PIN, INPUT_PULLDOWN);
attachInterrupt(SIGNAL_PIN, interruptHandler, RISING);
Particle.subscribe("udpLatencyReq", subscribeHandler, MY_DEVICES);
}
void loop() {
if (WiFi.ready()) {
if (!wifiReady) {
wifiReady = true;
udp.begin(UDP_PORT);
}
for(size_t numPerLoop = 1; ; numPerLoop++) {
int count = udp.receivePacket(buffer, sizeof(buffer));
if (count <= 0) {
break;
}
if (signalTime) {
unsigned long latency = millis() - signalTime;
uint32_t counter = *(uint32_t *)buffer;
// Log.info("received packet latency=%u ms, size=%d, counter=%lu", latency, count, counter);
if (lastCounter != 0) {
lost += (counter - lastCounter - 1);
}
lastCounter = counter;
if (latency < minLatency) {
minLatency = latency;
}
if (latency > maxLatency) {
maxLatency = latency;
}
totalLatency += latency;
samples++;
if (numPerLoop > maxPerLoop) {
maxPerLoop = numPerLoop;
}
}
else {
Log.info("received packet, no timing information available");
}
}
if (millis() - lastReport >= REPORT_PERIOD_MS) {
lastReport = millis();
if (samples) {
Log.info("latency min=%lu mean=%lu max=%lu samples=%u lost=%u maxPerLoop=%u", minLatency, (totalLatency / samples), maxLatency, samples, lost, maxPerLoop);
}
// Optionally comment this block out to make the results cumulative instead of bucketing for each report period
minLatency = ULONG_MAX;
maxLatency = 0;
totalLatency = 0;
samples = 0;
lost = 0;
maxPerLoop = 0;
}
if (publishAddr) {
publishAddr = false;
char buf[128];
snprintf(buf, sizeof(buf), "%s:%d", WiFi.localIP().toString().c_str(), UDP_PORT);
Particle.publish("udpLatencyResp", buf, PRIVATE);
Log.info("sending address %s", buf);
}
}
else {
if (wifiReady) {
wifiReady = false;
}
}
}
void interruptHandler(void) {
signalTime = millis();
}
void subscribeHandler(const char *event, const char *data) {
publishAddr = true;
}
Client Code
#include "Particle.h"
SYSTEM_THREAD(ENABLED);
SerialLogHandler logHandler(LOG_LEVEL_INFO, {
{ "comm", LOG_LEVEL_ERROR } // Don't log comm INFO or TRACE messages
});
const int SIGNAL_PIN = D2;
const unsigned long SEND_PERIOD_MS = 100;
const size_t SEND_SIZE = 64; // Must be at least 4
void subscribeHandler(const char *event, const char *data);
UDP udp;
bool wifiReady = false;
uint32_t counter = 0;
uint8_t buffer[512];
unsigned long lastSend = 0;
IPAddress serverAddr;
int serverPort = 0;
unsigned lastServerRequest = 0;
void setup() {
pinMode(SIGNAL_PIN, OUTPUT);
digitalWrite(SIGNAL_PIN, LOW);
Particle.subscribe("udpLatencyResp", subscribeHandler, MY_DEVICES);
}
void loop() {
if (WiFi.ready()) {
if (!wifiReady) {
wifiReady = true;
udp.begin(0);
}
if (!serverAddr) {
// Request server address by publish/subscribe
if (millis() - lastServerRequest > 2000) {
lastServerRequest = millis();
Particle.publish("udpLatencyReq", "", PRIVATE);
}
return;
}
if (millis() - lastSend >= SEND_PERIOD_MS) {
lastSend = millis();
counter++;
Log.info("sending counter=%lu", counter);
digitalWrite(SIGNAL_PIN, HIGH);
memcpy(buffer, (uint8_t *)&counter, sizeof(counter));
udp.sendPacket(buffer, SEND_SIZE, serverAddr, serverPort);
digitalWrite(SIGNAL_PIN, LOW);
}
}
else {
if (wifiReady) {
wifiReady = false;
}
}
}
void subscribeHandler(const char *event, const char *data) {
int a[4];
if (sscanf(data, "%d.%d.%d.%d:%d", &a[0], &a[1], &a[2], &a[3], &serverPort) == 5) {
serverAddr = IPAddress(a[0], a[1], a[2], a[3]);
Log.info("server address set to %s:%d", serverAddr.toString().c_str(), serverPort);
}
else {
Log.info("unknown server request %s", data);
}
}
Argon Client (transmitting), Photon Server (receiving)
This looks good.
0000140000 [app] INFO: latency min=6 mean=10 max=23 samples=100 lost=0 maxPerLoop=1
0000150000 [app] INFO: latency min=6 mean=11 max=89 samples=100 lost=0 maxPerLoop=1
0000160000 [app] INFO: latency min=7 mean=10 max=86 samples=100 lost=0 maxPerLoop=1
0000170000 [app] INFO: latency min=6 mean=11 max=49 samples=97 lost=3 maxPerLoop=1
0000180000 [app] INFO: latency min=6 mean=7 max=17 samples=99 lost=1 maxPerLoop=1
0000190000 [app] INFO: latency min=5 mean=8 max=74 samples=100 lost=0 maxPerLoop=1
0000200000 [app] INFO: latency min=6 mean=8 max=18 samples=100 lost=0 maxPerLoop=1
0000210000 [app] INFO: latency min=7 mean=8 max=34 samples=100 lost=0 maxPerLoop=1
0000220000 [app] INFO: latency min=6 mean=9 max=73 samples=100 lost=0 maxPerLoop=1
0000230000 [app] INFO: latency min=4 mean=7 max=38 samples=100 lost=0 maxPerLoop=1
0000240000 [app] INFO: latency min=6 mean=8 max=39 samples=100 lost=0 maxPerLoop=1
0000250000 [app] INFO: latency min=4 mean=8 max=18 samples=100 lost=0 maxPerLoop=1
0000260000 [app] INFO: latency min=5 mean=7 max=14 samples=100 lost=0 maxPerLoop=1
0000270000 [app] INFO: latency min=6 mean=8 max=40 samples=100 lost=0 maxPerLoop=1
0000280000 [app] INFO: latency min=4 mean=7 max=15 samples=100 lost=0 maxPerLoop=1
0000290000 [app] INFO: latency min=4 mean=9 max=66 samples=98 lost=0 maxPerLoop=1
0000300000 [app] INFO: latency min=4 mean=10 max=96 samples=102 lost=0 maxPerLoop=1
0000310000 [app] INFO: latency min=7 mean=8 max=18 samples=97 lost=3 maxPerLoop=1
0000320000 [app] INFO: latency min=5 mean=9 max=59 samples=100 lost=0 maxPerLoop=1
0000330000 [app] INFO: latency min=5 mean=7 max=12 samples=100 lost=0 maxPerLoop=1
0000340000 [app] INFO: latency min=5 mean=7 max=16 samples=100 lost=0 maxPerLoop=1
0000350000 [app] INFO: latency min=7 mean=8 max=37 samples=100 lost=0 maxPerLoop=1
Photon client (transmitting), Argon Server (receiving)
In this direction, the latency is much higher. Also, it may be higher than the log would indicate.
The maxPerLoop is the maximum number of packets we were able to read at once. Since we only transmit packets every 100 milliseconds, in theory this should always be 1, unless loop is delayed, or packets are queued before delivery.
In this direction, maxPerLoop is no longer 1, which means packets are getting bunched up.
Since the toggling of D2 can’t account for which packet time is being recorded, it could actually be greater than 100 milliseconds but it just can’t be measured this way.
0000020000 [app] INFO: latency min=26 mean=46 max=77 samples=100 lost=0 maxPerLoop=2
0000030000 [app] INFO: latency min=27 mean=45 max=81 samples=100 lost=0 maxPerLoop=2
0000040000 [app] INFO: latency min=31 mean=44 max=82 samples=98 lost=2 maxPerLoop=2
0000050000 [app] INFO: latency min=32 mean=52 max=86 samples=99 lost=0 maxPerLoop=2
0000060000 [app] INFO: latency min=38 mean=54 max=89 samples=101 lost=0 maxPerLoop=2
0000070000 [app] INFO: latency min=39 mean=58 max=100 samples=99 lost=0 maxPerLoop=4
0000080000 [app] INFO: latency min=1 mean=51 max=100 samples=100 lost=0 maxPerLoop=2
0000090000 [app] INFO: latency min=1 mean=37 max=54 samples=100 lost=0 maxPerLoop=2
0000100000 [app] INFO: latency min=4 mean=31 max=57 samples=97 lost=3 maxPerLoop=3
0000110000 [app] INFO: latency min=7 mean=32 max=68 samples=99 lost=1 maxPerLoop=3
0000120000 [app] INFO: latency min=20 mean=46 max=76 samples=100 lost=0 maxPerLoop=3
0000130000 [app] INFO: latency min=26 mean=48 max=78 samples=101 lost=0 maxPerLoop=4
0000140000 [app] INFO: latency min=28 mean=45 max=78 samples=99 lost=0 maxPerLoop=4
0000150000 [app] INFO: latency min=30 mean=48 max=86 samples=101 lost=0 maxPerLoop=2
0000160000 [app] INFO: latency min=36 mean=56 max=88 samples=99 lost=0 maxPerLoop=2
0000170000 [app] INFO: latency min=38 mean=57 max=91 samples=101 lost=0 maxPerLoop=2
0000180000 [app] INFO: latency min=41 mean=54 max=91 samples=100 lost=0 maxPerLoop=2
0000190000 [app] INFO: latency min=41 mean=54 max=92 samples=99 lost=0 maxPerLoop=2
0000200000 [app] INFO: latency min=42 mean=61 max=98 samples=100 lost=0 maxPerLoop=2
0000210000 [app] INFO: latency min=48 mean=64 max=100 samples=100 lost=0 maxPerLoop=4
0000220000 [app] INFO: latency min=50 mean=62 max=100 samples=99 lost=1 maxPerLoop=3
0000230000 [app] INFO: latency min=1 mean=37 max=53 samples=100 lost=0 maxPerLoop=2
0000240000 [app] INFO: latency min=3 mean=38 max=58 samples=101 lost=0 maxPerLoop=5
0000250000 [app] INFO: latency min=8 mean=31 max=62 samples=99 lost=0 maxPerLoop=3
Photon client (transmitting), Argon Server (receiving) (1 second)
To find the actual latency I decreased the transmission speed in the client to 1 per second. From this, I can now see that the mean latency is sometimes > 100 milliseconds, and reached as high as 252 milliseconds.
This would explain the bunching up of the packets where maxPerLoop was > 1 in the previous test.
0000047480 [app] INFO: latency min=43 mean=94 max=145 samples=10 lost=0 maxPerLoop=1
0000057480 [app] INFO: latency min=47 mean=98 max=149 samples=10 lost=0 maxPerLoop=1
0000067480 [app] INFO: latency min=48 mean=88 max=148 samples=10 lost=0 maxPerLoop=1
0000077480 [app] INFO: latency min=50 mean=90 max=151 samples=10 lost=0 maxPerLoop=1
0000087480 [app] INFO: latency min=51 mean=111 max=252 samples=10 lost=0 maxPerLoop=1
0000097480 [app] INFO: latency min=53 mean=83 max=153 samples=10 lost=0 maxPerLoop=1
0000107480 [app] INFO: latency min=53 mean=84 max=106 samples=10 lost=0 maxPerLoop=1
0000117480 [app] INFO: latency min=56 mean=91 max=156 samples=10 lost=0 maxPerLoop=1
0000127480 [app] INFO: latency min=7 mean=82 max=156 samples=10 lost=0 maxPerLoop=1
0000137480 [app] INFO: latency min=59 mean=89 max=159 samples=10 lost=0 maxPerLoop=1
0000147480 [app] INFO: latency min=59 mean=109 max=160 samples=10 lost=0 maxPerLoop=1
0000157480 [app] INFO: latency min=60 mean=105 max=210 samples=10 lost=0 maxPerLoop=1
0000167480 [app] INFO: latency min=60 mean=86 max=113 samples=10 lost=0 maxPerLoop=1
0000177480 [app] INFO: latency min=64 mean=103 max=214 samples=10 lost=0 maxPerLoop=1
0000187480 [app] INFO: latency min=64 mean=94 max=116 samples=10 lost=0 maxPerLoop=1
0000197480 [app] INFO: latency min=22 mean=82 max=121 samples=10 lost=0 maxPerLoop=1
0000207480 [app] INFO: latency min=72 mean=93 max=123 samples=10 lost=0 maxPerLoop=1
0000217480 [app] INFO: latency min=28 mean=91 max=127 samples=10 lost=0 maxPerLoop=1
0000227480 [app] INFO: latency min=27 mean=93 max=132 samples=10 lost=0 maxPerLoop=1
0000237480 [app] INFO: latency min=32 mean=92 max=133 samples=10 lost=0 maxPerLoop=1
0000247480 [app] INFO: latency min=34 mean=93 max=133 samples=10 lost=0 maxPerLoop=1
0000257480 [app] INFO: latency min=35 mean=85 max=135 samples=10 lost=0 maxPerLoop=1
Summary
It does appear that when receiving UDP the Argon does have much higher latency that the Photon does. I do not know why at this time.