-
Notifications
You must be signed in to change notification settings - Fork 9
Bluetooth / Radio communication between bots! #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Thanks for the pull request - I like the direction and, having thought about it for a couple of days, I think we could take it a couple of steps further! Note that everything below is a proposal, a bag of thoughts - feel free to comment, come up with counter-ideas, and so on. Overall, I tend for each peripheral to be somewhat different from each other not only in the tangible functionality, but also the intrinsic mechanic -- serial drops older bytes, compass auto-refreshes in the background etc. For Bluetooth (aka radio, and we'll get to the naming soon) the mechanic we could apply to make it intrinsically different from radar is to make it possible to send messages without specifying the radius. Rather, the message travels outwards from the bot and with every tick it travels, it wanes, increasing the chance for observing bit flip(s) within it. (in a similar spirit -- when two messages collide mid-air, both can get mangled, which touches on the electronic warfare subject you mentioned.) Practically speaking, instead of having
->
->
When a message "hits" a bot, that's when it's pushed onto the radio's "message stack" (if there's space for it), with some of the bits mangled (maybe 1 bit for each 1 traversed tile? though it would certainly make much more sense to apply inverse square law here so that messages received in close proximity have almost 100% chance of being correct). NamingI'm feeling radio more than Bluetooth - that's because Bluetooth kinda implies a high-level thing going on (with device discovery etc. provided by the protocol), while what we'd like to offer is more of a "send a bag of bytes and receive a bag of bytes" functionality. In particular, I'd like to incentivize players to implement error recovery algorithms (Hamming code and whatnot) and higher-level protocols on top of the basic tools provided for the game. InterfaceAs with everything in here, the interface here is also a proposal - a sketched version of my thoughts is: // N.B. we could assume that radio is always turned on, but at some point I'd
// like to revisit the concept of implementing a battery - turning the
// radio on/off could be a battery-saving strategy then
pub fn radio_on() {
// N.B. we're reserving 0x01 as a general "set radio power" command - in the
// future we could have a "set_radio_power()" function that would cause
// the radio to eat more battery in exchange for allowing it to send
// messages for longer distances (and vice versa - reduce the chance of
// observing a bit flip in a retrieved message)
//
// this would be then implemented as cmd(0x01, power, 0x00, 0x00)
wri(MEM_RADIO, 0, cmd(0x01, 0x01, 0x00, 0x00));
}
pub fn radio_off() {
wri(MEM_RADIO, 0, cmd(0x01, 0x00, 0x00, 0x00));
}
pub fn radio_status() -> u32 {
rdi(MEM_RADIO, 0)
}
pub fn is_radio_on() -> bool {
radio_status() & 1 > 0
}
// aka "can we send the next packet"
pub fn can_radio_send() -> bool {
radio_status() & 2 > 0
}
// aka "is there any packet waiting for us in the queue"
pub fn can_radio_recv() -> bool {
radio_status() & 4 > 0
}
pub fn radio_send(msg: &[u8; 32]) {
for (idx, payload) in msg.array_windows().enumerate() {
wri(MEM_RADIO, idx + 1, u32::from_le_bytes(payload));
}
wri(MEM_RADIO, 0, cmd(0x02, 0x00, 0x00, 0x00));
}
// if there's no packet, just fills msg with zeros
pub fn radio_recv(msg: &mut [u8; 32]) {
for (idx, payload) in msg.array_windows_mut().enumerate() {
*msg = rdi(MEM_RADIO, idx + 1).to_le_bytes();
}
}
// Shiny thing! Allows to specify a predicate for radio_recv().
//
// E.g. calling `radio_filter(&[0xca, 0xfe, 0xba, 0xbe])` will cause the radio
// to transparently ignore all incoming messages which have a different prefix;
// doesn't affect `radio_send()`.
//
// This pairs up with `radio_send()` *not* allowing to send a message to a
// specific robot id -- rather, players are encouraged to come up with a
// "namespacing" scheme of their own.
//
// For instance - if someone wants to have a direct bot-to-bot connection, they
// can prefix their messages with the bot id (or rather a hash of it / half of
// it, since we purposefully keep the prefix shorter than a full bot id).
//
// Alternatively - if someone wants to form a bot-cluster and communicate within
// it instead of directly to a specific bot, they can just randomize a 32-bit
// number and use it to prefix the messages.
//
// Even more combinations exist, e.g. the prefix could encode a message type so
// that you can have different bots deployed on the field that respond respond
// only to their own specific message type (which is sort of a variation on the
// clustering mechanism above, now that I think about it).
pub fn radio_filter(prefix: &[u8; 4]) {
wri(MEM_RADIO, 0, cmd(0x03, 0x00, prefix[0], prefix[1]));
wri(MEM_RADIO, 0, cmd(0x03, 0x01, prefix[2], prefix[3]));
} TutorialI think the tutorial is plenty long as it is - instead of introducing more chapters (which could be frustrating for people who want to just grasp the basics), we could introduce a new challenge. I was thinking a distributed key-value database? 👀 You'd have to implement a couple of bots that together form a cluster that's able to respond to basic queries such as "put foo=bar" or "get foo" (those would also get transmitted to your robot over the radio from a built-in challenge-robot). You'd be forced to implement more than one robot simply because the challenge-robot would generate over 128 kB of random queries - so you would have to keep multiple robots working together, so that each robot contains its own "set" (shard?) of the database, otherwise you wouldn't be able to respond back to all of the queries asked by the challenge-bot. In the extreme version we could force the robots to fight with some enemies - this way you'd be forced to implement some kind of storage redundancy; but that's maybe for another day. Open questionsradio_filter() vs bitrotIf messages are applicable to this "distance bitrot", then it would make sense for each bit of the message to be equally likely to get flipped - and this can make it frustrating to use (i.e. the specific problem is that A non-realistic (I think?) but good-enough solution could be to penalize further bits more than the initial bits, something like: fn mangle_message(msg: &mut [Bit; 256], prob: f32) {
for (idx, bit) in msg.iter_mut().enumerate() {
let bit_prob = (*idx as f32 / 128.0).clamp(0.0, 1.0) * prob;
if rng.gen(bit_prob) {
*bit = bit.flip();
}
}
} Intuitively, when calling Message size vs cooldown vs challengeIf we're able to send just 32b of payload, then the whole "challenge requires you to handle hundredths of kilobytes of queries" part might take solid minutes to complete, mostly waiting for the queries to be sent/received (= no fun). On the other hand, forcing messages to be e.g. 128b can also be wasteful (those bytes need to be copied into MMIO, after all). Maybe a good compromise would be to allow to send messages of arbitrary length? (but more than 4b, to match the prefix, and less than 128b, not to overflow the receiving part) This would be sort of "pay as you go" mechanism: you want to send a short message? cool; you want to send a longer message? cool, but you have to wait a bit longer. Next stepsAlright, this turned out to be quite a message! Feel free to treat is as an RFC - nothing here is set in stone, it's just a couple of ideas. In terms of the implementation, nothing here shouts terribly difficult to me - if you'd want to give it a stab, I'd love to support and review! If you don't have much time or ain't feeling like implementing more at the moment, that's also fine, just lemme know. (but as I said, I think it's a nice, moderately challenging project that should be also quite rewarding; I've got a couple of blog posts to write myself, so I can't complain for lack of work at the moment either 😄) Implementation hintsAssuming that the basic idea here stands, we'd have to store messages into the world itself:
Something akin to: struct Messages {
message: Vec<Message>,
}
struct Message {
emitted_at: IVec2,
radius: i32,
} Then we'd have to create a new system be responsible for propagating those messages, checking when they collide with bots, and putting them into bots' recv queues (performing the
Something like: fn receive_messages(messages: ResMut<Messages>, bots: ResMut<AliveBots>) {
let mut msgs_to_delete = Vec::new();
for (msg_idx, msg) in messages.iter_mut().enumerate() {
for bot in bots {
// Having `O(n * m)` isn't ideal, especially since this comparison
// will necessarily return _false_ most of the time, but it's a good
// starting point
if bot.pos.distance(msg.emitted_at) == message.radius {
bot.msgs.receive(msg);
}
}
msg.radius += 1; // possibly every 128 world ticks or so, depending on what's more fun in practice
if msg.radius >= 32 { // possibly even further apart, depending on how it turns out in practice
msgs_to_delete.push(msg_idx);
}
}
/* ... */
} Eventually messages should also be persisted into the savefile:
... but for development you can start off with |
Hi really like all of these ideas, and I think you've hit the nail on the head with practically all of them 1.If we are doing the different message sizes from 4 to 128 Bytes then should the recieve buffer still be 5 messages long? 2.With the comment on message mangling:
Would it be worth considering the strength of the sent message as to how much it mangles / gets mangled |
Yeah, that'd be a pity - I think a size-limited buffer makes more sense (e.g. 512b?). Now that I think about it, dynamically-sized messages require changing the API as well, maybe something like: pub fn radio_send(msg: &[u8]) {
/* ... */
}
// high-level function
pub fn radio_recv() -> Vec<u8> {
// would be much better to use MaybeUninit, just sketching a general concept
let mut msg = vec![0; radio_recv_len()];
radio_recv_msg(&mut msg):
msg
}
// low-level function: returns the length of the next pending message (or 0, if there's none)
pub fn radio_recv_len() -> usize {
/* ... */
}
// low-level function: fills msg with the pending message (requires extra care if msg.len() % 4 != 0, same as radio_send())
pub fn radio_recv_msg(msg: &mut [u8]) {
/* ... */
}
I think so! Currently we can only define strength in terms of message distance (further apart from |
…r messages expanding etc.
Hello! The message bufferOriginally I tried to make the reading of the message buffer quite transparent. Basically the first 132 Bytes are for the data about the actual radio module, and the message it's getting ready to send.
Then the next chunk of addresses were for the message buffer. Originally I was aiming for 512B of messages, meaning you could hold 4 max size messages
with 512 addressable bytes in the buffer we would need to store a message pointer as a u16, and length can be a u8.
or in total
An alternative is just presenting the top message on the buffer, this means that we only need to present 132B of data for the message buffer, but this reduces how much the user can interact with the message buffer. The status of the radio module could then also send the length of the current message at the top of the buffer Or we can limit the number of messages pointers we can store, this would mean that there could possibly be unused space in the message buffer if lots of 4B messages are sent I've included solutions for both ways so it should be easy to play around and choose one, (though currently the read functionality for the presenting only the top message) has some really horrible slice manipulation / work that I want to go back to and clean asap The message emittingI've also been actually writing the code for the messages in world space, and already come into some fun weird interactions! The mangling idea is interesting, but if we model a message as kartoffels/app/crates/kartoffels-world/src/messages.rs Lines 103 to 112 in 04ce002
then two messages a and b that have an intersecting point c will mangle
then even bots touching
or just
This follows nicely onto the expanding message shape / code kartoffels/app/crates/kartoffels-world/src/messages.rs Lines 132 to 155 in 04ce002
Frankly I'm not happy with it, it feels messy and if the mangling being location dependent would also break the functionality I think I'm going to have a bit of a look around, see if I can work out a nicer implementation Finally in this part, (mostly as a note for myself) the check for if a message should be sent to a bot only happens when it expands, this could in theory mean a bot could miss a message by driving onto a cell whilst a message is there. Conclusions / TLDR:
|
Hi, amazing! I'll take a look at it tomorrow 🦀 |
…with available addressable space
How about we level the playing field? Instead of having explicit struct BotRadio {
// Radio's memory.
//
// memory[0] is ignored - it would contain the status byte, but it's better
// to materialize it on the fly (see BotRadio::mmio_read())
//
// memory[1] contains the length of the first message, memory[2] contains
// the first four bytes of that message's payload etc.; if memory[1] == 0,
// there's no message waiting to be received
//
// memory[2 + memory[1]] contains the length of the second message (possibly
// zero if there's no second message waiting to be read) and similarly the
// bytes that follow contain that message's payload etc.
//
// Messages essentially form a linked list, but instead of having `next`
// as a pointer (which would take an extra `u16`), all messages are located
// one after another:
//
// mem[0] | mem[1] | mem[2..] | mem[..] | ...
// -------|-----------------|---------------------|------------------|-----
// xxxxxx | first msg's len | first msg's payload | second msg's len | ...
memory: Vec<u32>,
}
impl Default for BotRadio {
fn default() -> Self {
Self {
memory: vec![0; 1024 / 4],
}
}
}
impl BotRadio {
pub fn mmio_read(&self, addr: usize) -> u32 {
// if addr = 0, return the status byte, otherwise fall back to self.memory
}
pub fn mmio_write(&mut self, addr: usize, val: u32) {
// we need support for a `radio_drop(nth: u8)` syscall
//
// invoking this drops nth message from `self.memory` and shuffles the
// following messages to previous places
//
// e.g. `radio_drop(0)` removes the first message and then copies the
// second message into first message's place etc. (can be done quite
// efficiently with `&[T]::copy_within()`, just remember to zero-out
// length of the last message)
}
pub fn recv_message(&mut self, msg: &[u32]) {
// TODO early-reject message if it doesn't match the filter
let mut ptr = 1;
loop {
let Some(len) = self.memory.get(ptr) else {
// No more space in the buffer, welp.
return;
};
if len == 0 {
self.memory[ptr] = len;
self.memory[ptr + 1..=ptr + len].copy_from_slice(msg);
// ^ careful, this can panic!
// |
// | if `ptr + len >= self.memory.len()`, there is *some* space
// | left, but not sufficient to store the entire message
// |
// | in this case we could either drop the message or just save
// | it partially (store as many bytes as there's space left and
// | call it a day)
// |
// | i think the latter is more fun, it makes also the best use
// | of the buffer
// Message received, yay!
return;
} else {
// There's already a message here, skip over it
ptr += len + 1;
}
}
// Note that since this algorithms walks the entire message list for
// each received message, it's quite inefficient - can be made O(1) by
// saving the next free ptr (as in `self.next_free_ptr: usize`) instead
// of recalculating it all the time
}
} I think this makes the best usage of the available space and also allows for interesting use cases (firmware has access to all messages, it can keep messages in the queue as a memory-saving measure etc., it's essentially a free 1 KB RAM block, just with limited writing capabilities).
I see the issues - I think it'd be best to postpone this for the time being and go back once the main mechanic is done; maybe a solution will materialize then 😄 |
Add all the tests as well as intergration with large portions of kartoffel/radio and bot/radio.rs
This should close #52
There's still some stuff I think might be worth doing:
[ ] - Review current message implementation, is the message size, style etc. worth it?
[ ] - Calculate the cooldown values and adjust them a for balancing
[ ] - Should the write buffer have better write functionality? I would consider using an offset from MEM_BLUETOOTH and writing each 4 bytes instead of just sending 1 byte at a time, this would probably mean you should be able to read from it as well, effectively treat it like a memory location
[ ] - Check over all the names for interfacing functions in kartoffel::bluetooth
[ ] - Add wider / better tests
[ ] - Tutorial level
Other notes
I added two test bots Ianthe and Coronabeth, happy to delete them, but thought if I was going to do a tutorial level they might be useful currently they just work as a tx rx pair doing nothing else
I'd certainly be interested in other messaging concepts as well, creating a send / receive stream between two bots for instance could be a fun task
I think when writing this there was a small part of my brain always thinking about electronic warfare, it'd be cool if you could do some sort of that in a way, but also probably really messy, I made the bots send their own ID as part of the message, but if you wanted to encourage people to write their own clever networking code then maybe the bot id section could be dropped (and i'd probably increase the Message length to accommodate