Depends on how much you care about power use.
My application doesn’t have any power limitations and so the default idle state is listening for packets to receive. But receive mode is high power, if you care about battery life you can only listen for some of the time which makes things a lot trickier.
In my solution the code handling packet reception is common for all devices. Each packet contains a packet type and sender and target IDs. So the same code will handle receiving TWR start request or a TWR reply equally well.
When a correctly addressed packet is received the driver code automatically sends the appropriate type of reply packet and returns to receive mode as soon as transmission is complete.
This means that if a device wants to initiate a range all the application code needs to do is cancels the receive and transmit the TWR start packet. As soon as the packet is sent the system returns to it’s default receive mode. The remainder of the exchange will all be triggered by responses within the packet receive code without any user space code being involved.
When a range measurement completes it triggers the appropriate callback to the user code.
This structure means that all devices default to being responders except while they are actively transmitting.
This does open up all sorts of possible issues e.g. what if two devices try to range to the same target at the same time? What if tag A tries to range to tag B while tag B is trying to range to tag C? etc…
But as long as you can make sure that only one device is trying to initiate ranges at a time then you avoid all of these problems.
The only other possible issue is if you try to initiate two ranges from a tag with a spacing of less than the time a full TWR exchange takes. But that situation is not exactly rocket science to avoid.