The Linkplay A31 is a WiFi audio streaming module (System on Module, SoM) developed to be used in wireless speakers and wireless audio systems. The module seems to be sold under many different names (Rakoit, Wiimu).
Because the module itself does not have a DAC and amplifier, it is typically connected to another board in many consumer products. For example, in the DYON Area L (Serie 2) WiFi speaker it is connected to an C.AP8064.05 amplifier board (PCB back, PCB front with serial cable attached to the A31, MVsilicon AP8064 MCU datasheet).
The aim of this project is to replace the vendor firmware by an open source solution. With the following instructions you can install an OpenWrt system with network streaming capabilities for Apple Airplay devices.
Please do me a favor: 👍 If you use any information or code you find here, please link back to this page. ⭐ Also, please consider to star this project. I really like to keep track of who is using this to do creative things, especially if you are from other parts of the world. 😃 You are welcome to open an issue to report on your personal success project and share it with others.
The Linkplay A31 V04 module (PCB front, datasheet) is based on a MediaTek MT7688AN MCU (32-bit MIPS 24KEc) with 64MB RAM and 16MB W25Q128 SPI NOR flash. The module offers pins for I2S, I2C, ethernet, USB, UART and 5 GPIOs.
The module has an undocumented serial port used as Linux console ttyS1 (RX/TX pins next to the WiFi antenna, full PCB). The serial port works with 5V (not 3V3) and 57600 8N1.
✋ Some (newer) modules do not output anything to the serial port. This likely can be fixed by patching the bootloader.
The module uses a patched (and sometimes crippled) version of U-Boot 1.1.3 (Ralink UBoot Version 4.3.0.0), a (MediaTek SDK) patched Linux kernel 2.6.36 and a weird combination of proprietary and OSS/GNU operating system components.
Other users thankfully have comprehensivly analysed the system and vendor application layer: Crymeiriver started reverse engineering some years ago, Jan21493 added a ton of more information and AndersFluur wrote an inofficial API documentation.
mt7628an_linkplay_a31.dts is a Device Tree Source file which describes the hardware components of the Linkplay A31 WiFi module.
The DTS configures the A31 as an I2S slave (see A31 manual section 2.3) using a generic S/PDIF transmitter. For I2S master configuration, you'll need to add a real codec, see DTS files of other boards if needed.
Original firmware has a 'firmware' partition with size 0x730000 at 0x250000, a jffs2 'user' partitition with size 0x80000 at 0x980000 and a jffs2 'user2' partition with size 0x600000 at 0xa00000. The DTS drops both user partitions in favour of a bigger 'firmware' partition (writeable, with OverlayFS).
The WiFi module GPIOs 1-5 (PCB pins 23, 22, 27, 26, 19) are wired to MT7688AN pins 14-18, one can access them from Linux via GPIOs 494 (GPIO2), 495 (GPIO1), 496 (GPIO4), 497 (GPIO3) and 498 (GPIO5).
Make sure to read the general OpenWrt docs here and here and at least install the necessary build packages.
Then proceed like this:
git clone https://github.com/hn/linkplay-a31.git
git clone https://git.openwrt.org/openwrt/openwrt.git
cd openwrt
git checkout v23.05.5
./scripts/feeds update -a
./scripts/feeds install -a
../linkplay-a31/openwrt-linkplay-a31/prepare-openwrt-a31.sh
# edit 'files/etc/config/wireless' and set your WiFi credentials
make -j$(nproc) defconfig download clean world
Output images are found in ./bin/targets/ramips/mt76x8
.
The script includes a patch which works around the MT7688 from crashing the entire system when I2S audio playback is stopped manually.
The OpenWrt image can easily be tested without writing anything to flash (no permanent modifications). You can use self-compiled packages or packages provided by this project.
Install Kermit (apt-get install ckermit
),
connect the serial console of the A31
and execute ../linkplay-a31/linkplay-a31-serial-upload-kermit
(change port /dev/ttyUSB0
if necessary).
Reset (e.g. powercycle) the A31.
The Kermit script will interrupt the A31 boot process,
upload the openwrt-ramips-mt76x8-linkplay_a31-initramfs-kernel.bin
image
via loadb
and boot the system from RAM (bootm
).
Be patient, the serial upload will take roughly 20-25 minutes to finish.
You might want to edit /etc/config/wireless
within the
test system (followed by wifi reconf
) to set your WiFi credentials.
The image contains the package shairport-sync-mini
and
therefore you can use an Apple device to stream music to it.
Alternativly, you can use aplay
for wav-files or mpg123
for music streams (non-ssl only). Do not download large files
to the system, space is extremely limited.
With fw_printenv
one can list the NVRAM contents of the vendor firmware
(config fw_env.config).
It is recommended to boot the system via serial and flash the image from within the running system with
mtd -r write /tmp/openwrt-ramips-mt76x8-linkplay_a31-squashfs-sysupgrade.bin firmware
Make sure that the time of the newly installed system is correct, otherwise you won't be able to download HTTPS content due to certificate validity problems.
✋ Flash memory has a limited number of write cycles. Avoid unnecessary system upgrades or writing large files (always use tmpfs in /tmp if possible).
When installed to flash, the system sets up a rescue access point
if there is no WiFi connectivity (e.g. wrong credentials) three minutes after booting.
The SSID is LINKPLAY
and passphrase is A31A31A31
.
IP address for SSH connection is 192.168.31.1
(no DHCP server, you have to set IP manually on your client).
After 10 minutes of inactivity the rescue access point will be shut down (sytem reboot).
The flash space of the A31 module is extremely limited. Therefore the OpenWrt config includes only a minimal set of software packages (e.g. no web interface). After installation, you (only) have roughly 6 MB of free space available.
While it is possible to install additional software, many packages (e.g. gmrender-resurrect, OwnTone, ...) depend on Gstreamer and FFmpeg libraries, which are way to big to be installed on the module.
I suggest to install mpd-mini (config diff here, and you have to add user 'mpd' to the 'audio' group). HTTPS-streams seem to overload the cpu and tend to be unstable.
The vendor's web interface offers the option of uploading a binary firmware update to the device. However, the vendor has taken some precautions to ensure that only files in a certain format are accepted:
- The operating system (OS) identifier in the uImage kernel header must be set to 'SVR4' instead of the standard 'Linux'.
- The root filesystem has to be wrapped into an uImage as well (unlike OpenWrt does this).
- Various length checks are enforced.
openwrt-ramips-mt76x8-linkplay_a31-squashfs-firmware.bin is a firmware file that fulfills these requirements by patching the kernel and various headers and inserting some padding. This allows it to be flashed via the devices's web interface. Use the rescue access point to access the system after web flashing.
If you want to rebuild the web firmware image, you have to pass
the -fw
option to the prepare-openwrt-a31.sh script.
The Linkplay A31 does not have a DAC and amplifier, so it
is necessary to control volume and mute status via
serial commands (e.g. AXX+VOL+007
, sent via ttyS0 57600 8N1
to the amp board).
Audio is not always that straightforward with Linux, so this project uses a possibly confusing (some might call it clever) ALSA setup to control the amplifier status:
- The I2S S/PDIF output device does not have a mixer, so client programs (e.g. shairport-sync) would not be able to control the volume.
- Therefore a Linux dummy sound device (with dummy mixer) is activated (
kmod-sound-dummy
). - With a specially crafted asound.conf, default audio output is routed to the I2S device (the amp board accepts audio format S16, rate 44,100 Hz) and default audio control is routed to the dummy device (mixer). Client programs happily change the volume of the dummy mixer, but the real audio data is passed unchanged (volume 100%) to the amplifier board (assuming that the client program itself does no software mixing, it is recommended to disable any client volume/mixer processing).
- A Linkplay Emulator Daemon is installed to monitor both devices and send serial control commands to the amplifier board when sound is played or the volume changes.
The terms 'emulator' and 'daemon' are a bit exaggerated, the code only emulates three serial commands and does not daemonize itself, but hey, it works :)
Some (newer) A31 module versions are shipped with a crippled U-Boot bootloader which does not output anything to the serial port. It seems that the vendor does not want users to modify the software on the device themselves.
According to Jan21493's research the most recent bootloader version is uboot_v632.img.
If you have a look at the source code of a bootloader of a comparable board one can see the following initialization sequence:
timer_init();
env_init(); /* initialize environment */
init_baudrate(); /* initialze baudrate settings */
serial_init(); /* serial communications setup */
console_init_f();
Since uboot_v632.img
is distributed without source code,
a deeper understanding can be obtained by disassembling the binary file
(function names have been annotated after thorough analysis):
bc001164 10 00 bc 8f lw gp,local_40(sp)
bc001168 84 02 99 8f lw t9,0x284(gp)=>->FUN_timer_init_bc004358
bc00116c 09 f8 20 03 jalr t9=>FUN_timer_init_bc004358
bc001170 00 00 00 00 _nop
bc001174 10 00 bc 8f lw gp,local_40(sp)
bc001178 6c 01 99 8f lw t9,0x16c(gp)=>->FUN_env_init_bc011990
bc00117c 09 f8 20 03 jalr t9=>FUN_env_init_bc011990
bc001180 00 00 00 00 _nop
bc001184 10 00 bc 8f lw gp,local_40(sp)
bc001188 00 e1 02 34 ori v0,zero,0xe100
bc00118c 08 00 42 af sw v0,local_30(k0)
bc001190 c4 00 99 8f lw t9,0xc4(gp)=>->FUN_console_init_f_bc00f8ec
bc001194 09 f8 20 03 jalr t9=>FUN_console_init_f_bc00f8ec
bc001198 00 00 00 00 _nop
bc00119c 10 00 bc 8f lw gp,local_40(sp)
bc0011a0 ff df 02 3c lui v0,0xdfff
bc0011a4 ff ff 42 34 ori v0,v0,0xffff
bc0011a8 68 00 99 8f lw t9,0x68(gp)=>->FUN_dram_cali_bc003b90
bc0011ac 24 c8 22 03 and t9,t9,v0
bc0011b0 09 f8 20 03 jalr t9=>SUB_9c003b90
bc0011b4 00 00 00 00 _nop
One quickly can see that the calls to init_baudrate
and serial_init
are missing.
An obvious approach would be to replace the jump table call at offset
0xc4
to console_init_f
(this function is not absolutely necessary) with a call of serial_init
.
Unfortunately, the jump table does not contain an entry for serial_init
,
as this function is intentionally not used by the manufacturer's firmware.
But fortunately there is an entry for serial_setbrg
in the jump table at offset 0x30c
,
which does virtually nothing else than serial_init
.
So the minimally invasive solution is therefore to replace the (little-endian)
bytes 0xc400
at position 0x1190
with 0x0c03
, for example as follows:
$ wget http://silenceota.linkplay.com/wifi_audio_image/2ANRu7eyAEYtoo4NZPy9dL/uboot_v632.img
$ md5sum uboot_v632.img
8e0445af74e108eace0c90e849c37723
$ cp -v uboot_v632.img uboot_v632-patched.img
'uboot_v632.img' -> 'uboot_v632-patched.img'
$ echo -n -e "\x0C\x03" | dd seek=4496 bs=1 count=2 conv=notrunc of=uboot_v632-patched.img
2 bytes copied, 0.000151282 s
$ md5sum uboot_v632-patched.img
7c9cbe63d6c0f7f6425eb1c1246d7d0b
The modified firmware can be written to flash memory e.g. by executing
mtd_write -w write /tmp/uboot_v632-patched.img Bootloader
on the A31 module.
There are (at least) two different hardware revisions of this speaker:
Series 1 has a rectangular label (8x5 cm) and can be opened from bottom of the speaker enclosure. It consists of a WiiMu A21 WiFi module (MT7620A MCU) and a C.8064.01 amp board. No research has yet been undertaken with this model.
Series 2 has a square label (8x8 cm, with "Serie 2" printed on it) and can be opened from the back of the speaker enclosure, the screws are hidden behind the edges of the label.
The WiFi speaker plays a particularly annoying
welcome message ("Geniesse WiFi Musik" in german) when you
switch to the Airplay source (AXX+PLM+001
).
This message is not stored on the A31 module but on the amp board.
Since it was not possible to stop this dumb behavior via serial commands, a more stringent solution had to be found.
The firmware of the MVsilicon AP8064 MCU is stored in a 4MB GD25Q32 SPI NOR flash which can be read with a SOIC-8 clip like this:
Haven't looked closely, but the dump doesn't seem to contain a known kernel or file system, everything appears to be proprietary.
By examining the dump more carefully one sees that there is some
kind of 'table of contents' represented by text-offset-length values
at flash position 0x100000
:
ToC 01, Signature: WIFI, Offset: 100bfd, Length: 32f8
ToC 02, Signature: USB_, Offset: 103ef5, Length: 2c9c
ToC 03, Signature: WTCN, Offset: 106b91, Length: 2e9c
ToC 04, Signature: AUX_, Offset: 109a2d, Length: 316d
ToC 05, Signature: CARD, Offset: 10cb9a, Length: 2de1
ToC 06, Signature: FMRD, Offset: 10f97b, Length: 2442
ToC 07, Signature: CNND, Offset: 111dbd, Length: 1d07
...
ToC 29, Signature: 3DFF, Offset: 13fd41, Length: b36c
ToC 30, Signature: WIfi, Offset: 14b0ad, Length: 1631a
ToC 31, Signature: Aux_, Offset: 1613c7, Length: 15f05
ToC 32, Signature: CNnd, Offset: 1772cc, Length: c800
ToC 33, Signature: CNlt, Offset: 183acc, Length: 1094e
ToC 34, Signature: LWbt, Offset: 19441a, Length: 22b1a
ToC 35, Signature: CHrg, Offset: 1b6f34, Length: b398
ToC 36, Signature: PWou, Offset: 1c22cc, Length: 121cc
ToC 37, Signature: WTcn, Offset: 1d4498, Length: 17399
ToC 38, Signature: FMup, Offset: 1eb831, Length: af83
If you extract the data, you exactly get 38 valid MP3 files with audio data for all types of events ('Charging', 'Connection lost' etc.).
The annoying WiFi connection message is stored in ToC 30 (Signature WIfi
) for
german audio and in ToC 01 (Signature WIFI
) for english audio.
The fix is to change the WIfi
signature to something meaningless (FOOO
)
in the ToC and flash the modified image file back to the flash chip
(only 4 bytes changed compared to the vendor image).
The amplifier driver then will not be able to find the audio
data and ... you will really be able to enjoy WiFi music undisturbed :)