tinygo-org / bluetooth

Cross-platform Bluetooth API for Go and TinyGo. Supports Linux, macOS, Windows, and bare metal using Nordic SoftDevice or HCI
https://tinygo.org
Other
730 stars 136 forks source link

Want to make a BLE HID Keyboard #200

Open sago35 opened 11 months ago

sago35 commented 11 months ago

Perhaps Bonding is what is needed, but there may be other things needed.

https://github.com/tinygo-org/bluetooth#api-stability Bonding and private addresses.

Is anyone working on creating a BLE HID Keyboard? Or, what can I make that will help me reach my goal? I would like some hints.

funkycode commented 11 months ago

It might be also blocked by

Q. Can a device be both a GATT client and GATT server?

A. Yes, but this is not currently supported by Go Bluetooth. Current support is either to act as a central in client mode, or as a peripheral in server mode.

If I understand that correctly, as it is relevant for split keyboards. You have main keyboard that should be used for both advertising to receive data from other half and as well as one that connects to device as keyboard.

funkycode commented 11 months ago

I think the start would be to add events here as per this documentation for example to try this using this struct of data from my understanding. But again, my knowledge both for bluetooth, embeded, lowlevel code and c bindings is poor, just assumption going thru exasting code (based on me using nicenano v2 that uses softdevice 140 v6)

funkycode commented 11 months ago

just to play around i tried to implement simple bonding by adding something like:

        case C.BLE_GAP_EVT_SEC_PARAMS_REQUEST:
            var secParams C.ble_gap_sec_params_t

            secParams.set_bitfield_bond(1)
            secParams.set_bitfield_mitm(0)
            //  secParams.set_bitfield_keypress(0)
            secParams.set_bitfield_lesc(0)
            secParams.set_bitfield_io_caps(C.BLE_GAP_IO_CAPS_NONE)
            C.sd_ble_gap_sec_params_reply(gapEvent.conn_handle, C.BLE_GAP_SEC_STATUS_SUCCESS, &secParams, nil)

but i end up getting panic: runtime error at 0x00027df9: heap alloc in interrupt i saw similar issues and from my understanding maybe the solution would be to implement some kind of callback where it would trigger the response as you can't do it inside it above.

sago35 commented 11 months ago

panic: runtime error at 0x00027df9: heap alloc in interrupt

Commenting out the following lines will prevent a panic. @funkycode It would be best to eventually fix the issue, but for now, commenting it out for testing purposes could be beneficial.

https://github.com/tinygo-org/tinygo/blob/v0.30.0/src/runtime/gc_blocks.go#L281

funkycode commented 11 months ago

well, it;s little bit of problematic for me, i didn't build tinygo and using docker to build fw :( i tried to send bondCallbacck function and use DisableInterupts and RestoreInterups, but with no success yet :(

xudongzheng commented 11 months ago

This is something I've been looking into.

Implementing bonding in a secure manner, especially with LESC, is probably not trivial.

Aside from that, storing the bonding data to flash might be tricky since SoftDevice has strict timing requirements. It's not clear how much effort it will be to get SoftDevice to cooperate with machine.Flash or LittleFS or something else.

One option I'm considering is a dual-chip setup with Zephyr (handling Bluetooth, maybe running ZMK, on nRF52840) communicating with TinyGo over UART. This can perhaps be handled by a single chip like the dual-core Nordic nRF5340 with the Bluetooth stuff running on the network core and TinyGo running on the application core.

Another option is somehow embedding TinyGo within a Zephyr application. I'm keeping an eye on tinygo-org/tinygo#254 and tinygo-org/tinygo#3067.

If I understand that correctly, as it is relevant for split keyboards.

One workaround is having both halves be peripherals and a separate dongle be the central.

funkycode commented 11 months ago

Well, yeah, the charts in Nordic docs are simple. But for me low level is quite new, so I wanted to check "just works" examples first with simple bonding. As well as with secure ones you might need to make different flows depending on role, as well as keep keys and maybe generate those (like dht key)

As per ZMK, it might work, but I think then I could simply use ZMK and avoid tinygo, I think to have purely in tinygo is the way to go IMHO

aykevl commented 11 months ago

but i end up getting panic: runtime error at 0x00027df9: heap alloc in interrupt

This is because the code is running inside an interrupt, and you can't do heap allocations there.


            C.sd_ble_gap_sec_params_reply(gapEvent.conn_handle, C.BLE_GAP_SEC_STATUS_SUCCESS, &secParams, nil)

&secParams here results in a heap allocation: you're taking the pointer to a local variable, which forces the compiler to allocate it on the heap. One solution is to make secParams a global.

Commenting out the following lines will prevent a panic. @funkycode It would be best to eventually fix the issue, but for now, commenting it out for testing purposes could be beneficial.

Don't! This is a bug, even if it might appear to work most of the time. It really needs to be fixed (as described above, for example).

aykevl commented 11 months ago

Aside from that, storing the bonding data to flash might be tricky since SoftDevice has strict timing requirements. It's not clear how much effort it will be to get SoftDevice to cooperate with machine.Flash or LittleFS or something else.

I've found that as long as you don't block the SoftDevice for a too long period, it's usually fine. What is needed however is that the SoftDevice APIs must be called when needed. For example, here:

    for j := 0; j < len(padded); j += int(f.WriteBlockSize()) {
        // write word
        *(*uint32)(unsafe.Pointer(address)) = binary.LittleEndian.Uint32(padded[j : j+int(f.WriteBlockSize())])
        address += uintptr(f.WriteBlockSize())
        waitWhileFlashBusy()
    }

For that, the code in here might be helpful: https://github.com/tinygo-org/tinygo/blob/release/src/runtime/runtime_nrf_softdevice.go

funkycode commented 11 months ago

Yeah, I had a thought to take it out as global, but not sure why I didn't. Anyhow I was able to implement simple pairing (without bonding), going to try to implement bonding and maybe simple auth.

I think I would make global variables for sec_params and for keys and implement global functions for changing the parameters of those, not sure if there is still need to make a pairing request as callback function to enable to handle error (according to specs if you get an error, you can resend request with different value).

Will try to make simple PR for review in case someone is interested and maybe can give feedback 🙏🏽

funkycode commented 11 months ago

Okay, I was able to connect from nice!nano to my android phone and send text input. It took me long time to understand why report map characteristics is just disappearing once i set the map value. Is there any reason for the value max len limit of 20 ?

xudongzheng commented 11 months ago

Is there any reason for the value max len limit of 20 ?

The default Bluetooth MTU value 23 corresponds to 20 bytes of data. Longer data such as the report map is typically transferred in 20-byte chunks.

In some cases, the peripheral and central can negotiate a larger MTU value.

funkycode commented 11 months ago

Aside from that, storing the bonding data to flash might be tricky since SoftDevice has strict timing requirements. It's not clear how much effort it will be to get SoftDevice to cooperate with machine.Flash or LittleFS or something else.

I've found that as long as you don't block the SoftDevice for a too long period, it's usually fine. What is needed however is that the SoftDevice APIs must be called when needed. For example, here:

  for j := 0; j < len(padded); j += int(f.WriteBlockSize()) {
      // write word
      *(*uint32)(unsafe.Pointer(address)) = binary.LittleEndian.Uint32(padded[j : j+int(f.WriteBlockSize())])
      address += uintptr(f.WriteBlockSize())
      waitWhileFlashBusy()
  }

For that, the code in here might be helpful: https://github.com/tinygo-org/tinygo/blob/release/src/runtime/runtime_nrf_softdevice.go

So to try to store data i need to use .Flash as i saw BlockDevice - seems from code you linked above it is same type, so I can use any of those?

@sago35 If i recall correctly you implement vial support, does it use flash to write data? I would like to see some example of how data is saved and how you read it in case it is :pray:

sago35 commented 11 months ago

@funkycode

In tinygo-keyboard, the following code is used to utilize machine.Flash.

Other example is here.

sago35 commented 11 months ago

@funkycode I had forgotten, but I suspect that machine.Flash may not be working with vial support on the nRF52840. I needed to investigate this, but I forgot. I will also check on this.

It was my misunderstanding. I had turned the XIAO-BLE into a BLE SNIFFER, and I've come to realize that this breaks various functionalities of TinyGo. In such a case, it's necessary to re-flash the softdevice.

sago35 commented 11 months ago

@funkycode I believe there are parts I can help with, so I would really appreciate it if you could share the source code.

funkycode commented 11 months ago

@funkycode I believe there are parts I can help with, so I would really appreciate it if you could share the source code.

I have a lot of mess, printlns and etc I need to revert some things and cleanup. Will try to do it in upcoming days. Would love some help and feedback 🙏🏽

xudongzheng commented 11 months ago

If you haven't already, I would suggest looking at the "peer manager" in the nRF5 SDK for examples of how the callbacks are used. I think smd_ble_evt_handler() in components/ble/peer_manager/security_dispatcher.c will be the most relevant.

It might be a good idea to start by writing an in-memory backend. The bonding data wouldn't persist between reboots but it will require less effort and help verify that you are storing and restoring the correct data.

Keep in mind that erasing flash is expensive on embedded systems so most likely you'll want to implement an append-only key-value store where the last value has priority when data is read back from flash. Using something higher level like LittleFS is another option.

funkycode commented 11 months ago

I was able to successfully establish LESC communication. as well Gonna try to save keys and see how and where i should use them in order to reestablish connection. @sago35 would love your help on that one :pray:

Moreover seems that public key generation is heavy, as for some reason, when I utilize I can't create simple goroutine in the keyboard logic for key press events I had working in just bond connection.

SerialVelocity commented 10 months ago

@sago35 Something else which I don't think exists yet, is attribute descriptors which i think are required to setup a BLE HID Keyboard, right?

aykevl commented 9 months ago

@sago35 Something else which I don't think exists yet, is attribute descriptors which i think are required to setup a BLE HID Keyboard, right?

You can add the notify or indicate permission which will automatically create an attribute descriptor. Other kinds of attribute descriptors aren't supported right now.

sago35 commented 9 months ago

The BLE HID Keyboard can be made to work to some extent as follows. However, it worked on Android but failed around pairing on Windows.

https://github.com/sago35/tinygo-keyboard/blob/fde11c000fdef1b0c6813fffc7f0e05a13ac4a90/ble/ble.go https://github.com/funkycode/bluetooth/tree/bc92782f67c9f73054f6efee60670e53297a79c5

The following value needs to be changed to a value larger than 20 (the size of the Descriptor). It may be necessary to make this value configurable.

https://github.com/tinygo-org/bluetooth/blob/d0c7887b81e136df90b732d1162cdb335633aafa/gatts_sd.go#L60