Skip to content

Conversation

@cannino
Copy link

@cannino cannino commented Oct 26, 2025

Solved issues and/or description of the change

Adds support for the R2D2 sphero droid. Much of the PR has been inspired by the spherov2 python library here
...

Manual test

  • OS and Version (Win/Mac/Linux): Mac
  • Adaptor(s) and/or driver(s): bleclient.Adaptor, sphero.R2D2Driver

Checklist

  • The PR's target branch is 'hybridgroup:dev'
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes (e.g. by run make test_race)
  • No linter errors exist locally (e.g. by run make fmt_check)
  • I have performed a self-review of my own code

If this is a new driver or adaptor:

  • I have added the name to the corresponding README.md
  • I have added an example to see how to setup and use it
  • I have checked or build at least my new example (e.g. by run make examples_check)

If this is a Go version or module update:

  • go.mod to new version updated
  • modules updated (go get -u -t ./...)
  • CI files updated
  • linter setting and linter version (if a newer one exist) updated
  • linter issues fixed or suppressed by config

If this is a PR for release:

  • The PR's target branch is 'hybridgroup:release'
  • I have adjusted the CHANGELOG.md (or already prepared and will be merged as soon as possible)

@@ -0,0 +1,271 @@
package sphero

type Audio uint16
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opted to put these in a different file being that there are quite a few Audio and Animation options


// GetLocatorData calls the passed function with the data from the locator
func (d *R2D2Driver) GetLocatorData(f func(p Point2D)) {
// CID 0x15 is the code for the locator request
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied from the Ollie driver, I think the R2 uses a different CID. I need to add a TODO here

}

// avoid ddos the r2
time.Sleep(commandInterval)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see a Sleep in used in other sphero drivers but it avoids the write channel from writing out of sequence. The python lib does something similar.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could merge the "writeCommand" into the go function, except you plan to write a unit test for it. Than we will see that the frequency depends on the frequency of packets in the "packetChannel", which is filled by "sendCraftPacket". If the according functions can be called to often, a sleep can be useful, but it would also be possible to simply reduce the channel length, so there is a direct synchronization at this level.


log.Printf("processing asyncMessage: % X", d.asyncMessage)

//TODO get sensor data from a packet starting with 0x8d 0x0 0x18 0x2 0xff
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more work needs to be done here. I could see us doing something similar to the python lib were the response stores local sensor variables that can be used by clients calling a simple GetLocation(): X, Y that returns the stored sensor data. A sample sensor packet looks like this:

0x8d 0x0 0x18 0x2 0xff 0x41 0x8a 0x5e 0x2f 0x3e 0x61 0x39 0xe3 0x3f 0x2c 0xf5 0x95 0x3b 0xcf 0x4 0x60 0xbe 0x9b 0x13 0xc0 0x3f 0x78 0x68 0x65 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0xbd 0xfa 0xe7 0xa4 0xbd 0xa2 0xee 0xda 0x0 0x0 0x0 0x0 0xd7 0xd8

d.seqMutex.Lock()
defer d.seqMutex.Unlock()

//TODO handle flags, tid, sid err better
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably should just remove this TODO, I haven't seen a need to handle request Flags or TID, SID optional packet parts.

return append(escaped, b)
}

func unescapeBytes(data []uint8) []uint8 {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be used during handleResponse, though it's not currently referenced.

@cannino cannino marked this pull request as ready for review November 8, 2025 15:52
@cannino
Copy link
Author

cannino commented Nov 12, 2025

@gen2thomas sorry for all of the linting issues. I haven't been able my linter working locally where it catches some of these before I push changes.

@gen2thomas
Copy link
Collaborator

@cannino please can you also inspect your own comments and mark them as resolved or update the content, if possible.

@cannino
Copy link
Author

cannino commented Nov 17, 2025

@cannino please can you also inspect your own comments and mark them as resolved or update the content, if possible.

Hi @gen2thomas,
I've been having some issues parsing packet responses where the packets are coming in 1 byte chunks. Sometimes they are out of order and am seeing very different behavior than the spark+ bot driver. Here's a sample response from the r2 driver

2025/09/29 23:02:26 handleResponse of 1 bytes: 8D
2025/09/29 23:02:26 handleResponse of 1 bytes: 09
2025/09/29 23:02:26 handleResponse of 1 bytes: 13
2025/09/29 23:02:26 handleResponse of 1 bytes: 0D
2025/09/29 23:02:26 handleResponse of 1 bytes: 00
2025/09/29 23:02:26 handleResponse of 1 bytes: D8
2025/09/29 23:02:26 handleResponse of 1 bytes: D6 <- out of order
2025/09/29 23:02:26 handleResponse of 1 bytes: 00 <- out of order
2025/09/29 23:02:26 handleResponse of 1 bytes: 8D
2025/09/29 23:02:26 handleResponse of 1 bytes: 09
2025/09/29 23:02:26 handleResponse of 1 bytes: 1A
2025/09/29 23:02:26 handleResponse of 1 bytes: 02
2025/09/29 23:02:26 handleResponse of 1 bytes: 00
2025/09/29 23:02:26 handleResponse of 1 bytes: CC
2025/09/29 23:02:26 handleResponse of 1 bytes: 0E
2025/09/29 23:02:26 handleResponse of 1 bytes: D8
2025/09/29 23:02:26 handleResponse of 1 bytes: 09
2025/09/29 23:02:26 handleResponse of 1 bytes: 8D
2025/09/29 23:02:26 handleResponse of 1 bytes: 18
2025/09/29 23:02:26 handleResponse of 1 bytes: 11
2025/09/29 23:02:26 handleResponse of 1 bytes: 01
2025/09/29 23:02:26 handleResponse of 1 bytes: 00
2025/09/29 23:02:26 handleResponse of 2 bytes: CCD8

where the spark+ driver contains many bytes

2025/10/01 00:15:49 handleResponse of 6 bytes: FFFF000201FC
2025/10/01 00:15:49 handleCollisionDetected <- i'm actually not certain this code is working properly either as this response is not a collisionDetected response from the spark+, this log data was captured from a simple LED example and not any Roll behavior that would cause a collision.
2025/10/01 00:15:50 handleResponse of 6 bytes: FFFF000301FB
2025/10/01 00:15:50 handleCollisionDetected
2025/10/01 00:15:51 handleResponse of 6 bytes: FFFF000401FA
2025/10/01 00:15:51 handleCollisionDetected
2025/10/01 00:15:52 handleResponse of 6 bytes: FFFF000501F9
2025/10/01 00:15:52 handleCollisionDetected
2025/10/01 00:15:53 handleResponse of 6 bytes: FFFF000601F8

I have attempted multiple strategies to attempt to resolve this problem, but I feel without changing a ble configuration I may not be able to reliably re-sequence the response packets to determine a power state response or sensor data from the bot. Do you have any suggestions? Should I remove all of the handleResponse and callback funcs in this PR because they do not work properly?

@gen2thomas
Copy link
Collaborator

Hi @cannino , unfortunately I have no such device, but found a document for the Sphero API 1.2 on my PC. I will try to match your response messages to this. Maybe you have a link to the matching document for me?

@cannino
Copy link
Author

cannino commented Nov 20, 2025

Hi @cannino , unfortunately I have no such device, but found a document for the Sphero API 1.2 on my PC. I will try to match your response messages to this. Maybe you have a link to the matching document for me?

I've been using the wayback machine to gain information about the v2 BLE protocol: https://web.archive.org/web/20231001145625/https://sdk.sphero.com/docs/api_spec/general_api/

That in combination with the pythonv2 library where I can see a queue is created for the wait packet with the key being the packet sequence.
https://github.com/artificial-intelligence-class/spherov2.py/blob/4252ddb1a12a25db725257d66e3e8ec3057dd48b/spherov2/toy/__init__.py#L111

I don't know enough about ble protocols but perhaps there may be a configuration I can apply that would allow more packets to be read somewhere in here:

func readFromCharacteristic(chara bluetoothExtCharacteristicer) ([]byte, error) {

that would allow an entire packet to be read rather than a single byte response. Not sure.

@gen2thomas
Copy link
Collaborator

gen2thomas commented Nov 21, 2025

I don't know enough about ble protocols but perhaps there may be a configuration I can apply that would allow more packets to be read

The packet length (count of bytes in the received buffer) is defined by the used characteristic or the related implementation in the software of R2D2 hardware device. Maybe there is a configuration parameter to change this on device initialization. Or, the R2D2 send-algorithm is interrupted by something, e.g. by sending our commands to often?

If there is no such configuration, we can just collect each received byte until a package is complete. As far as I can see a package starts with 0x8D and ends with 0xD8 - and normally has length of 8 bytes for this kind of response-packages (0x09). Your last "0xCCD8" is also an exception, but when the package is parsed in this way, it should work. Maybe the "out of order" bytes are only a print-problem, because the complete handling is asynchronously.

It seems the "spark+" uses the API 1.2 protocol, because there exists SOP1+SOP2 => 0xFFFF (SOP2 for acknowledgment, otherwise SOP2 is 0xFE).

@cannino
Copy link
Author

cannino commented Nov 26, 2025

The packet length (count of bytes in the received buffer) is defined by the used characteristic or the related implementation in the software of R2D2 hardware device. Maybe there is a configuration parameter to change this on device initialization. Or, the R2D2 send-algorithm is interrupted by something, e.g. by sending our commands to often?

I need to spend more time on this if we want to include getting sensor/feedback data from the R2 in the initial commit.

I did set a breakpoint at the time the BLE characteristic is initialized and see the MTU size is 20 bytes. You may have given a clue as to the BLE characteristic buffer getting overloaded. The set_data_streaming python function pertains to how the sensor data is sent back and not particularly how the BLE characteristic reads response data. Here's a full log of the python lib with my comments on the commands so you can see how the request/response flow is working. A majority of the commands are initialization with the only actual coded part of the python program was setting the main LED to RGB values of 90, 255, 90.

// [SOP, FLAGS, TID (optional), SID (optional), DID, CID, SEQ, ERR (at response), DATA..., CHK, EOP]
// set power state
encode> did: 19, cid: 13, proc: None, data: None
request 0x8d 0xa 0x13 0xd 0x0 0xd5 0xd8
response 0x8d 0x9 0x13 0xd 0x0 0x0 0xd6 0xd8
// set head position
encode> did: 23, cid: 15, proc: None, data: b'\x00\x00\x00\x00'
request 0x8d 0xa 0x17 0xf 0x1 0x0 0x0 0x0 0x0 0xce 0xd8
response 0x8d 0x8 0x13 0x11 0xff 0xd4 0xd8
response 0x8d 0x9 0x17 0xf 0x1 0x0 0xcf 0xd8
// perform leg action
encode> did: 23, cid: 13, proc: None, data: [<R2LegActions.THREE_LEGS: 1>]
request 0x8d 0xa 0x17 0xd 0x2 0x1 0xce 0xd8
response 0x8d 0x9 0x17 0xd 0x2 0x0 0xd0 0xd8
// set locator flags
encode> did: 24, cid: 23, proc: None, data: [0]
request 0x8d 0xa 0x18 0x17 0x3 0x0 0xc3 0xd8
response 0x8d 0x9 0x18 0x17 0x3 0x0 0xc4 0xd8
// configure collision detection
encode> did: 24, cid: 17, proc: None, data: [<CollisionDetectionMethods.ACCELEROMETER_BASED_DETECTION: 1>, 90, 130, 90, 130, 1]
request 0x8d 0xa 0x18 0x11 0x4 0x1 0x5a 0x82 0x5a 0x82 0x1 0xe 0xd8
response 0x8d 0x9 0x18 0x11 0x4 0x0 0xc9 0xd8
// set power notifications - enable_battery_state_changed_notify
encode> did: 19, cid: 5, proc: None, data: [1]
request 0x8d 0xa 0x13 0x5 0x5 0x1 0xd7 0xd8
response 0x8d 0x9 0x13 0x5 0x5 0x0 0xd9 0xd8
// enable_gyro_max_notify
encode> did: 24, cid: 15, proc: None, data: [1]
request 0x8d 0xa 0x18 0xf 0x6 0x1 0xc7 0xd8
response 0x8d 0x9 0x18 0xf 0x6 0x0 0xc9 0xd8
// set_sensor_streaming_mask
encode> did: 24, cid: 0, proc: None, data: [0, 0, 0, 0, 0, 0, 0]
request 0x8d 0xa 0x18 0x0 0x7 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0xd6 0xd8
response 0x8d 0x9 0x18 0x0 0x7 0x0 0xd7 0xd8
// set_extended_sensor_streaming_mask
encode> did: 24, cid: 12, proc: None, data: b'\x00\x00\x00\x00'
request 0x8d 0xa 0x18 0xc 0x8 0x0 0x0 0x0 0x0 0xc9 0xd8
response 0x8d 0x9 0x18 0xc 0x8 0x0 0xca 0xd8
// set_sensor_streaming_mask ; interval
encode> did: 24, cid: 0, proc: None, data: [0, 150, 0, 0, 0, 0, 0]
request 0x8d 0xa 0x18 0x0 0x9 0x0 0x96 0x0 0x0 0x0 0x0 0x0 0x3e 0xd8
response 0x8d 0x9 0x18 0x0 0x9 0x0 0xd5 0xd8
// turn off leds ; set_multiple_leds ; LedControl ; set_all_leds_with_16_bit_mask
encode> did: 26, cid: 14, proc: None, data: [0, 255, 0, 0, 0, 0, 0, 0, 0, 0]
request 0x8d 0xa 0x1a 0xe 0xa 0x0 0xff 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0xc4 0xd8
response 0x8d 0x9 0x1a 0xe 0xa 0x0 0xc4 0xd8
// reset_locator_x_and_y
encode> did: 24, cid: 19, proc: None, data: None
request 0x8d 0xa 0x18 0x13 0xb 0xbf 0xd8
response 0x8d 0x9 0x18 0x13 0xb 0x0 0xc0 0xd8
// __start_capturing_sensor_data; you can see response packets start shortly after this with a much larger payload
encode> did: 24, cid: 0, proc: None, data: [0, 0, 0, 0, 7, 224, 120]
request 0x8d 0xa 0x18 0x0 0xc 0x0 0x0 0x0 0x0 0x7 0xe0 0x78 0x72 0xd8
response 0x8d 0x9 0x18 0x0 0xc 0x0 0xd2 0xd8
encode> did: 24, cid: 12, proc: None, data: b'\x03\x80\x00\x00'
request 0x8d 0xa 0x18 0xc 0xd 0x3 0x80 0x0 0x0 0x41 0xd8
response 0x8d 0x9 0x18 0xc 0xd 0x0 0xc5 0xd8
encode> did: 24, cid: 0, proc: None, data: [0, 150, 0, 0, 7, 224, 120]
request 0x8d 0xa 0x18 0x0 0xe 0x0 0x96 0x0 0x0 0x7 0xe0 0x78 0xda 0xd8
response 0x8d 0x9 0x18 0x0 0xe 0x0 0xd0 0xd8
// api.set_main_led
encode> did: 26, cid: 14, proc: None, data: [0, 119, 90, 255, 90, 90, 255, 90]
response 0x8d 0x0 0x18 0x2 0xff 0x41 0x8a 0x5e 0x2f 0x3e 0x61 0x39 0xe3 0x3f 0x2c 0xf5 0x95 0x3b 0xcf 0x4 0x60 0xbe 0x9b 0x13 0xc0 0x3f 0x78 0x68 0x65 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0xbd 0xfa 0xe7 0xa4 0xbd 0xa2 0xee 0xda 0x0 0x0 0x0 0x0 0xd7 0xd8
request 0x8d 0xa 0x1a 0xe 0xf 0x0 0x77 0x5a 0xff 0x5a 0x5a 0xff 0x5a 0xe1 0xd8
response 0x8d 0x0 0x18 0x2 0xff 0x41 0x8b 0xf 0x62 0x3e 0x52 0xe3 0xb8 0x3f 0x27 0x3c 0xf2 0x3b 0x21 0xbd 0x81 0xbe 0xa1 0x1b 0xe9 0x3f 0x77 0x5f 0x28 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0xbe 0x3c 0x18 0x1b 0x3e 0x66 0xc6 0x66 0x0 0x0 0x0 0x0 0x33 0xd8
response 0x8d 0x9 0x1a 0xe 0xf 0x0 0xbf 0xd8
response 0x8d 0x0 0x18 0x2 0xff 0x41 0x8b 0x16 0x1b 0x3e 0x14 0x95 0x5 0x3f 0x29 0x37 0xc5 0xba 0xc0 0x24 0xbc 0xbe 0x9f 0x8 0x37 0x3f 0x79 0x5f 0x31 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x3e 0xfa 0x9b 0xea 0xbe 0xc 0xa5 0x1e 0x0 0x0 0x0 0x0 0x91 0xd8
// ... many more response packets
response 0x8d 0x0 0x18 0x2 0xff 0x41 0x8b 0xab 0x23 0x6a 0x3e 0x68 0xb 0x9f 0x3f 0x11 0x32 0x45 0x3b 0x1f 0x89 0xcb 0xbe 0x9d 0xe 0x1b 0x3f 0x79 0x64 0xc3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x3e 0x3c 0x19 0xfb 0xbd 0x9f 0x62 0x8a 0x0 0x0 0x0 0x0 0x87 0xd8
// sleep
encode> did: 19, cid: 1, proc: None, data: None
request 0x8d 0xa 0x13 0x1 0x10 0xd1 0xd8
response 0x8d 0x0 0x18 0x2 0xff 0x41 0x8b 0xc0 0x8a 0x3e 0x6d 0xff 0x24 0x3f 0x10 0x73 0x9d 0xba 0xc9 0x1 0x4 0xbe 0x9a 0xf9 0xeb 0x3f 0x79 0x60 0xe9 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x3d 0x78 0x4c 0x5c 0x3e 0x29 0x62 0x6a 0x0 0x0 0x0 0x0 0xce 0xd8
response 0x8d 0x9 0x13 0x1 0x10 0x0 0xd2 0xd8

@gen2thomas
Copy link
Collaborator

I did set a breakpoint at the time the BLE characteristic is initialized and see the MTU size is 20 bytes.

In my implementations with custom characteristics I have limited the count of bytes to 19, because I have determined for the 20th byte the first bit was always set to "0".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants