Skip to content
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

Bluetooth LE (BLE / Bluetooth Smart) TNC #124

Open
stephensworld opened this issue Mar 4, 2016 · 84 comments
Open

Bluetooth LE (BLE / Bluetooth Smart) TNC #124

stephensworld opened this issue Mar 4, 2016 · 84 comments

Comments

@stephensworld
Copy link

Is it possible to add Bluetooth LE support in aprsdroid?
e.g. HM-10 module

Thanks

Stephan

@ge0rg
Copy link
Owner

ge0rg commented Mar 9, 2016

Principally, yes. However, Bluetooth LE does not provide a "serial" profile comparable to Bluetooth SPP, so a specific profile would be needed.

I have created a proof-of-concept for APRS-IS-over-BLE here: https://github.com/ge0rg/bluetoothle-tnc

Something similar needs to be created for exchanging KISS packets over BLE, and then implemented both on the IC and on the Android side. Such a protocol could even be deployed on iOS.

I'd gladly volunteer to write out the specification of the protocol and do the Android side implementation, provided somebody creates an according BLE TNC (or at least a BLE-to-serial adapter of some sort).

@ge0rg ge0rg changed the title Feature Request: HM-10 BLE possibility Bluetooth LE (BLE / Bluetooth Smart) TNC Mar 9, 2016
@mobilinkd
Copy link

I am working on new TNC prototypes right now so, what the heck, I might at well do a version that has a dual-mode BT module on it. I'm not convinced that BLE is a good fit for a general purpose TNC, but I do like to see hams experimenting with this stuff. If I can get hardware into peoples hands and something good comes of it, that would be great. The TNC will also have a working USB CDC and BT SPP.

I will try to get one out to you in the next month or so. The firmware will be very alpha quality. And I will have little time to work on the BLE code.

Now I just need to find my HM-12 modules...

@ge0rg
Copy link
Owner

ge0rg commented Apr 7, 2016

@mobilinkd I think that BLE is the only reasonable way to attach a TNC to an iPhone, so that would be the main reason. Besides of that, it has sufficient capacity for 1200bd APRS, so it might be an interesting low-power alternative for Android as well, provided the AFSK codec doesn't eat too much in comparison to BT.

Ping me up some weeks before you start your work, and I'll sketch out the KISS-over-BLE protocol.

@kf7rcs
Copy link

kf7rcs commented Apr 27, 2016

I too would be interested in this. I have a Joying Android head unit for my car. It will not connect to my mobilinkd via blue tooth. I believe it does not support SPP profile. It sees the mobilinked as a phone. So i am willing to try out any beta code and provide feedback.

@ge0rg
Copy link
Owner

ge0rg commented May 12, 2017

Are there any news on BLE hardware?

@rriggs
Copy link

rriggs commented Jul 28, 2017

@ge0rg I received my first packets over BLE today.

@rriggs
Copy link

rriggs commented Aug 3, 2017

@ge0rg First packet TX happened this evening.

@ge0rg
Copy link
Owner

ge0rg commented Aug 4, 2017

Hey @rriggs, that's some awesome news. From your twitter feed I implied that you are running a HM-10 module of some sort, but I wasn't yet able to find its BLE serial protocol spec. If it's as simple as "read/write strings from certain characteristic", it shouldn't be too hard to add to APRSdroid.

Maybe you can contact me off-list with some more details? :)

@markqvist
Copy link

Even though this thread has not had activity for some time, I'll venture to add my input as well.

I'd argue that it makes more sense to simply run the communications over a generic UART profile, than creating a more specific KISS-over-BLE profile. KISS has always encapsulated in "raw" UART anyways, and several adapters already support a generic UART profile. Either way, I'd be interested to think what you all think.

@ge0rg
Copy link
Owner

ge0rg commented Jan 29, 2019

It looks like this is exactly what's happening in the TNC3. I didn't have the time to implement the BLE protocol yet, but I assume the battery savings will be meager compared to the GPS usage on the phone. OTOH, maybe the TNC battery will last significantly longer in LE mode?

@rriggs, do you happen to have numbers on BLE vs SPP battery life time expectations?

@mobilinkd
Copy link

mobilinkd commented Jan 29, 2019 via email

@rriggs
Copy link

rriggs commented Jan 29, 2019 via email

@markqvist
Copy link

In my upcoming TNC I ended up going for BLE instead of SPP, even though there is not much application support at the moment, but I think it makes more sense from a platform standpoint to support BLE in the long run. It's currently working with a generic UART over BLE profile, where it uses the KISS protocol transparently over UART, same as serial and USB serial. I'd be happy to test any experimental builds of APRS droid on the hardware, or supply you with a prototype if that could help out.

@mobilinkd
Copy link

mobilinkd commented Jan 30, 2019 via email

@markqvist
Copy link

I was referring to the example UART GATT service from Nordic, with UUID 6E400001-B5A3-F393-­E0A9-­E50E24DCCA9E. In my (admittedly still rather limited) testing, it seems to work as intended.

But I had assumed this service profile was used as a reference and was thus relatively standard, which apparently I was wrong in assuming.

What are your thoughts, do you think we should "stick" with just running everything over a generic UART profile (whatever profile that turns out to be), or would you rather see a specific profiles for different protocols, say a profile for KISS endpoints, a profile for CAT control and so on?

I just read through @ge0rg's specifications, and it looks soild enough, although I think there's a bit of an issue if the specification only talks about AX.25 frames, as opposed to KISS frames. It might just be me being pedantic here, but I think it's worth chiseling out clearly in the spec, if this is to be a standard we all converge on. The data frame command is only one command out of a potential 256 KISS commands (or 16 if using the high nibble as HDLC port number).

To be honest I don't see much added benefit to using the custom profile over a generic profile just advertising UART capability though, apart from the discoverability aspect, which might very well be worth it anyhow. The additional features from ge0rg's spec like audio volume and other diagnostics messages can just be implemented in KISS frames, and seems to me like a more elegant solution, than having to side-channel them into a separate characteristic.

@mobilinkd
Copy link

Hi @markqvist ,

Every module manufacturer uses a different service UUID for transparent data transfer.

We have a product on the market using those UUIDs, and @hessu supports those UUIDs in the aprs.fi iOS app. Using those existing UUIDs will allow you and others to build on existing infrastructure.

In the end, as you aptly note, it is just doing KISS over a transparent data connection [1].

The primary reason to use a custom service UUID is that we are not just providing a transparent data service -- we are providing a very specific service that sends and receives KISS packets over a BLE endpoint. An entire KISS TNC firmware can be implemented inside a BLE module, without a single UART on the module enabled.

Things like rig control belong under a separate service UUID. Rig control should not be tied to a TNC service. The idea behind BLE is that its services implement the purest form of a microservice.

I need to publish the current implementation with input from @hessu and @ge0rg. It is pretty straight-forward, but there are a few things to clarify. For example, we support packing multiple KISS packets into a single MTU transfer. That impacts things like setting of KISS parameters (0x01-0x05) and hardware parameters (0x06). And a single KISS packets can span multiple MTU transfers.

I agree that using KISS hardware parameters (0x06) to update things like volume levels is the way to go. That is how we configure our TNCs.

The Mobilinkd iOS config app is an open reference implementation of the BLE client side code in Swift. If you look at the SLIP and KISS code, there is nothing at all surprising in there.

The Mobilinkd TNC3 use a Microchip BM78 module. I have a number of prototypes using various BLE-only modules, including various Nordic nRF51 & nRF52 modules. At last count there were 7 or 8 different modules in various stages of testing on my bench. The nRF52 modules are certainly the way to go for a BLE-only product.

  • Rob
  1. I don't like the term UART for this since it implies setting serial port parameters which do not apply here. A true BLE UART service would have characteristics for controlling UART parameters as well as data tx/rx.

@markqvist
Copy link

@mobilinkd, you are right, it's the correct way to go about it. A KISS service should be advertised as that over BLE, not a UART service, which as you mention is even quite a misnomer here, since there is no serial parameters involved.

If a more formal specification is worked out, I will support that in my upcoming products as well. Having an agreed upon standard is the way to go here.

@hessu
Copy link

hessu commented May 15, 2020

I guess nobody wrote the updated formal spec yet? @mobilinkd ? I was just thinking I might do it if nobody else did. It'd be good to have it out there before someone makes yet another protocol which is different, and then we have to start reimplementing clients.

@markqvist
Copy link

I know you probably also have a ton of other things to work on, but I really like the idea of you being the author of it @hessu. It seems more fitting than if it originates from one of us “hardware implementers”. We can all help out, of course. But you being the main author, I think would be very suitable.

And I agree completely, it would be very good to have it formalised sooner than later.

@mobilinkd
Copy link

mobilinkd commented May 15, 2020 via email

@hessu
Copy link

hessu commented May 16, 2020

Here goes, initial proposal:

https://github.com/hessu/aprsfi/blob/kiss-ble/AX25-KISS-BLE.md

Please review and provide feedback. I was very tired so there may be something silly in there. Maybe Georg does not mind if we hijack this aprsdroid ticket for the discussion since we've been here so long already. :)

@markqvist
Copy link

Great work. That is clear, concise and easy to follow for implementation, and I don’t think it leaves anything up to the imagination. Thanks a lot for the effort.

I’d like to discuss whether it’s beneficial to tie it exclusively to AX.25. I know that in practice, AX.25 is almost all that is ever encapsulated in KISS, but that’s not a limitation of KISS, since it can encapsulate and transport frames for anything to and from a TNC. It would also be “free” specification-wise to state it more as just “KISS over BLE”, since it would not pose any restrictions for the current primary use-case of AX.25 over KISS over BLE. Personally I have several uses of KISS TNCs that do not involve AX.25, but use other link layers.

I think the primary focus should still be on AX.25, and that the frame and buffer size calculations and rationales you present should be based on AX.25, but opening it up a little will make it come across as more generic, which I think is good in this case.

If you’re interested I can submit an edit proposal as a pull request, and you can look it over and incorporate what you find useful, if you find that my points are sensible at all, that is.

@mobilinkd
Copy link

It looks like someone else is a fan of RFC 2119. :-)

One change I believe needs to be made is with regards to the vendor extensions section. One cannot just pick a random UUID for an attribute. The BLE spec is written to say that, but Nordic software is written such that the service and attribute UUIDs must share a base, and you get 16 bits for the attribute UUIDs. This is why I changed the UUIDs used by @ge0rg's original implementation.

The way it is done with Nordic (and I think I have seen others do the same) is to create a UUID, then zero out the third and fourth octet. Set it to 1 for the service UUID and then increment from there for the service attributes.

Maybe the right thing to do is ask people to pick a 13-bit number for their vendor code and use the bottom 3-bits for their own 8 unique attributes?

Additionally, you are welcome to refer to the Mobilinkd iOS Config App as well. This is written in Swift and released under the Apache license. https://github.com/mobilinkd/iosTncConfig It implements part of the spec for configuring the TNC using SetHardware codes.

Should this be added as a reference for the KISS protocol? http://www.ax25.net/kiss.aspx

@hessu
Copy link

hessu commented May 18, 2020

Thanks, good points.

@markqvist I think the application should be made aware of whether the KISS device can accept AX.25 or something else. For example, my application can only do AX.25 at the moment, and it might cause issues if it would send AX.25 to a non-AX.25 KISS device. Should there be a different service UUID for non-AX.25 KISS-over-BLE device, so that they wouldn't even show up in my app?

@markqvist
Copy link

In practice, most TNCs will happily (and should) transmit whatever is encapsulated in its KISS data frame, whether that be AX.25 or not. That was part of the point with KISS, to leave only the physical layer processing to the TNC, and actual data link-layer protocols to the host.

A KISS-device is per definition network- and link-layer protocol agnostic, so there is not really a concept of "AX.25 vs non-AX.25" KISS device. You could go as far as saying that all KISS devices are essentially non-AX.25 devices. Even the Frame Check Sequence, which is sometimes taken as a part of AX.25 is technically part of the physical layer HDLC-like framing.

My argument is that any KISS device should in theory adhere to the following operating principle:

  1. Accept any data frame that is correctly encapsulated in a KISS frame and does not exceed the devices MTU.
  2. Reconstruct the actual frame data from the potentially escaped byte sequence in the KISS frame.
  3. Transmit the data on the physical interface encapsulated in HDLC and add the Frame Check Sequence.

Conversely, a receiving KISS device should transparently pass any received frames with a valid Frame Check Sequence to the host device encapsulated as a KISS frame, without making any judgements on the content.

@mobilinkd
Copy link

I agree with Mark.  KISS is agnostic as to the contents of the payload.  I have used the TNC3 to receive satellite telemetry data which are not in AX.25 format, but which are embedded in HDLC frames.

The spec should probably make it clear that it is up to the client application to determine whether the data it receives from the TNC is properly formatted.

@hessu
Copy link

hessu commented May 25, 2020

Please find an updated version here:

https://github.com/hessu/aprs-specs/blob/master/BLE-KISS-API.md

diff:
hessu/aprs-specs@e72f376

I tried to implement all of your suggestions, and give a fairly precise method to allocate extension UUIDs. Did I miss something?

@na7q
Copy link
Contributor

na7q commented Dec 17, 2024

Here's the full log for startup. Appears to only start once. Then 2 seconds later it throws an error. In this attempt it actually allowed data to be sent followed by the same errors 2 seconds after writing. A follow up disconnect and reconnect provides the exact same results, but data is not sent to the device.

2024-12-16 19:14:54.704 8581-8581 APRSdroid.Service pid-8581 D onStartCommand: Intent { act=org.aprsdroid.app.SERVICE cmp=org.aprsdroid.app/.AprsService }, 0, 1
2024-12-16 19:14:54.711 8581-8581 APRSdroid.Service pid-8581 D addPost: null - APRS Service started: Manual Position, TNC (KISS), Bluetooth Low Energy.
2024-12-16 19:14:54.713 8581-8581 APRSdroid.BluetoothLE pid-8581 D BluetoothTncBle.createConnection: 38:D2:00:00:F3:56
2024-12-16 19:14:55.947 8581-10635 APRSdroid.BluetoothLE pid-8581 D Connected to GATT server
2024-12-16 19:14:55.948 8581-10635 APRSdroid.BluetoothLE pid-8581 D MTU changed to 155 bytes
2024-12-16 19:14:55.949 8581-10635 APRSdroid.KissProto pid-8581 D Backend Name1: TNC (KISS), Bluetooth Low Energy
2024-12-16 19:14:55.949 8581-17372 APRSdroid....eiveThread pid-8581 D BLEReceiveThread.run()
2024-12-16 19:14:55.971 8581-8581 APRSdroid.Service pid-8581 D onPosterStarted
2024-12-16 19:14:55.973 8581-8581 APRSdroid.FixedPosition pid-8581 D start: periodic=false single=false
2024-12-16 19:14:57.954 8581-9072 APRSdroid.BluetoothLE pid-8581 D Services discovered and characteristics set
2024-12-16 19:14:58.074 8581-9072 APRSdroid.BluetoothLE pid-8581 D Notification enabled
2024-12-16 19:14:58.163 8581-9072 APRSdroid.BluetoothLE pid-8581 D MTU changed to 155 bytes
2024-12-16 19:14:58.168 8581-9072 BluetoothGatt pid-8581 W Unhandled exception in callback
java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:960)
at org.aprsdroid.app.BluetoothLETnc$$anon$1.onMtuChanged(BluetoothLETnc.scala:136)
at android.bluetooth.BluetoothGatt$1$13.run(BluetoothGatt.java:770)
at android.bluetooth.BluetoothGatt.runOrQueueCallback(BluetoothGatt.java:948)
at android.bluetooth.BluetoothGatt.-$$Nest$mrunOrQueueCallback(Unknown Source:0)
at android.bluetooth.BluetoothGatt$1.onConfigureMTU(BluetoothGatt.java:765)
at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:353)
at android.os.Binder.execTransactInternal(Binder.java:1299)
at android.os.Binder.execTransact(Binder.java:1253)

@na7q
Copy link
Contributor

na7q commented Dec 17, 2024

I think I will pick up on this tomorrow, as I can't focus on this enough without missing details. But what I provided is the most I can provide at this time. Unless you have other info that would help. This is on a Moto G Stylus 2023, Android 13, and a BTech UV-PRO.

@mobilinkd
Copy link

mobilinkd commented Dec 17, 2024 via email

@mobilinkd
Copy link

mobilinkd commented Dec 17, 2024 via email

@na7q
Copy link
Contributor

na7q commented Dec 17, 2024

Well I attempted a change in that order. No error!! ..... but sending data still doesn't seem to make it to the radio on 13. Sometimes it works, 95% of the time it doesn't. Making progress though. I figure what you have might be even better. Still working on understanding things.

2024-12-16 20:24:02.983 28737-28737 APRSdroid.Service pid-28737 D onStartCommand: Intent { act=org.aprsdroid.app.SERVICE cmp=org.aprsdroid.app/.AprsService }, 0, 1
2024-12-16 20:24:02.995 28737-28737 APRSdroid.Service pid-28737 D addPost: null - APRS Service started: Manual Position, TNC (KISS), Bluetooth Low Energy.
2024-12-16 20:24:02.998 28737-28737 APRSdroid.BluetoothLE pid-28737 D BluetoothTncBle.createConnection: 38:D2:00:00:F3:56
2024-12-16 20:24:05.641 2424-4560 NotificationService pid-2424 W Toast already killed. pkg=org.aprsdroid.app token=android.os.BinderProxy@fd17dba
2024-12-16 20:24:09.056 28737-28756 APRSdroid.BluetoothLE pid-28737 D Connected to GATT server
2024-12-16 20:24:09.065 28737-28756 APRSdroid.BluetoothLE pid-28737 D MTU changed to 155 bytes
2024-12-16 20:24:09.066 28737-32396 APRSdroid....eiveThread pid-28737 D BLEReceiveThread.run()
2024-12-16 20:24:09.066 28737-28737 APRSdroid.Service pid-28737 D onPosterStarted
2024-12-16 20:24:09.068 28737-28756 APRSdroid.KissProto pid-28737 D Backend Name1: TNC (KISS), Bluetooth Low Energy
2024-12-16 20:24:09.069 28737-28737 APRSdroid.FixedPosition pid-28737 D start: periodic=false single=false
2024-12-16 20:24:10.869 28737-28756 APRSdroid.BluetoothLE pid-28737 D Services discovered and MTU request initiated
2024-12-16 20:24:10.927 28737-28756 APRSdroid.BluetoothLE pid-28737 D Notification enabled
2024-12-16 20:24:13.222 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:24:15.281 28737-28737 APRSdroid.Service pid-28737 D onStartCommand: Intent { act=org.aprsdroid.app.ONCE cmp=org.aprsdroid.app/.AprsService }, 0, 2
2024-12-16 20:24:15.282 28737-28737 APRSdroid.Service pid-28737 D onPosterStarted
2024-12-16 20:24:15.284 28737-28737 APRSdroid.FixedPosition pid-28737 D start: periodic=false single=false
2024-12-16 20:24:15.285 28737-28737 APRSdroid.Service pid-28737 D packet: NA7Q-7>T6QPRT,WIDE1-1,WIDE2-1:3>5l �[/[1
2024-12-16 20:24:15.287 28737-29266 APRSdroid.KissProto pid-28737 D writePacket: NA7Q-7>T6QPRT,WIDE1-1,WIDE2-1:3>5l �[/[1
2024-12-16 20:24:15.288 28737-29266 APRSdroid.BluetoothLE pid-28737 D write 0xC000A86CA2A0A4A8E09C826EA240406EAE92888A624062AE92888A64406303F060333E356C201C5B2F605B31C0
2024-12-16 20:24:15.288 28737-29266 APRSdroid.BluetoothLE pid-28737 D Flushed. Send to BLE
2024-12-16 20:24:15.293 28737-29266 APRSdroid.Storage pid-28737 D got NA7Q-7(46170670, -123570830)/[ -> [1
2024-12-16 20:24:15.488 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:24:15.497 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:24:15.501 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))

@mobilinkd
Copy link

mobilinkd commented Dec 17, 2024 via email

@na7q
Copy link
Contributor

na7q commented Dec 17, 2024

So like mine, no error. But same issues on sending data to the radio. As well as the unmentioned disconnect of the gatt server after sending.

2024-12-16 20:38:05.708 9575-9575 APRSdroid.Service pid-9575 D onStartCommand: Intent { act=org.aprsdroid.app.SERVICE cmp=org.aprsdroid.app/.AprsService }, 0, 1
2024-12-16 20:38:05.713 9575-9575 APRSdroid.Service pid-9575 D addPost: null - APRS Service started: Manual Position, TNC (KISS), Bluetooth Low Energy.
2024-12-16 20:38:05.715 9575-9575 APRSdroid.BluetoothLE pid-9575 D BluetoothTncBle.createConnection: 38:D2:00:00:F3:56
2024-12-16 20:38:05.932 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:38:08.320 2424-4068 NotificationService pid-2424 W Toast already killed. pkg=org.aprsdroid.app token=android.os.BinderProxy@a2e6d8
2024-12-16 20:38:13.522 9575-10158 APRSdroid.BluetoothLE pid-9575 D Connected to GATT server
2024-12-16 20:38:13.530 9575-9585 APRSdroid.BluetoothLE pid-9575 D MTU changed to 155 bytes
2024-12-16 20:38:15.539 9575-10158 APRSdroid.BluetoothLE pid-9575 D Services discovered and characteristics set
2024-12-16 20:38:15.618 9575-10158 APRSdroid.BluetoothLE pid-9575 D Notification enabled
2024-12-16 20:38:15.708 9575-9585 APRSdroid.BluetoothLE pid-9575 D MTU changed to 155 bytes
2024-12-16 20:38:15.709 9575-9585 APRSdroid.KissProto pid-9575 D Backend Name1: TNC (KISS), Bluetooth Low Energy
2024-12-16 20:38:15.711 9575-10841 APRSdroid....eiveThread pid-9575 D BLEReceiveThread.run()
2024-12-16 20:38:15.711 9575-9575 APRSdroid.Service pid-9575 D onPosterStarted
2024-12-16 20:38:15.717 9575-9575 APRSdroid.FixedPosition pid-9575 D start: periodic=false single=false
2024-12-16 20:38:15.933 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:38:17.102 9575-9575 APRSdroid.Service pid-9575 D onStartCommand: Intent { act=org.aprsdroid.app.ONCE cmp=org.aprsdroid.app/.AprsService }, 0, 2
2024-12-16 20:38:17.103 9575-9575 APRSdroid.Service pid-9575 D onPosterStarted
2024-12-16 20:38:17.104 9575-9575 APRSdroid.FixedPosition pid-9575 D start: periodic=false single=false
2024-12-16 20:38:17.105 9575-9575 APRSdroid.Service pid-9575 D packet: NA7Q-7>T6QPRT,WIDE1-1,WIDE2-1:3>5l �[/[1
2024-12-16 20:38:17.105 9575-10242 APRSdroid.KissProto pid-9575 D writePacket: NA7Q-7>T6QPRT,WIDE1-1,WIDE2-1:3>5l �[/[1
2024-12-16 20:38:17.106 9575-10242 APRSdroid.BluetoothLE pid-9575 D write 0xC000A86CA2A0A4A8E09C826EA240406EAE92888A624062AE92888A64406303F060333E356C201C5B2F605B31C0
2024-12-16 20:38:17.106 9575-10242 APRSdroid.BluetoothLE pid-9575 D Flushed. Send to BLE
2024-12-16 20:38:17.109 9575-10242 APRSdroid.Storage pid-9575 D got NA7Q-7(46170670, -123570830)/[ -> [1
2024-12-16 20:38:17.316 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:38:17.322 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:38:17.328 9623-9623 MotoDisplay pid-9623 I onNotificationPosted: StatusBarNotification(pkg=org.aprsdroid.app user=UserHandle{0} id=1 tag=null key=0|org.aprsdroid.app|1|null|10612: Notification(channel=status shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0x00000000 vis=PRIVATE))
2024-12-16 20:38:20.551 9575-9585 APRSdroid.BluetoothLE pid-9575 D Disconnected from GATT server

@mobilinkd
Copy link

Just a quick update for those following along.

NA7Q and I have been working offline on getting a few issues worked out. He is having an issue with his Android 13 device (Moto G Stylus) that I have not been able to replicate on my Android 13 device (Samsung Tab S7). The issue is that on BLE write for TX, the Moto disconnects with status 22 (GATT_CONN_TERMINATE_LOCAL_HOST).

Current issue list:

  • Behind the scenes MTU change by some Android 13 devices cause connection state issues. (Attempt to start a started thread.) FIXED.
  • Disconnect with status 22 on Moto G Stylus Android 13.
  • Disconnects due to out of range cause connection state issues. (Attempt to start a started thread.) (FIX Pending)
  • APRSdroid disconnected but device remained in connected state; both Android and TNC showed connected. (Need to reproduce.)
  • APRSdroid SPP/RFCOMM has retry logic on lost connection. Need to implement same for BLE. (TODO)
  • Mobilinkd TNC3/TNC4 stops BLE advertising after some time. Attempts to initiate first connection after advertising stops results in Service Discovery failure. Need to enable/disable Bluetooth on Android to clear the cached BLE service data to establish connection. (May need fix in TNC BT module configuration or TNC firmware.) (Note: this has never been an issue on iOS.)
  • Disconnects for unknown reasons not yet handled properly. (FIX Pending)

The 2 other Android BLE apps I have are a BLE Config App and an M17 voice app. Both work well but the apps manage the connection lifecycle a little differently. The config app has a rather complicated BLE connection state machine due to the need to disconnect on Stop and reconnect on Start.

One thing I still need to test is the behavior when two apps attempt to connect to the device at the same time. Unlike SPP, GATT will allow it.

@mumrah
Copy link

mumrah commented Dec 18, 2024

Are there instructions on testing the BLE build? I'd like to test it with my BLE KISS board.

@mobilinkd
Copy link

@mumrah Not yet. You can try one of my APKs (links above), select connection type "Bluetooth Low Energy", select a paired BLE device in the TNC Bluetooth Device dialog, then "Start Tracking".

@na7q
Copy link
Contributor

na7q commented Dec 19, 2024

Feel free to try my build. It's feature packed, so beware of all the extra settings. Just added new stuff in the last hour here.
https://na7q.com/wp-content/uploads/2024/12/aprsdroid-release-noapi.apk

@mobilinkd
Copy link

I finally am moved into the new workshop and found all of my older Android devices on which to test. It seems that for the most part we have a working BLE implementation. I am running into an issue with Android 6.0. The first time it attempts to enable notification, it invariably fails to invoke the onDescriptorWrite() callback. There is no timeout or any indication that the request has failed. The result is that the BLE service is stuck waiting for a callback that never comes. It never completes the connection setup. The user is never notified that the connection is in limbo. This failure on Android 6.0 happens on my (Kotlin) BLE config app as well.

Disconnecting/closing the connection then re-establishing the connection seems to work, although I have seen a spurious "CONNECTION TERMINATED DUE TO MIC FAILURE" (status 61) on 6.0 as well. This may be due to a corrupted write caused by an internal race conditions in the older Android BLE stack.

I think we have a couple of options here. The simplest is to just not support BLE connections until API level 26 (Android 8.0) in APRSdroid. Otherwise we could add a timer to the requests in the BLE service to detect the missing callback and retry. My concern here is that 6.0 is known to have race conditions, so we may need to add a timer and retry logic to every GATT call.

I have tested on:

  • Phones
    • Android 6.0.1 (Nexus 5)
    • Android 8.1 (Nexus 5X)
    • Android 11 (Pixel 2)
    • Android 14/15 (Pixel 8)
  • Tablets
    • Android 8.1 (Pixel C)
    • Android 13 (Galaxy Tab S7)

I have only done preliminary testing with the tablets, but they seem fine.

If you have tested on another Android version, please document the version and device here.

Any thoughts on how we want to handle the issue on Android 6.0? My preference is to only offer BLE as a connection option on Android 8.0 and greater.

@na7q
Copy link
Contributor

na7q commented Feb 11, 2025

I fully agree that supporting Android 8+ would be the best approach. As realistically most users at this point will be at this OS version or higher. If you're using a BLE device, I'd suspect you're using a newer device anyway.

@hessu
Copy link

hessu commented Feb 11, 2025

I agree as well, just ignore the older devices. There is a limit on how much effort should be put into supporting obsolete versions.

I would personally not care much about versions which no longer get security updates from the upstream (I believe Android 11 and lower, currently). But this is not my project, of course.

@mobilinkd
Copy link

Recap of the above issue list:

  • Behind the scenes MTU change by some Android 13 devices cause connection state issues. (Attempt to start a started thread.) FIXED.
  • Disconnect with status 22 on Moto G Stylus Android 13. UNABLE TO REPRODUCE.
  • Disconnects due to out of range cause connection state issues. (Attempt to start a started thread.) FIXED
  • APRSdroid disconnected but device remained in connected state; both Android and TNC showed connected. ROOT CAUSE: Nordic "nRF Connect" app used for BLE diagnostics was holding the connection open.
  • APRSdroid SPP/RFCOMM has retry logic on lost connection. Need to implement same for BLE. DONE
  • Mobilinkd TNC3/TNC4 stops BLE advertising after some time. Attempts to initiate first connection after advertising stops results in Service Discovery failure. Need to enable/disable Bluetooth on Android to clear the cached BLE service data to establish connection. TBD. Unable to reproduce consistently.
  • Disconnects for unknown reasons not yet handled properly. FIXED

Additional changes:

  • Verbose connection logging added to match the logging available from SPP connection. This provides valuable diagnostics when Connection Logging is ticked.
  • Ignore INVALID_PDU when writing descriptor to enable notification.

The above logging change has resulted in 2 new string values which may benefit from translations since they are visible to the user.

To Do:

  • Limit BLE to Android 8.0 and greater.

https://github.com/mobilinkd/aprsdroid/releases/tag/MOBILINKD_BLE_4

There's a signed APK file with that release which uses my maps API key, so google maps should work in this one. You will need to uninstall APRSdroid before installing that package.

The only thing that has not yet been done is limiting BLE to Android 8.0 and greater. The simple option here is to change minSdkVersion from 14 (Android 4.0) to 26 (Android 8.0). Otherwise I suspect we'll need some changes in AprsBackend.scala to conditionally include/exclude BLE support, but I don't know the proper way to do that. If that's the preferred route, I will need help with making that change.

@mobilinkd
Copy link

For anyone interested, there is an Android BLE Config App for Mobilinkd TNCs available for testing here: https://github.com/mobilinkd/BLEConfig/releases/tag/v1.0.0

Version 1.1.0 will be coming soon, but this works well enough that people can test it out. Please report any problems with this app at https://github.com/mobilinkd/BLEConfig/issues rather than here.

@mobilinkd
Copy link

This is the first real "release candidate" build. All known BLE issues in APRSdroid are addressed.

https://github.com/mobilinkd/aprsdroid/releases/tag/MOBILINKD_BLE_5

I have been working with @na7q offline. He alerted me to an RX issue.

This release addresses two issues:

  • Disable BLE below Android 8.0
  • Fix missing RX packets until first TX.

There is a signed APK with Google Maps support like the previous release. @mumrah please test if you can. Install the APK available from the link above.

Pair the TNC to the Android device.

In Preferences:

  • Set the Connection Protocol to TNC (KISS)
  • Set the Connection Type to Bluetooth Low Energy
  • Set the TNC Bluetooth Device to the paired TNC
  • Enable Connection Logging

In Log view:

  • Start Tracking

You should see the connection established and a packet transmitted.

@mumrah
Copy link

mumrah commented Feb 15, 2025

@mobilinkd it seems to be working. I did have to "pair" to my device using the SPP advert, which seems incorrect. There could very well be something awry with my advertisements, though it does work reliably on iOS with aprs.fi.

Edit: I wonder if there's some caching within Android regarding device hardware IDs and the adverts. I'll see if I can try a fresh Pico W without the SPP code enabled and see what happens.


Here are the logs from my BLE device with some commentary:

I tried connecting to the "NinoBLE" device, which is from the GAP advertisement.

ATT connected. address=20041c98 type=1 handle=0040
Unhandled event E7
Unhandled event 3E
Unhandled event 3E
Unhandled event 3E
Unhandled event FF
att_write_callback conn=0040 attr=0000
ATT disconnected. handle=0040

This did not work. The Android device never completed the connection. When I select the SPP advertised device "SPP KISS 00:00:00:00..." it is able to connect as before.

Unhandled event 04
Unhandled event 0F
Unhandled event 1B
Unhandled event 38
Unhandled event E0
Unhandled event 32
Unhandled event 31
SSP User Confirmation Request with numeric value '030851'
SSP User Confirmation Auto accept
Unhandled event E1
Unhandled event 36
Unhandled event 18
att_write_callback conn=0000 attr=0000
ATT disconnected. handle=000b    // Not sure what connection this was. The only connection I see before this is handle 0040

Once I selected "Start Tracking" I see the ATT connection, MTU exchange and the test message received from the device. This message is displayed in APRSDroid

ATT connected. address=20041c98 type=1 handle=0040
Unhandled event E7
Unhandled event 3E
Unhandled event 3E
Unhandled event 3E
Unhandled event FF
Unhandled event 3E
Unhandled event 3E
Unhandled event 3E
att_write_callback conn=0040 attr=0016
ble_connection_callback(0)
ATT MTU exchange complete. handle=0040 MTU=517
Unhandled event 3E
Unhandled event 3E
RX TNC -> BLE
RX value to send to BLE client (43 bytes):
C0 00 00 1F 04 20 EB 00 00 00 35 00 00 00 31 00 00 00 4D 75 01 03 7A 00 C4 00 1D 00 00 00 00 23 02 88 9A 42 03 D0 43 88 04 30 C0 

I then pushed the button on the NinoTNC to simulate a packet and I get this

Got valid KISS from TNC
86 A2 84 8A 8A A0 EA 9C 72 6C 60 60 82 69 03 F0 3D 46 69 72 6D 77 61 72 65 56 72 3A 33 2E 34 32 3D 53 65 72 69 61 6C 4E 6D 62 72 3A 00 00 00 00 00 00 00 00 3D 55 70 74 69 6D 65 4D 69 6C 53 3A 30 30 30 32 39 35 31 35 3D 42 72 64 53 77 63 68 4D 6F 64 3A 30 34 30 36 30 30 30 32 3D 41 58 32 35 52 78 50 6B 74 73 3A 30 30 30 30 30 30 30 30 3D 49 4C 32 50 52 78 50 6B 74 73 3A 30 30 30 30 30 30 30 30 3D 49 4C 32 50 52 78 55 6E 43 72 3A 30 30 30 30 30 30 30 30 3D 54 78 50 6B 74 43 6F 75 6E 74 3A 30 30 30 30 30 30 30 30 3D 50 72 65 61 6D 62 6C 43 6E 74 3A 30 30 30 30 30 30 33 44 3D 4C 6F 6F 70 43 79 63 6C 65 73 3A 30 30 31 33 32 41 42 34 3D 4C 6F 73 74 41 44 43 53 6D 70 3A 30 30 30 30 30 30 30 36 

Dest: CQBEEP-5 (rr=3 c=1 l=0)
Source: N9600A-4 (rr=3 c=0 l=1)
APRS type: 3d
RX TNC -> BLE
RX value to send to BLE client (235 bytes):
C0 00 86 A2 84 8A 8A A0 EA 9C 72 6C 60 60 82 69 03 F0 3D 46 69 72 6D 77 61 72 65 56 72 3A 33 2E 34 32 3D 53 65 72 69 61 6C 4E 6D 62 72 3A 00 00 00 00 00 00 00 00 3D 55 70 74 69 6D 65 4D 69 6C 53 3A 30 30 30 32 39 35 31 35 3D 42 72 64 53 77 63 68 4D 6F 64 3A 30 34 30 36 30 30 30 32 3D 41 58 32 35 52 78 50 6B 74 73 3A 30 30 30 30 30 30 30 30 3D 49 4C 32 50 52 78 50 6B 74 73 3A 30 30 30 30 30 30 30 30 3D 49 4C 32 50 52 78 55 6E 43 72 3A 30 30 30 30 30 30 30 30 3D 54 78 50 6B 74 43 6F 75 6E 74 3A 30 30 30 30 30 30 30 30 3D 50 72 65 61 6D 62 6C 43 6E 74 3A 30 30 30 30 30 30 33 44 3D 4C 6F 6F 70 43 79 63 6C 65 73 3A 30 30 31 33 32 41 42 34 3D 4C 6F 73 74 41 44 43 53 6D 70 3A 30 30 30 30 30 30 30 36 C0 

Next I sent position from APRSDroid. It is received by my device

RX TNC -> BLE
RX value to send to BLE client (44 bytes):
C0 00 82 A0 B4 84 98 8A 80 9C 72 6C 60 60 40 01 03 F0 3E 4E 69 6E 6F 54 4E 43 20 42 4C 45 20 31 31 33 73 20 64 30 30 34 34 33 66 C0 

att_write_callback conn=0040 attr=0012
TX BLE -> TNC:
C0 00 82 A0 88 A4 6A 40 E0 96 68 88 84 B4 40 6A AE 92 88 8A 62 40 63 03 F0 3D 33 36 30 34 2E 32 31 4E 2F 30 37 38 33 34 2E 36 34 57 24 2F 41 3D 30 30 30 33 37 33 20 68 74 74 70 73 3A 2F 2F 61 70 72 73 64 72 6F 69 64 2E 6F 72 67 2F C0 

BLE client has data for us
Got valid KISS from BLE, sending to TNC
Dest: APDR5-0 (rr=3 c=1 l=0)
Source: K4DBZ-5 (rr=3 c=0 l=0)
Repeaters:
WIDE1-1 (rr=3 c=0 l=1)
APRS type: 3d
LAT DEG 36
LAT MIN 04
LAT SEC 21
LON DEG 078
LON MIN 34
LON SEC 64

@na7q
Copy link
Contributor

na7q commented Feb 15, 2025

I have a "scanner" version that you can attempt to connect to a device that isn't "SPP paired", whether it works correctly or not, I'm not sure. I can only test using a UV-PRO with BLE. And it does seem to work, but with the radio in "pairing mode". Not sure how that works on a Pico or others.

@mobilinkd
Copy link

@mumrah Please let me know what device model(s) and Android version(s) you are testing with.

Android does cache service information. There are a couple of ways to flush the cache, but the most reliable I have found is to disable/enable Bluetooth on the Android device.

Android does have a few issues with dual-mode devices. It seems the preferred way to deal with this on the device is to have different MAC addresses for BLE (KISS) and BT Classic (SPP). Alas that's not possible on the module used in the TNC3/TNC4. I deal with this in the BLE Config app by asking the user to disable/enable Bluetooth. That seems to always address the issue.

You can try replicating the issue with the following steps:

  1. Unpair TNC
  2. Disable/enable Bluetooth adapter to clear cache
  3. Pair TNC
  4. Connect to TNC via SPP
  5. Disconnect from TNC
  6. Attempt to connect via BLE KISS

Discovery should fail most of the time at step 6. Repeating just step 2 will allow the connection to proceed.

The same issue also affect SPP connections. However, after connecting via BLE, you won't be able to connect until doing step 1 & 3 (unpair/pair device).

Frustratingly, this behavior really depends on the device and Android version. Step 6 does not fail on my Nexus 5X (Android 8.1). It will always successfully connect via BLE. The behavior appears to have changed on my Pixel 8 after it was upgraded from Android 14 to Android 15.

@mumrah
Copy link

mumrah commented Feb 15, 2025

My test phone is a Pixel 2 XL running Android 11.

With my SPP code removed, I cannot pair to the Pico W of BLE. I tried disabling/enabling bluetooth as well as restarting the phone. No joy.

Here is the unsuccessful pairing:

15:24:38:873 -> ATT connected. address=20041c98 type=1 handle=0040
15:24:38:873 -> Unhandled event E7
15:24:38:874 -> Unhandled event 3E
15:24:38:874 -> Unhandled event 3E
15:24:38:875 -> Unhandled event 3E
15:24:38:876 -> Unhandled event FF
15:25:12:332 -> att_write_callback conn=0040 attr=0000
15:25:12:332 -> ATT disconnected. handle=0040

Here is a successful "pairing" from iOS

15:25:32:032 -> ATT connected. address=20041c98 type=1 handle=0040
15:25:32:032 -> Unhandled event E7
15:25:32:032 -> Unhandled event 3E
15:25:32:033 -> Unhandled event 3E
15:25:32:033 -> Unhandled event 3E
15:25:32:034 -> Unhandled event FF
15:25:32:140 -> Unhandled event 3E
15:25:32:232 -> ATT MTU exchange complete. handle=0040 MTU=527

So it seems that Android is not completing the ATT connection when attempting to pair through the bluetooth settings.

Is it possible for APRSDroid to scan for BLE devices which advertise the correct service UUID?

@mumrah
Copy link

mumrah commented Feb 15, 2025

Another quirk I have noticed with SPP is that it often re-prompts the user to pair (i.e., asks the user to confirm the PIN). I suspect this is because I am not using a persistent address and am letting BTStack create a random hardware address

gap_set_local_name("SPP KISS 00:00:00:00:00:00");

With this BLE version, it seems that it no longer asks me to repair the device.

@na7q
Copy link
Contributor

na7q commented Feb 15, 2025

My test phone is a Pixel 2 XL running Android 11.

With my SPP code removed, I cannot pair to the Pico W of BLE. I tried disabling/enabling bluetooth as well as restarting the phone. No joy.

Here is the unsuccessful pairing:

15:24:38:873 -> ATT connected. address=20041c98 type=1 handle=0040
15:24:38:873 -> Unhandled event E7
15:24:38:874 -> Unhandled event 3E
15:24:38:874 -> Unhandled event 3E
15:24:38:875 -> Unhandled event 3E
15:24:38:876 -> Unhandled event FF
15:25:12:332 -> att_write_callback conn=0040 attr=0000
15:25:12:332 -> ATT disconnected. handle=0040

Here is a successful "pairing" from iOS

15:25:32:032 -> ATT connected. address=20041c98 type=1 handle=0040
15:25:32:032 -> Unhandled event E7
15:25:32:032 -> Unhandled event 3E
15:25:32:033 -> Unhandled event 3E
15:25:32:033 -> Unhandled event 3E
15:25:32:034 -> Unhandled event FF
15:25:32:140 -> Unhandled event 3E
15:25:32:232 -> ATT MTU exchange complete. handle=0040 MTU=527

So it seems that Android is not completing the ATT connection when attempting to pair through the bluetooth settings.

Is it possible for APRSDroid to scan for BLE devices which advertise the correct service UUID?

It is possible to scan for devices in one of my builds. It'll display any BLE device detected.
https://na7q.com/wp-content/uploads/2025/02/aprsdroid-release-noapi-2-13-25-blescan.apk

You can select it from the list and see what happens. I'm still no expert of BLE, so this implementation could be worthless.

@mobilinkd
Copy link

mobilinkd commented Feb 16, 2025

Image

Is it possible for APRSDroid to scan for BLE devices which advertise the correct service UUID?

It can be done, but it makes the code quite a bit more complicated. The code would need to initiate the bonding when selecting an unbonded device. For dual mode devices, the bond state may change when connecting via BLE, which requires a state machine to track the bond state.

I may need to resurrect my nRF dev board with a BLE-only KISS service to test this.

In the meantime, can you instant install nRF Connect and show what it sees being advertised. In the attached screenshot, you can see a device I have been testing with that tries to emulate a BLE only device.

Edit: I should be more explicit. I don't recall seeing any notable pairing/connection issues with the device running in BT 4.0 (LE only) mode.

@mobilinkd
Copy link

mobilinkd commented Feb 16, 2025

I have modified the configuration of the BM78 (a dual mode device) to completely disable the BT Classic side of things so that it only supports BLE. The device with this configuration does not have any issues pairing on Android or connecting with APRSdroid.

Something to note that I discovered in developing the BLE TNC config app: scanning interferes with the BLE connection and service discovery process on Android. Stopping scanning is an async operation with no callback. Because APRSdroid does not do any scanning, it was much more reliable at making BLE connections than my BLE TNC config app until I discovered this.

@mumrah
Copy link

mumrah commented Feb 16, 2025

After trying again today, the BLE "pairing" seems to be working. When I select the device from the Bluetooth settings, it doesn't seem to really do anything, but then the device appears under "previously connected devices" and APRSDroid can find it.

Edit: well I spoke too soon. I tried to unpair and repeat the whole process. Now, I cannot get the Android to pair with the BLE device. However, APRSDroid is still able to send data! I guess someone remembers the last device it used? Under "TNC Bluetooth Device" nothing is present.

@mumrah
Copy link

mumrah commented Feb 16, 2025

@na7q is your "scanner" fork on github?

@na7q
Copy link
Contributor

na7q commented Feb 17, 2025

@na7q is your "scanner" fork on github?

Yes, I put it here.
https://github.com/na7q/aprsdroid/commits/ble4/

@mobilinkd
Copy link

@mumrah I have been doing a bit more testing with my BLE-only device. One thing that is occurring on Android 15 is that as soon as the device is paired using Android's Bluetooth Settings, the device is left in a "connected" state in the Bluetooth Settings. On my device, when connected, advertising is disabled so it cannot be seen by any scanner. This is not a problem with the way APRSdroid currently works because it does not need to scan for the BLE devices. It just pulls the list of paired devices and lets the user choose. As soon as a new connection is open/closed, the device shows "saved" rather than "connected". But with my TNC config app, it is a problem. It cannot see the BLE device at all after pairing. Either the device needs to be reset or Bluetooth needs to be recycled on Android to release the connection and resume advertising.

This does not happen with my dual-mode devices. It shows "connected" momentarily after pairing, then shows "saved" and is always visible.

Just dropping this here in case it helps anyone with connection issues. This is the first time I have spent much time testing BLE-only devices, so this is really helpful.

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

No branches or pull requests