External (USB) Audio on EV3 Using ev3dev

Recently, whilst working on my Braille Reader rebuild, I decided that it was time to investigate options for an external speaker to increase the volume, as the onboard speaker is rather quiet.

Bluetooth Speaker

I’d seen that other people had managed to get Bluetooth speakers working with ev3dev and Pulseaudio, so since I had a BT speaker lying around I thought I’d give it a go. Unfortunately it wouldn’t connect. The EV3 would see it, pair, but any attempt to connect resulted in a Bluez error. It was an old speaker so I decided I’d get a new Anker (I’m a fan of their stuff) one, which would also see duty with my bat detector as well. That also didn’t connect. I’m guessing that the EV3 wants to use a rather old protocol that neither of my speakers supported, which is a shame.

USB Audio

Not one to give up on using an external speaker I wondered if USB audio would work instead. When I changed my phone, I ended up buying an Anker USB-C to 3.5mm headphone DAC:

I did need to use a USB-A to USB-C adapter, but it worked. It worked really well, on the whole. Using USB alone didn’t require Pulseaudio to be installed once I’d set up the .asoundrc file in ~robot:

defaults.pcm.card 1
defaults.ctl.card 1

The downside to using .asoundrc is that if the USB adapter wasn’t plugged in, there’d be no output. So I decided I write a little audio configure function that could be called at the start of my program to detect if there were any additional audio outputs and present a menu of choices, defaulting to the current one in .asoundrc:

If only the onboard audio is available, no menu will be shown and it sets the .asoundrc to use device 0.

Issues

So that I didn’t need to use the USB-A to USB-C adapter I did buy a USB-A audio device:

I wouldn’t suggest using that one. It’s very, very, quiet and the audio quality is poor. It glitches and, worst of all, it chops off the beginning of any sounds played to it which, given I want to use it for speech output, isn’t useful. The Anker unit is just so much better: louder, smoother, and plays the entire sound.

The other issue I came across is that using the ev3.speaker.say(…) function would often crash. It appears that the EV3 isn’t powerful enough to manage sending USB data whilst generating speech via espeak and sending that on to aplay. My answer to that is to manage all that in my own code and temporarily write the speech output to the /run/user/1000 tmpfs directory – that way it won’t damage the SD card:

    def Speak(self, text, filename=None):
        if filename:
            play = False
        else:
            filename = "/run/user/1000/speech.wav"
            play = True

        os.system(
            "espeak -a {} -v {} -s {} -p -w {} '{}'".format(
                self.speech['volume'],
                self.speech['voice'],
                self.speech['speed'],
                self.speech['pitch'],
                filename,
                text
            )
        )

        if play:
            self.ev3.speaker.play_file(filename)

The code above accepts a filename as, for the Braille reader, I pre-generate some cached text prompts.

Utils Code Library

I’ll blog separately, but I refactored all the code that was used to generate the device selection menu et al into its own package so that I could release that. Hopefully it’ll be useful to someone else.

Updated EV3g Mailbox Messaging in Python

Previously I posted an article on handling EV3g binary Mailbox messages under Python3. Since then I have carried on working on this class, along with adding a handler class.

Improved Mailbox Handling

One of the things I wasn’t so keen on with my implementation was the need to specify the type of Mailbox value, i.e. BOOL, NUMBER, or TEXT. Python’s variables have their own type, so the code has been adjusted to use the value’s own type to determine the binary payload format. It is still possible to coerce the type:

from ev3mailbox import EV3Mailbox

float_msg  = EV3Mailbox("Pi", 3.1415)

# Coerce to a string
string_msg = EV3Mailbox("Pie", 3.1415, str)

These changes have made the use of this side of the code much cleaner.

Mailbox I/O Handler

Whilst working on my use case for the original code, I had been working on the principle that I’d be using it in a simple synchronous send/receive pattern. This worked well, until I started using threads at both sides of the ev3dev <-> EV3g link. Once threads were in the mix, there’s a risk that the bt_socket.recv(…) call could actually receive a message that wasn’t destined for that particular call, but for another area of the program.

The solution to the above problem was to implement a receiving thread that deals with all the socket.recv(…) calls. Each message is decoded, and then each Mailbox name has its own FIFO of message objects. It’s a deliberate choice to maintain the list of objects, rather than just their values, so that they can be forced to floats if it’s known they may be very small – see my previous post about that problem.

The new class implements a handler that will deal with all the Bluetooth and thread side of things. All that’s then required to do is call send(…), get(…), or stop() on the class instance:

from ev3messages import EV3Messages

handler = EV3Message(bt_mac_address)

handler.send("Name", value)
msg = handler.get("ANOther")
value = msg.value

handler.stop()

The calls to send(…) and get(…) should (!) be thread safe, so calls to get(…) wait on receiving a message of the requested name.

Code Repo

The repo is available from: https://gitlab.com/Jander/ev3-mailbox-python and is released under the GPLv3.