Skip to content

Commit 596b379

Browse files
davidbuzzclaude
andcommitted
SITL: optional shared-memory transport for ride-along JSON
When a ride-along master (--slave N) and its JSON slaves are all started with --cluster, the fdm/servo exchange runs over the cluster shared-memory segment instead of the UDP sockets; without --cluster the UDP transport remains exactly as before. The wire content is unchanged in both directions - the master's JSON line and the slave's binary servo packet are byte-identical over either transport - so the two paths are directly comparable. Each instance's 4096-byte payload slot is partitioned: SwarmInfo keeps offset 0, and bytes 512+ become a seq-numbered one-way channel (the master publishes fdm in its own slot, each slave publishes servos in its own slot; readers poll seq for change, and the per-slot robust mutex makes every read internally consistent). write_payload() and read_payload() gain a default-0 offset argument to address the partition. The master now also reports its achieved exchange rate every 2s of wall time over either transport, so the two can be A/B compared: measured master+1-slave on one host, UDP averaged 11.6k frames/s (peak 13.9k) and shared memory 15.0k frames/s (peak 20.6k), ~30% faster on average with ~50% higher peaks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3bd5661 commit 596b379

7 files changed

Lines changed: 314 additions & 22 deletions

File tree

libraries/AP_HAL_SITL/SITL_SharedMem.cpp

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -704,20 +704,20 @@ uint8_t AP_SITL_SharedMem::get_instance_count() const
704704
mutex. EOWNERDEAD (a prior crashed run reusing this slot) is marked
705705
consistent and we proceed, since we are the sole writer for our slot.
706706
*/
707-
void AP_SITL_SharedMem::write_payload(const void *data, uint32_t len)
707+
void AP_SITL_SharedMem::write_payload(const void *data, uint32_t len, uint32_t offset)
708708
{
709-
if (_data == nullptr) {
709+
if (_data == nullptr || offset >= AP_SITL_SHMEM_PAYLOAD_SIZE) {
710710
return;
711711
}
712-
if (len > AP_SITL_SHMEM_PAYLOAD_SIZE) {
713-
len = AP_SITL_SHMEM_PAYLOAD_SIZE;
712+
if (len > AP_SITL_SHMEM_PAYLOAD_SIZE - offset) {
713+
len = AP_SITL_SHMEM_PAYLOAD_SIZE - offset;
714714
}
715715
auto &slot = _data->instance[_instance_id];
716716

717717
#ifdef __CYGWIN__
718718
// no cross-process lock exists on this platform - see
719719
// _init_segment_contents(); the copy is unguarded
720-
memcpy(slot.payload, data, len);
720+
memcpy(&slot.payload[offset], data, len);
721721
#else
722722
int ret = pthread_mutex_lock(&slot.payload_mutex);
723723
#if AP_SITL_SHMEM_ROBUST_MUTEX
@@ -736,7 +736,7 @@ void AP_SITL_SharedMem::write_payload(const void *data, uint32_t len)
736736
return;
737737
}
738738

739-
memcpy(slot.payload, data, len);
739+
memcpy(&slot.payload[offset], data, len);
740740

741741
pthread_mutex_unlock(&slot.payload_mutex);
742742
#endif // __CYGWIN__
@@ -747,20 +747,21 @@ void AP_SITL_SharedMem::write_payload(const void *data, uint32_t len)
747747
mutex. EOWNERDEAD means the peer died mid-write; mark it consistent
748748
but treat the data as unreliable and return false.
749749
*/
750-
bool AP_SITL_SharedMem::read_payload(uint8_t instance_id, void *data, uint32_t len) const
750+
bool AP_SITL_SharedMem::read_payload(uint8_t instance_id, void *data, uint32_t len, uint32_t offset) const
751751
{
752-
if (_data == nullptr || instance_id >= AP_SITL_SHMEM_MAX_INSTANCES) {
752+
if (_data == nullptr || instance_id >= AP_SITL_SHMEM_MAX_INSTANCES ||
753+
offset >= AP_SITL_SHMEM_PAYLOAD_SIZE) {
753754
return false;
754755
}
755-
if (len > AP_SITL_SHMEM_PAYLOAD_SIZE) {
756-
len = AP_SITL_SHMEM_PAYLOAD_SIZE;
756+
if (len > AP_SITL_SHMEM_PAYLOAD_SIZE - offset) {
757+
len = AP_SITL_SHMEM_PAYLOAD_SIZE - offset;
757758
}
758759
auto &slot = _data->instance[instance_id];
759760

760761
#ifdef __CYGWIN__
761762
// no cross-process lock exists on this platform - see
762763
// _init_segment_contents(); the copy is unguarded
763-
memcpy(data, slot.payload, len);
764+
memcpy(data, &slot.payload[offset], len);
764765
return true;
765766
#else
766767
int ret = pthread_mutex_lock(&slot.payload_mutex);
@@ -782,7 +783,7 @@ bool AP_SITL_SharedMem::read_payload(uint8_t instance_id, void *data, uint32_t l
782783
}
783784

784785
if (!owner_died) {
785-
memcpy(data, slot.payload, len);
786+
memcpy(data, &slot.payload[offset], len);
786787
}
787788

788789
pthread_mutex_unlock(&slot.payload_mutex);

libraries/AP_HAL_SITL/SITL_SharedMem.h

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,39 @@
5757
// e.g. vehicle position/attitude or other user-defined telemetry.
5858
#define AP_SITL_SHMEM_PAYLOAD_SIZE 4096
5959

60+
/*
61+
the payload block is partitioned between its tenants at fixed offsets:
62+
[0, AP_SITL_SHMEM_RIDE_OFFSET) AP_SITL_SwarmInfo, published from
63+
Aircraft::sync_frame_time()
64+
[AP_SITL_SHMEM_RIDE_OFFSET, end) ride-along JSON transport channel,
65+
used by SIM_JSON_Master (master) and
66+
SIM_JSON (slave) instead of the UDP
67+
sockets when the ride-along pair was
68+
started with --cluster
69+
Both tenants are guarded by the same per-slot robust mutex.
70+
*/
71+
#define AP_SITL_SHMEM_RIDE_OFFSET 512
72+
#define AP_SITL_SHMEM_RIDE_SIZE (AP_SITL_SHMEM_PAYLOAD_SIZE - AP_SITL_SHMEM_RIDE_OFFSET)
73+
74+
/*
75+
one-directional message channel living in the ride-along region of a
76+
slot: the master publishes its fdm JSON line in its own slot, each
77+
slave publishes its servo packet in its own slot. A publish rewrites
78+
the whole record under the slot mutex and bumps seq last, so a single
79+
read_payload() of the channel always yields an internally-consistent
80+
record; readers poll seq for change.
81+
*/
82+
struct AP_SITL_RideAlongChannel {
83+
uint32_t magic; // which endpoint writes this slot's channel (below)
84+
uint32_t seq; // bumped once per publish
85+
uint32_t len; // valid bytes in data[]
86+
uint8_t data[AP_SITL_SHMEM_RIDE_SIZE - 12];
87+
};
88+
static_assert(sizeof(AP_SITL_RideAlongChannel) == AP_SITL_SHMEM_RIDE_SIZE,
89+
"ride-along channel must exactly fill its payload region");
90+
#define AP_SITL_RIDE_FDM_MAGIC 0x52464D31 // "RFM1": master -> slaves fdm JSON
91+
#define AP_SITL_RIDE_SERVO_MAGIC 0x52535631 // "RSV1": slave -> master servo packet
92+
6093
/*
6194
Robust process-shared mutexes make a peer that is killed mid-write hand
6295
the next locker EOWNERDEAD instead of deadlocking every other instance
@@ -153,18 +186,20 @@ class AP_SITL_SharedMem {
153186
write up to AP_SITL_SHMEM_PAYLOAD_SIZE bytes into this instance's
154187
payload block, for peers to read via read_payload(). Takes the
155188
slot's robust process-shared mutex for the duration of the copy so
156-
readers never observe a torn write.
189+
readers never observe a torn write. offset positions the write
190+
within the block, for tenants above the first (see the payload
191+
partition map by AP_SITL_SHMEM_RIDE_OFFSET).
157192
*/
158-
void write_payload(const void *data, uint32_t len);
193+
void write_payload(const void *data, uint32_t len, uint32_t offset = 0);
159194

160195
/*
161196
read the given instance's payload block into data (up to len bytes,
162-
capped at AP_SITL_SHMEM_PAYLOAD_SIZE). Takes the slot's robust
163-
process-shared mutex for the duration of the copy. Returns true on
164-
success, false if shm is not initialised or instance_id is out of
165-
range.
197+
capped at AP_SITL_SHMEM_PAYLOAD_SIZE, starting offset bytes into
198+
the block). Takes the slot's robust process-shared mutex for the
199+
duration of the copy. Returns true on success, false if shm is not
200+
initialised or instance_id is out of range.
166201
*/
167-
bool read_payload(uint8_t instance_id, void *data, uint32_t len) const;
202+
bool read_payload(uint8_t instance_id, void *data, uint32_t len, uint32_t offset = 0) const;
168203

169204
/*
170205
barrier sync: spin-wait until every peer that has joined the cluster

libraries/AP_HAL_SITL/SITL_cmdline.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ void SITL_State::_usage(void)
119119
"\t--sysid ID set MAV_SYSID\n"
120120
"\t--cluster ID join SITL cluster ID (0-255) for shared-memory\n"
121121
"\t clock sync with other instances; off unless given\n"
122-
"\t--slave number set the number of JSON slaves\n"
122+
"\t--slave number set the number of JSON ride-along slaves; with\n"
123+
"\t --cluster the fdm/servo exchange runs over the\n"
124+
"\t cluster's shared memory instead of UDP\n"
123125
"\t--use_sim_time <true|false> use ROS2 simulation clock for DDS topics. Defaults to false\n"
124126
);
125127
}

libraries/SITL/SIM_JSON.cpp

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
#include <AP_HAL/utility/replace.h>
3232
#include <SRV_Channel/SRV_Channel.h>
3333
#include <AP_Filesystem/AP_Filesystem.h>
34+
#include <stddef.h>
35+
#include <time.h>
36+
#include <unistd.h>
37+
38+
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
39+
#include <AP_HAL_SITL/HAL_SITL_Class.h>
40+
extern const HAL_SITL& hal_sitl;
41+
#endif
3442

3543
#define UDP_TIMEOUT_MS 100
3644

@@ -99,6 +107,98 @@ void JSON::set_interface_ports(const char* address, const int port_in, const int
99107
printf("JSON control interface set to %s:%u\n", target_ip, control_port);
100108
}
101109

110+
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
111+
static uint64_t ride_wall_micros(void)
112+
{
113+
struct timespec ts {};
114+
clock_gettime(CLOCK_MONOTONIC, &ts);
115+
return ts.tv_sec * 1000000ULL + ts.tv_nsec / 1000ULL;
116+
}
117+
118+
/*
119+
the ride-along exchange runs over the cluster's shared memory segment
120+
instead of the UDP sockets when this instance was started with
121+
--cluster (the master must be too)
122+
*/
123+
bool JSON::ride_shmem_active(void) const
124+
{
125+
return hal_sitl.get_sitl_state()->_shared_mem.is_initialised();
126+
}
127+
128+
// publish our servo packet in our own slot's ride-along channel
129+
void JSON::ride_shmem_send(const void *pkt, uint32_t len)
130+
{
131+
auto &shm = hal_sitl.get_sitl_state()->_shared_mem;
132+
static AP_SITL_RideAlongChannel ch;
133+
ch.magic = AP_SITL_RIDE_SERVO_MAGIC;
134+
ch.len = MIN(len, (uint32_t)sizeof(ch.data));
135+
memcpy(ch.data, pkt, ch.len);
136+
ch.seq = ++ride_out_seq;
137+
shm.write_payload(&ch, offsetof(AP_SITL_RideAlongChannel, data) + ch.len,
138+
AP_SITL_SHMEM_RIDE_OFFSET);
139+
}
140+
141+
/*
142+
poll the master's slot for a fresh fdm JSON line; mirrors
143+
sock.recv(buf, size, timeout_ms) semantics, returning 0 on timeout
144+
*/
145+
ssize_t JSON::ride_shmem_recv(uint8_t *buf, uint32_t size, uint32_t timeout_ms)
146+
{
147+
auto &shm = hal_sitl.get_sitl_state()->_shared_mem;
148+
const uint64_t deadline_us = ride_wall_micros() + timeout_ms * 1000ULL;
149+
uint32_t spins = 0;
150+
while (true) {
151+
if (ride_master_slot < 0) {
152+
// discover which slot carries the fdm channel (usually the
153+
// master's instance 0, but any slot with the magic works;
154+
// give each ride-along group its own --cluster id)
155+
for (uint8_t i = 0; i < AP_SITL_SHMEM_MAX_INSTANCES; i++) {
156+
AP_SITL_RideAlongChannel hdr;
157+
if (shm.instance_active(i) &&
158+
shm.read_payload(i, &hdr, offsetof(AP_SITL_RideAlongChannel, data),
159+
AP_SITL_SHMEM_RIDE_OFFSET) &&
160+
hdr.magic == AP_SITL_RIDE_FDM_MAGIC) {
161+
ride_master_slot = i;
162+
printf("JSON: ride-along fdm found in shared-memory slot %u\n", i);
163+
break;
164+
}
165+
}
166+
}
167+
if (ride_master_slot >= 0) {
168+
static AP_SITL_RideAlongChannel ch;
169+
if (shm.read_payload(ride_master_slot, &ch, sizeof(ch),
170+
AP_SITL_SHMEM_RIDE_OFFSET) &&
171+
ch.magic == AP_SITL_RIDE_FDM_MAGIC &&
172+
ch.seq != ride_in_seq) {
173+
ride_in_seq = ch.seq;
174+
const uint32_t n = MIN(ch.len, MIN((uint32_t)sizeof(ch.data), size));
175+
memcpy(buf, ch.data, n);
176+
return n;
177+
}
178+
}
179+
if (ride_wall_micros() >= deadline_us) {
180+
return 0;
181+
}
182+
if (++spins > 5000) {
183+
usleep(100);
184+
spins = 0;
185+
}
186+
}
187+
}
188+
#endif // CONFIG_HAL_BOARD == HAL_BOARD_SITL
189+
190+
ssize_t JSON::transport_recv(uint32_t timeout_ms)
191+
{
192+
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
193+
if (ride_shmem_active()) {
194+
return ride_shmem_recv(&sensor_buffer[sensor_buffer_len],
195+
sizeof(sensor_buffer)-sensor_buffer_len, timeout_ms);
196+
}
197+
#endif
198+
return sock.recv(&sensor_buffer[sensor_buffer_len],
199+
sizeof(sensor_buffer)-sensor_buffer_len, timeout_ms);
200+
}
201+
102202
/*
103203
Decode and send servos
104204
*/
@@ -114,6 +214,12 @@ void JSON::output_servos(const struct sitl_input &input)
114214
pkt.pwm[i] = input.servos[i];
115215
}
116216
pkt_size = sizeof(pkt);
217+
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
218+
if (ride_shmem_active()) {
219+
ride_shmem_send(&pkt, pkt_size);
220+
send_ret = pkt_size;
221+
} else
222+
#endif
117223
send_ret = sock.sendto(&pkt, pkt_size, target_ip, control_port);
118224
} else {
119225
servo_packet_16 pkt;
@@ -123,6 +229,12 @@ void JSON::output_servos(const struct sitl_input &input)
123229
pkt.pwm[i] = input.servos[i];
124230
}
125231
pkt_size = sizeof(pkt);
232+
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
233+
if (ride_shmem_active()) {
234+
ride_shmem_send(&pkt, pkt_size);
235+
send_ret = pkt_size;
236+
} else
237+
#endif
126238
send_ret = sock.sendto(&pkt, pkt_size, target_ip, control_port);
127239
}
128240

@@ -303,7 +415,7 @@ uint64_t JSON::parse_sensors(const char *json)
303415
void JSON::recv_fdm(const struct sitl_input &input)
304416
{
305417
// Receive sensor packet
306-
ssize_t ret = sock.recv(&sensor_buffer[sensor_buffer_len], sizeof(sensor_buffer)-sensor_buffer_len, UDP_TIMEOUT_MS);
418+
ssize_t ret = transport_recv(UDP_TIMEOUT_MS);
307419
uint32_t wait_ms = UDP_TIMEOUT_MS;
308420

309421
if (state.no_lockstep && ret <= 0) {
@@ -320,7 +432,7 @@ void JSON::recv_fdm(const struct sitl_input &input)
320432

321433
while (ret <= 0) {
322434
//printf("No JSON sensor message received - %s\n", strerror(errno));
323-
ret = sock.recv(&sensor_buffer[sensor_buffer_len], sizeof(sensor_buffer)-sensor_buffer_len, UDP_TIMEOUT_MS);
435+
ret = transport_recv(UDP_TIMEOUT_MS);
324436
wait_ms += UDP_TIMEOUT_MS;
325437
// if no sensor message is received after 10 second resend servos, this help cope with SITL and the physics getting out of sync
326438
if (wait_ms > 1000) {

libraries/SITL/SIM_JSON.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,24 @@ class JSON : public Aircraft {
7575
void output_servos(const struct sitl_input &input);
7676
void recv_fdm(const struct sitl_input &input);
7777

78+
// receive one chunk from whichever transport is active into
79+
// sensor_buffer (shared-memory ride-along channel when this
80+
// instance was started with --cluster, the UDP socket otherwise);
81+
// mirrors sock.recv() semantics, returning 0 on timeout
82+
ssize_t transport_recv(uint32_t timeout_ms);
83+
84+
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
85+
// ride-along over the cluster's shared memory segment, as an
86+
// alternative to the UDP sockets: see AP_SITL_RideAlongChannel in
87+
// SITL_SharedMem.h
88+
bool ride_shmem_active(void) const;
89+
void ride_shmem_send(const void *pkt, uint32_t len);
90+
ssize_t ride_shmem_recv(uint8_t *buf, uint32_t size, uint32_t timeout_ms);
91+
uint32_t ride_out_seq;
92+
uint32_t ride_in_seq;
93+
int8_t ride_master_slot = -1; // shmem slot the fdm channel was found in
94+
#endif
95+
7896
uint64_t parse_sensors(const char *json);
7997

8098
// buffer for parsing pose data in JSON format

0 commit comments

Comments
 (0)