From a04ab09b48ce386f70ef298bb691b4910d196729 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Thu, 13 Nov 2025 15:18:02 +0100 Subject: [PATCH 01/10] bug #397: 2.1.2 firmware with better echo cancelation. script for easy updating --- .../firmware/99-reachy-mini-audio.rules | 5 +++++ .../assets/firmware/install_udev_rules.sh | 20 +++++++++++++++++++ .../reachymini_ua_io16_lin_v2.1.2.bin | 3 +++ src/reachy_mini/assets/firmware/update.sh | 9 +++++++++ 4 files changed, 37 insertions(+) create mode 100644 src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules create mode 100755 src/reachy_mini/assets/firmware/install_udev_rules.sh create mode 100644 src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin create mode 100755 src/reachy_mini/assets/firmware/update.sh diff --git a/src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules b/src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules new file mode 100644 index 00000000..73e2002b --- /dev/null +++ b/src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules @@ -0,0 +1,5 @@ +# usb 4mic array +SUBSYSTEM=="usb", ATTR{idVendor}=="2886", ATTR{idProduct}=="001a", MODE="0666" + +# reachy mini audio +SUBSYSTEM=="usb", ATTR{idVendor}=="38fb", ATTR{idProduct}=="1001", MODE="0666" diff --git a/src/reachy_mini/assets/firmware/install_udev_rules.sh b/src/reachy_mini/assets/firmware/install_udev_rules.sh new file mode 100755 index 00000000..ed351ef2 --- /dev/null +++ b/src/reachy_mini/assets/firmware/install_udev_rules.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +UDEV_RULE="99-reachy-mini-audio.rules" +SRC_DIR="$(dirname "$0")" +RULE_PATH="$SRC_DIR/$UDEV_RULE" +DEST_PATH="/etc/udev/rules.d/$UDEV_RULE" + +if [[ ! -f "$RULE_PATH" ]]; then + echo "Error: $RULE_PATH not found." + exit 1 +fi + +echo "Installing $UDEV_RULE to /etc/udev/rules.d/..." +sudo cp "$RULE_PATH" "$DEST_PATH" +sudo udevadm control --reload-rules +sudo udevadm trigger + +echo "udev rule installed successfully." \ No newline at end of file diff --git a/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin b/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin new file mode 100644 index 00000000..6314af04 --- /dev/null +++ b/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:661429a93b155cdbfed1e6c214820fd42e3372a08c3930f14fcd4e6db5be6796 +size 933888 diff --git a/src/reachy_mini/assets/firmware/update.sh b/src/reachy_mini/assets/firmware/update.sh new file mode 100755 index 00000000..e6f0f324 --- /dev/null +++ b/src/reachy_mini/assets/firmware/update.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Firmware update script for Reachy Mini +# Usage: ./update.sh +firmware="$1" +if [ -z "$firmware" ]; then + echo "Usage: $0 " + exit 1 +fi +dfu-util -R -e -a 1 -D "$firmware" \ No newline at end of file From 2503dfdb2b88502ccf35c4c2999d78baf1f16cb7 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Thu, 13 Nov 2025 16:58:55 +0100 Subject: [PATCH 02/10] bug #397: new --fix-audio argument for fixing wrongly initialized respeaker --- src/reachy_mini/daemon/app/main.py | 19 ++++++ src/reachy_mini/media/audio_base.py | 79 ++-------------------- src/reachy_mini/media/audio_sounddevice.py | 46 +++++-------- src/reachy_mini/media/audio_utils.py | 14 +++- 4 files changed, 54 insertions(+), 104 deletions(-) diff --git a/src/reachy_mini/daemon/app/main.py b/src/reachy_mini/daemon/app/main.py index e12feb98..50e15724 100644 --- a/src/reachy_mini/daemon/app/main.py +++ b/src/reachy_mini/daemon/app/main.py @@ -55,6 +55,8 @@ class Args: localhost_only: bool | None = None + fix_audio: bool = False + def create_app(args: Args, health_check_event: asyncio.Event | None = None) -> FastAPI: """Create and configure the FastAPI application.""" @@ -152,6 +154,17 @@ def run_app(args: Args) -> None: """Run the FastAPI app with Uvicorn.""" logging.basicConfig(level=logging.INFO) + if args.fix_audio: + """Temporary fix for audio issues with beta version.""" + logging.info("Applying audio fix for beta version.") + + from reachy_mini.media.audio_control_utils import init_respeaker_usb + + respeaker = init_respeaker_usb() + respeaker.write("REBOOT", [1]) + respeaker.close() + logging.debug("Respeaker rebooted.") + health_check_event = asyncio.Event() app = create_app(args, health_check_event) @@ -314,6 +327,12 @@ def main() -> None: help="Set the logging level (default: INFO).", ) + parser.add_argument( + "--fix-audio", + action="store_true", + help="Fix audio issues with beta version (default: False).", + ) + args = parser.parse_args() run_app(Args(**vars(args))) diff --git a/src/reachy_mini/media/audio_base.py b/src/reachy_mini/media/audio_base.py index eeabf89d..c8d6080d 100644 --- a/src/reachy_mini/media/audio_base.py +++ b/src/reachy_mini/media/audio_base.py @@ -5,39 +5,30 @@ """ import logging -import struct from abc import ABC, abstractmethod -from typing import List, Optional +from typing import Optional import numpy as np import numpy.typing as npt -import usb -from libusb_package import get_libusb1_backend + +from reachy_mini.media.audio_control_utils import ReSpeaker, init_respeaker_usb class AudioBase(ABC): """Abstract class for opening and managing audio devices.""" SAMPLE_RATE = 16000 # respeaker samplerate - TIMEOUT = 100000 - PARAMETERS = { - "VERSION": (48, 0, 4, "ro", "uint8"), - "AEC_AZIMUTH_VALUES": (33, 75, 16 + 1, "ro", "radians"), - "DOA_VALUE": (20, 18, 4 + 1, "ro", "uint16"), - "DOA_VALUE_RADIANS": (20, 19, 8 + 1, "ro", "radians"), - } def __init__(self, log_level: str = "INFO") -> None: """Initialize the audio device.""" self.logger = logging.getLogger(__name__) self.logger.setLevel(log_level) - self._respeaker = self._init_respeaker_usb() - # name, resid, cmdid, length, type + self._respeaker: ReSpeaker = init_respeaker_usb() def __del__(self) -> None: """Destructor to ensure resources are released.""" if self._respeaker: - usb.util.dispose_resources(self._respeaker) + self._respeaker.close() @abstractmethod def start_recording(self) -> None: @@ -79,63 +70,6 @@ def play_sound(self, sound_file: str) -> None: """ pass - def _init_respeaker_usb(self) -> Optional[usb.core.Device]: - try: - dev = usb.core.find( - idVendor=0x2886, idProduct=0x001A, backend=get_libusb1_backend() - ) - return dev - except usb.core.NoBackendError: - self.logger.error( - "No USB backend was found ! Make sure libusb_package is correctly installed with `pip install libusb_package`." - ) - return None - - def _read_usb(self, name: str) -> Optional[List[int] | List[float]]: - try: - data = self.PARAMETERS[name] - except KeyError: - self.logger.error(f"Unknown parameter: {name}") - return None - - if not self._respeaker: - self.logger.warning("ReSpeaker device not found.") - return None - - resid = data[0] - cmdid = 0x80 | data[1] - length = data[2] - - response = self._respeaker.ctrl_transfer( - usb.util.CTRL_IN - | usb.util.CTRL_TYPE_VENDOR - | usb.util.CTRL_RECIPIENT_DEVICE, - 0, - cmdid, - resid, - length, - self.TIMEOUT, - ) - - self.logger.debug(f"Response for {name}: {response}") - - result: Optional[List[float] | List[int]] = None - if data[4] == "uint8": - result = response.tolist() - elif data[4] == "radians": - byte_data = response.tobytes() - num_values = (data[2] - 1) / 4 - match_str = "<" - for i in range(int(num_values)): - match_str += "f" - result = [ - float(x) for x in struct.unpack(match_str, byte_data[1 : data[2]]) - ] - elif data[4] == "uint16": - result = response.tolist() - - return result - def get_DoA(self) -> tuple[float, bool] | None: """Get the Direction of Arrival (DoA) value from the ReSpeaker device. @@ -153,7 +87,8 @@ def get_DoA(self) -> tuple[float, bool] | None: if not self._respeaker: self.logger.warning("ReSpeaker device not found.") return None - result = self._read_usb("DOA_VALUE_RADIANS") + + result = self._respeaker.read("DOA_VALUE_RADIANS") if result is None: return None return float(result[0]), bool(result[1]) diff --git a/src/reachy_mini/media/audio_sounddevice.py b/src/reachy_mini/media/audio_sounddevice.py index 6ac053d3..fb138230 100644 --- a/src/reachy_mini/media/audio_sounddevice.py +++ b/src/reachy_mini/media/audio_sounddevice.py @@ -30,8 +30,12 @@ def __init__( self.stream = None self._output_stream = None self._buffer: List[npt.NDArray[np.float32]] = [] - self._output_device_id = self.get_output_device_id("respeaker") - self._input_device_id = self.get_input_device_id("respeaker") + self._output_device_id = self.get_device_id( + ["Reachy Mini Audio", "respeaker"], device_io_type="output" + ) + self._input_device_id = self.get_device_id( + ["Reachy Mini Audio", "respeaker"], device_io_type="input" + ) def start_recording(self) -> None: """Open the audio input stream, using ReSpeaker card if available.""" @@ -181,7 +185,9 @@ def _clean_up_thread() -> None: daemon=True, ).start() - def get_output_device_id(self, name_contains: str) -> int: + def get_device_id( + self, names_contains: List[str], device_io_type: str = "output" + ) -> int: """Return the output device id whose name contains the given string (case-insensitive). If not found, return the default output device id. @@ -189,35 +195,17 @@ def get_output_device_id(self, name_contains: str) -> int: devices = sd.query_devices() for idx, dev in enumerate(devices): - if ( - name_contains.lower() in dev["name"].lower() - and dev["max_output_channels"] > 0 - ): - return idx + for name_contains in names_contains: + if ( + name_contains.lower() in dev["name"].lower() + and dev[f"max_{device_io_type}_channels"] > 0 + ): + return idx # Return default output device if not found self.logger.warning( - f"No output device found containing '{name_contains}', using default." - ) - return self._safe_query_device("output") - - def get_input_device_id(self, name_contains: str) -> int: - """Return the input device id whose name contains the given string (case-insensitive). - - If not found, return the default input device id. - """ - devices = sd.query_devices() - - for idx, dev in enumerate(devices): - if ( - name_contains.lower() in dev["name"].lower() - and dev["max_input_channels"] > 0 - ): - return idx - # Return default input device if not found - self.logger.warning( - f"No input device found containing '{name_contains}', using default." + f"No {device_io_type} device found containing '{name_contains}', using default." ) - return self._safe_query_device("input") + return self._safe_query_device(device_io_type) def _safe_query_device(self, kind: str) -> int: try: diff --git a/src/reachy_mini/media/audio_utils.py b/src/reachy_mini/media/audio_utils.py index cfb64c64..53dbbf17 100644 --- a/src/reachy_mini/media/audio_utils.py +++ b/src/reachy_mini/media/audio_utils.py @@ -14,12 +14,20 @@ def get_respeaker_card_number() -> int: lines = output.split("\n") for line in lines: - if "respeaker" in line.lower() and "card" in line: + if "reachy mini audio" in line.lower() and "card" in line: card_number = line.split(":")[0].split("card ")[1].strip() - logging.debug(f"Found ReSpeaker sound card: {card_number}") + logging.debug(f"Found Reachy Mini Audio sound card: {card_number}") + return int(card_number) + elif "respeaker" in line.lower() and "card" in line: + card_number = line.split(":")[0].split("card ")[1].strip() + logging.warning( + f"Found ReSpeaker sound card: {card_number}. Please update firmware!" + ) return int(card_number) - logging.warning("ReSpeaker sound card not found. Returning default card") + logging.warning( + "Reachy Mini Audio sound card not found. Returning default card" + ) return 0 # default sound card except subprocess.CalledProcessError as e: From 55764174a369dad78ce05ecccf73d74f13f52af0 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Thu, 13 Nov 2025 17:06:18 +0100 Subject: [PATCH 03/10] bug #397: update documentation --- docs/troubleshooting.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c65be22f..349174cd 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,29 +1,36 @@ # Troubleshooting + ## No Microphone Input *For beta only* -There is a known issue where the microphone may not initialize correctly. The best way to resolve this is to reboot the microphone array using [xvf_host](https://github.com/respeaker/reSpeaker_XVF3800_USB_4MIC_ARRAY/tree/master/host_control): +There is a known issue where the microphone may not initialize correctly. You can use the `--fix-audio` argument to force the audio card to reset before starting the daemon. + +Make sure your firmware is updated to version 2.1.2. You may need to run the [update script](../src/reachy_mini/assets/firmware/update.sh). Linux users should also install the proper udev rules using [this script](../src/reachy_mini/assets/firmware/install_udev_rules.sh). Then run: ```bash -xvf_host(.exe) REBOOT 1 +reachy-mini-daemon --fix-audio ``` -Then run [examples/debug/sound_record.py](../examples/debug/sound_record.py) to check that everything is working properly. +Afterwards, run [examples/debug/sound_record.py](../examples/debug/sound_record.py) to check that everything is working properly. If the problem persists, check the connection of the flex cables ([see slides 45 to 47](https://huggingface.co/spaces/pollen-robotics/Reachy_Mini_Assembly_Guide)). + ## Sound Direction of Arrival Not Working *For beta only* -The microphone array requires firmware version 2.1.0 or higher to support this feature. The firmware is located in `src/reachy_mini/assets/firmware/*.bin`. +The microphone array requires firmware version 2.1.0 or higher to support this feature. The firmware files are located in `src/reachy_mini/assets/firmware/*.bin`. Refer to the [Seeed documentation](https://wiki.seeedstudio.com/respeaker_xvf3800_introduction/#update-firmware) for the upgrade process. +A [helper script](../src/reachy_mini/assets/firmware/update.sh) is available for Unix users. + + ## Volume Is Too Low *Linux only* -Check with `alsamixer` that PCM1 is set to 100%. Then use PCM,0 to adjust the volume. +Check in `alsamixer` that PCM1 is set to 100%. Then use PCM,0 to adjust the volume. To make this change permanent: ```bash @@ -32,6 +39,7 @@ amixer -c "$CARD" set PCM,1 100% sudo alsactl store "$CARD" ``` + ## Circular Buffer Overrun Warning When starting a client with `with ReachyMini() as mini:` in Mujoco (--sim mode), you may see the following warning: From 6b388eae472ae481b5d89a222396672de0e7605e Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Thu, 13 Nov 2025 17:25:55 +0100 Subject: [PATCH 04/10] bug #397: mypy --- src/reachy_mini/daemon/app/main.py | 9 ++++++--- src/reachy_mini/media/audio_base.py | 2 +- src/reachy_mini/media/audio_sounddevice.py | 13 +++++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/reachy_mini/daemon/app/main.py b/src/reachy_mini/daemon/app/main.py index 50e15724..3ec74e56 100644 --- a/src/reachy_mini/daemon/app/main.py +++ b/src/reachy_mini/daemon/app/main.py @@ -161,9 +161,12 @@ def run_app(args: Args) -> None: from reachy_mini.media.audio_control_utils import init_respeaker_usb respeaker = init_respeaker_usb() - respeaker.write("REBOOT", [1]) - respeaker.close() - logging.debug("Respeaker rebooted.") + if respeaker is None: + logging.error("Respeaker device not found. Cannot apply audio fix.") + else: + respeaker.write("REBOOT", [1]) + respeaker.close() + logging.debug("Respeaker rebooted.") health_check_event = asyncio.Event() app = create_app(args, health_check_event) diff --git a/src/reachy_mini/media/audio_base.py b/src/reachy_mini/media/audio_base.py index c8d6080d..c3a438c9 100644 --- a/src/reachy_mini/media/audio_base.py +++ b/src/reachy_mini/media/audio_base.py @@ -23,7 +23,7 @@ def __init__(self, log_level: str = "INFO") -> None: """Initialize the audio device.""" self.logger = logging.getLogger(__name__) self.logger.setLevel(log_level) - self._respeaker: ReSpeaker = init_respeaker_usb() + self._respeaker: Optional[ReSpeaker] = init_respeaker_usb() def __del__(self) -> None: """Destructor to ensure resources are released.""" diff --git a/src/reachy_mini/media/audio_sounddevice.py b/src/reachy_mini/media/audio_sounddevice.py index fb138230..386198ec 100644 --- a/src/reachy_mini/media/audio_sounddevice.py +++ b/src/reachy_mini/media/audio_sounddevice.py @@ -30,10 +30,10 @@ def __init__( self.stream = None self._output_stream = None self._buffer: List[npt.NDArray[np.float32]] = [] - self._output_device_id = self.get_device_id( + self._output_device_id = self._get_device_id( ["Reachy Mini Audio", "respeaker"], device_io_type="output" ) - self._input_device_id = self.get_device_id( + self._input_device_id = self._get_device_id( ["Reachy Mini Audio", "respeaker"], device_io_type="input" ) @@ -185,12 +185,17 @@ def _clean_up_thread() -> None: daemon=True, ).start() - def get_device_id( + def _get_device_id( self, names_contains: List[str], device_io_type: str = "output" ) -> int: - """Return the output device id whose name contains the given string (case-insensitive). + """Return the output device id whose name contains the given strings (case-insensitive). + + Args: + names_contains (List[str]): List of strings that should be contained in the device name. + device_io_type (str): 'input' or 'output' to specify device type. If not found, return the default output device id. + """ devices = sd.query_devices() From 796b52987ab83ab58d25c9a62fa8bd069e5ee0c9 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Thu, 13 Nov 2025 17:37:58 +0100 Subject: [PATCH 05/10] bug #397: adding audio host control script --- src/reachy_mini/media/audio_control_utils.py | 424 +++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 src/reachy_mini/media/audio_control_utils.py diff --git a/src/reachy_mini/media/audio_control_utils.py b/src/reachy_mini/media/audio_control_utils.py new file mode 100644 index 00000000..fea8b41b --- /dev/null +++ b/src/reachy_mini/media/audio_control_utils.py @@ -0,0 +1,424 @@ +"""Allows tuning of the XMOS XVF3800 chip. + +Example usage: + + # Read a parameter + python reachy_host.py AUDIO_MGR_OP_L + # Output: + # ReadCMD: cmdid: 143, resid: 35, response: array('B', [0, 8, 0]) + # AUDIO_MGR_OP_L: [0, 8, 0] + + # Write a parameter + python reachy_host.py AUDIO_MGR_OP_L --values 3 0 + # Output: + # Writing to AUDIO_MGR_OP_L with values: [3, 0] + # WriteCMD: cmdid: 15, resid: 35, payload: [3, 0] + # Write operation completed successfully + +More details about the parameters is available at: +https://www.xmos.com/documentation/XM-014888-PC/html/modules/fwk_xvf/doc/user_guide/AA_control_command_appendix.html +""" + +import argparse +import logging +import struct +import sys +import time +from typing import Any, Optional + +import usb.core +import usb.util +from libusb_package import get_libusb1_backend + +CONTROL_SUCCESS = 0 +SERVICER_COMMAND_RETRY = 64 + +# name, resid, cmdid, length, type +PARAMETERS = { + # APPLICATION_SERVICER_RESID commands + "VERSION": (48, 0, 3, "ro", "uint8"), + "BLD_MSG": (48, 1, 50, "ro", "char"), + "BLD_HOST": (48, 2, 30, "ro", "char"), + "BLD_REPO_HASH": (48, 3, 40, "ro", "char"), + "BLD_MODIFIED": (48, 4, 6, "ro", "char"), + "BOOT_STATUS": (48, 5, 3, "ro", "char"), + "TEST_CORE_BURN": (48, 6, 1, "rw", "uint8"), + "REBOOT": (48, 7, 1, "wo", "uint8"), + "USB_BIT_DEPTH": (48, 8, 2, "rw", "uint8"), + "SAVE_CONFIGURATION": (48, 9, 1, "wo", "uint8"), + "CLEAR_CONFIGURATION": (48, 10, 1, "wo", "uint8"), + # AEC_RESID commands + "SHF_BYPASS": (33, 70, 1, "rw", "uint8"), + "AEC_NUM_MICS": (33, 71, 1, "ro", "int32"), + "AEC_NUM_FARENDS": (33, 72, 1, "ro", "int32"), + "AEC_MIC_ARRAY_TYPE": (33, 73, 1, "ro", "int32"), + "AEC_MIC_ARRAY_GEO": (33, 74, 12, "ro", "float"), + "AEC_AZIMUTH_VALUES": (33, 75, 4, "ro", "radians"), + "TEST_AEC_DISABLE_CONTROL": (33, 76, 1, "wo", "uint32"), + "AEC_CURRENT_IDLE_TIME": (33, 77, 1, "ro", "uint32"), + "AEC_MIN_IDLE_TIME": (33, 78, 1, "ro", "uint32"), + "AEC_RESET_MIN_IDLE_TIME": (33, 79, 1, "wo", "uint32"), + "AEC_SPENERGY_VALUES": (33, 80, 4, "ro", "float"), + "AEC_FIXEDBEAMSAZIMUTH_VALUES": (33, 81, 2, "rw", "radians"), + "AEC_FIXEDBEAMSELEVATION_VALUES": (33, 82, 2, "rw", "radians"), + "AEC_FIXEDBEAMSGATING": (33, 83, 1, "rw", "uint8"), + "SPECIAL_CMD_AEC_FAR_MIC_INDEX": (33, 90, 2, "wo", "int32"), + "SPECIAL_CMD_AEC_FILTER_COEFF_START_OFFSET": (33, 91, 1, "rw", "int32"), + "SPECIAL_CMD_AEC_FILTER_COEFFS": (33, 92, 15, "rw", "float"), + "SPECIAL_CMD_AEC_FILTER_LENGTH": (33, 93, 1, "ro", "int32"), + "AEC_FILTER_CMD_ABORT": (33, 94, 1, "wo", "int32"), + "AEC_AECPATHCHANGE": (33, 0, 1, "ro", "int32"), + "AEC_HPFONOFF": (33, 1, 1, "rw", "int32"), + "AEC_AECSILENCELEVEL": (33, 2, 2, "rw", "float"), + "AEC_AECCONVERGED": (33, 3, 1, "ro", "int32"), + "AEC_AECEMPHASISONOFF": (33, 4, 1, "rw", "int32"), + "AEC_FAR_EXTGAIN": (33, 5, 1, "rw", "float"), + "AEC_PCD_COUPLINGI": (33, 6, 1, "rw", "float"), + "AEC_PCD_MINTHR": (33, 7, 1, "rw", "float"), + "AEC_PCD_MAXTHR": (33, 8, 1, "rw", "float"), + "AEC_RT60": (33, 9, 1, "ro", "float"), + "AEC_ASROUTONOFF": (33, 35, 1, "rw", "int32"), + "AEC_ASROUTGAIN": (33, 36, 1, "rw", "float"), + "AEC_FIXEDBEAMSONOFF": (33, 37, 1, "rw", "int32"), + "AEC_FIXEDBEAMNOISETHR": (33, 38, 2, "rw", "float"), + # AUDIO_MGR_RESID commands + "AUDIO_MGR_MIC_GAIN": (35, 0, 1, "rw", "float"), + "AUDIO_MGR_REF_GAIN": (35, 1, 1, "rw", "float"), + "AUDIO_MGR_CURRENT_IDLE_TIME": (35, 2, 1, "ro", "int32"), + "AUDIO_MGR_MIN_IDLE_TIME": (35, 3, 1, "ro", "int32"), + "AUDIO_MGR_RESET_MIN_IDLE_TIME": (35, 4, 1, "wo", "int32"), + "MAX_CONTROL_TIME": (35, 5, 1, "ro", "int32"), + "RESET_MAX_CONTROL_TIME": (35, 6, 1, "wo", "int32"), + "I2S_CURRENT_IDLE_TIME": (35, 7, 1, "ro", "int32"), + "I2S_MIN_IDLE_TIME": (35, 8, 1, "ro", "int32"), + "I2S_RESET_MIN_IDLE_TIME": (35, 9, 1, "wo", "int32"), + "I2S_INPUT_PACKED": (35, 10, 1, "rw", "uint8"), + "AUDIO_MGR_SELECTED_AZIMUTHS": (35, 11, 2, "ro", "radians"), + "AUDIO_MGR_SELECTED_CHANNELS": (35, 12, 2, "rw", "uint8"), + "AUDIO_MGR_OP_PACKED": (35, 13, 2, "rw", "uint8"), + "AUDIO_MGR_OP_UPSAMPLE": (35, 14, 2, "rw", "uint8"), + "AUDIO_MGR_OP_L": (35, 15, 2, "rw", "uint8"), + "AUDIO_MGR_OP_L_PK0": (35, 16, 2, "rw", "uint8"), + "AUDIO_MGR_OP_L_PK1": (35, 17, 2, "rw", "uint8"), + "AUDIO_MGR_OP_L_PK2": (35, 18, 2, "rw", "uint8"), + "AUDIO_MGR_OP_R": (35, 19, 2, "rw", "uint8"), + "AUDIO_MGR_OP_R_PK0": (35, 20, 2, "rw", "uint8"), + "AUDIO_MGR_OP_R_PK1": (35, 21, 2, "rw", "uint8"), + "AUDIO_MGR_OP_R_PK2": (35, 22, 2, "rw", "uint8"), + "AUDIO_MGR_OP_ALL": (35, 23, 12, "rw", "uint8"), + "I2S_INACTIVE": (35, 24, 1, "ro", "uint8"), + "AUDIO_MGR_FAR_END_DSP_ENABLE": (35, 25, 1, "rw", "uint8"), + "AUDIO_MGR_SYS_DELAY": (35, 26, 1, "rw", "int32"), + "I2S_DAC_DSP_ENABLE": (35, 27, 1, "rw", "uint8"), + # GPO_SERVICER_RESID commands + "GPO_READ_VALUES": (20, 0, 5, "ro", "uint8"), + "GPO_WRITE_VALUE": (20, 1, 2, "wo", "uint8"), + "GPO_PORT_PIN_INDEX": (20, 2, 2, "rw", "uint32"), + "GPO_PIN_VAL": (20, 3, 3, "wo", "uint8"), + "GPO_PIN_ACTIVE_LEVEL": (20, 4, 1, "rw", "uint32"), + "GPO_PIN_PWM_DUTY": (20, 5, 1, "rw", "uint8"), + "GPO_PIN_FLASH_MASK": (20, 6, 1, "rw", "uint32"), + "LED_EFFECT": (20, 12, 1, "rw", "uint8"), + "LED_BRIGHTNESS": (20, 13, 1, "rw", "uint8"), + "LED_GAMMIFY": (20, 14, 1, "rw", "uint8"), + "LED_SPEED": (20, 15, 1, "rw", "uint8"), + "LED_COLOR": (20, 16, 1, "rw", "uint32"), + "LED_DOA_COLOR": (20, 17, 2, "rw", "uint32"), + "DOA_VALUE": (20, 18, 2, "ro", "uint32"), + "DOA_VALUE_RADIANS": (20, 19, 2, "ro", "radians"), + # PP_RESID commands + "PP_CURRENT_IDLE_TIME": (17, 70, 1, "ro", "uint32"), + "PP_MIN_IDLE_TIME": (17, 71, 1, "ro", "uint32"), + "PP_RESET_MIN_IDLE_TIME": (17, 72, 1, "wo", "uint32"), + "SPECIAL_CMD_PP_NLMODEL_NROW_NCOL": (17, 90, 2, "ro", "int32"), + "SPECIAL_CMD_NLMODEL_START": (17, 91, 1, "wo", "int32"), + "SPECIAL_CMD_NLMODEL_COEFF_START_OFFSET": (17, 92, 1, "rw", "int32"), + "SPECIAL_CMD_PP_NLMODEL": (17, 93, 15, "rw", "float"), + "PP_NL_MODEL_CMD_ABORT": (17, 94, 1, "wo", "int32"), + "SPECIAL_CMD_PP_NLMODEL_BAND": (17, 95, 1, "rw", "uint8"), + "SPECIAL_CMD_PP_EQUALIZATION_NUM_BANDS": (17, 96, 1, "ro", "int32"), + "SPECIAL_CMD_EQUALIZATION_START": (17, 97, 1, "wo", "int32"), + "SPECIAL_CMD_EQUALIZATION_COEFF_START_OFFSET": (17, 98, 1, "rw", "int32"), + "SPECIAL_CMD_PP_EQUALIZATION": (17, 99, 15, "rw", "float"), + "PP_EQUALIZATION_CMD_ABORT": (17, 100, 1, "wo", "int32"), + "PP_AGCONOFF": (17, 10, 1, "rw", "int32"), + "PP_AGCMAXGAIN": (17, 11, 1, "rw", "float"), + "PP_AGCDESIREDLEVEL": (17, 12, 1, "rw", "float"), + "PP_AGCGAIN": (17, 13, 1, "rw", "float"), + "PP_AGCTIME": (17, 14, 1, "rw", "float"), + "PP_AGCFASTTIME": (17, 15, 1, "rw", "float"), + "PP_AGCALPHAFASTGAIN": (17, 16, 1, "rw", "float"), + "PP_AGCALPHASLOW": (17, 17, 1, "rw", "float"), + "PP_AGCALPHAFAST": (17, 18, 1, "rw", "float"), + "PP_LIMITONOFF": (17, 19, 1, "rw", "int32"), + "PP_LIMITPLIMIT": (17, 20, 1, "rw", "float"), + "PP_MIN_NS": (17, 21, 1, "rw", "float"), + "PP_MIN_NN": (17, 22, 1, "rw", "float"), + "PP_ECHOONOFF": (17, 23, 1, "rw", "int32"), + "PP_GAMMA_E": (17, 24, 1, "rw", "float"), + "PP_GAMMA_ETAIL": (17, 25, 1, "rw", "float"), + "PP_GAMMA_ENL": (17, 26, 1, "rw", "float"), + "PP_NLATTENONOFF": (17, 27, 1, "rw", "int32"), + "PP_NLAEC_MODE": (17, 28, 1, "rw", "int32"), + "PP_MGSCALE": (17, 29, 3, "rw", "float"), + "PP_FMIN_SPEINDEX": (17, 30, 1, "rw", "float"), + "PP_DTSENSITIVE": (17, 31, 1, "rw", "int32"), + "PP_ATTNS_MODE": (17, 32, 1, "rw", "int32"), + "PP_ATTNS_NOMINAL": (17, 33, 1, "rw", "float"), + "PP_ATTNS_SLOPE": (17, 34, 1, "rw", "float"), +} + + +class ReSpeaker: + """Class to interface with the ReSpeaker XVF3800 USB device.""" + + TIMEOUT = 100000 + + def __init__(self, dev: usb.core.Device) -> None: + """Initialize the ReSpeaker interface with the given USB device.""" + self.dev = dev + + def write(self, name: str, data_list: Any) -> None: + """Write data to a specified parameter on the ReSpeaker device.""" + try: + data = PARAMETERS[name] + except KeyError: + return + + if data[3] == "ro": + raise ValueError("{} is read-only".format(name)) + + if len(data_list) != data[2]: + raise ValueError("{} value count is not {}".format(name, data[2])) + + windex = data[0] # resid + wvalue = data[1] # cmdid + data_cnt = data[2] # cnt + data_type = data[4] # data type + payload = [] # type: ignore[var-annotated] + + if data_type == "float" or data_type == "radians": + for i in range(data_cnt): + payload += struct.pack(b"f", float(data_list[i])) + elif data_type == "char": + # For char arrays, convert string to bytes + payload = ( + bytearray(data_list, "utf-8") # type: ignore[assignment] + if isinstance(data_list, str) + else bytearray(data_list) + ) + elif data_type == "uint8": + for i in range(data_cnt): + payload += data_list[i].to_bytes(1, byteorder="little") + elif data_type == "uint32" or data_type == "int32": + for i in range(data_cnt): + payload += struct.pack( + b"I" if data_type == "uint32" else b"i", data_list[i] + ) + else: + # Default to int32 for other types + for i in range(data_cnt): + payload += struct.pack(b"i", data_list[i]) + + logging.debug( + "WriteCMD: cmdid: {}, resid: {}, payload: {}".format( + wvalue, windex, payload + ) + ) + + self.dev.ctrl_transfer( + usb.util.CTRL_OUT + | usb.util.CTRL_TYPE_VENDOR + | usb.util.CTRL_RECIPIENT_DEVICE, + 0, + wvalue, + windex, + payload, + self.TIMEOUT, + ) + + def read(self, name: str) -> Any: + """Read data from a specified parameter on the ReSpeaker device.""" + try: + data = PARAMETERS[name] + except KeyError: + return + + read_attempts = 1 + windex = data[0] # resid + wvalue = 0x80 | data[1] # cmdid + data_cnt = data[2] # cnt + data_type = data[4] # data type + if data_type == "uint8" or data_type == "char": + length = data_cnt + 1 # 1 byte for status + elif ( + data_type == "float" + or data_type == "radians" + or data_type == "uint32" + or data_type == "int32" + ): + length = data_cnt * 4 + 1 # 1 byte for status + + response = self.dev.ctrl_transfer( + usb.util.CTRL_IN + | usb.util.CTRL_TYPE_VENDOR + | usb.util.CTRL_RECIPIENT_DEVICE, + 0, + wvalue, + windex, + length, + self.TIMEOUT, + ) + while True: + if read_attempts > 100: + raise ValueError("Read attempt exceeds 100 times") + if response[0] == CONTROL_SUCCESS: + break + elif response[0] == SERVICER_COMMAND_RETRY: + read_attempts += 1 + response = self.dev.ctrl_transfer( + usb.util.CTRL_IN + | usb.util.CTRL_TYPE_VENDOR + | usb.util.CTRL_RECIPIENT_DEVICE, + 0, + wvalue, + windex, + length, + self.TIMEOUT, + ) + else: + raise ValueError("Unknown status code: {}".format(response[0])) + time.sleep(0.01) + + logging.info( + "ReadCMD: cmdid: {}, resid: {}, response: {}".format( + wvalue, windex, response + ) + ) + + if data_type == "uint8": + result = response.tolist() + elif data_type == "char": + # For char arrays, convert bytes to string + byte_data = response.tobytes() + # Remove status byte and null terminators + result = byte_data[1:].rstrip(b"\x00").decode("utf-8", errors="ignore") + elif data_type == "radians" or data_type == "float": + byte_data = response.tobytes() + match_str = "<" + for i in range(data_cnt): + match_str += "f" + result = struct.unpack(match_str, byte_data[1:]) + elif data_type == "uint32" or data_type == "int32": + result = response.tolist() + + return result + + def close(self) -> None: + """Close the interface.""" + usb.util.dispose_resources(self.dev) + + +def find(vid: int = 0x2886, pid: int = 0x001A) -> ReSpeaker | None: + """Find and return the ReSpeaker USB device with the given Vendor ID and Product ID.""" + dev = usb.core.find(idVendor=vid, idProduct=pid) + if not dev: + return None + + return ReSpeaker(dev) + + +def init_respeaker_usb() -> Optional[ReSpeaker]: + """Initialize the ReSpeaker USB device. Looks for both new and beta device IDs.""" + try: + dev = usb.core.find( + idVendor=0x38FB, idProduct=0x1001, backend=get_libusb1_backend() + ) + if dev is None: + dev = usb.core.find( + idVendor=0x2886, idProduct=0x001A, backend=get_libusb1_backend() + ) + logging.warning("Old firmware detected. Please update the firmware!") + return ReSpeaker(dev) + except usb.core.NoBackendError: + logging.error( + "No USB backend was found ! Make sure libusb_package is correctly installed with `pip install libusb_package`." + ) + return None + + +def main() -> None: + """Parse arguments and execute read/write commands.""" + parser = argparse.ArgumentParser( + description="Reachy Mini Audio Host Control Script" + ) + parser.add_argument( + "command", + choices=PARAMETERS.keys(), + help="Command to execute (e.g., VERSION, DOA_VALUE, etc.)", + ) + parser.add_argument( + "--vid", + type=lambda x: int(x, 0), + default=0x38FB, + help="Vendor ID (default: 0x38FB)", + ) + parser.add_argument( + "--pid", + type=lambda x: int(x, 0), + default=0x1001, + help="Product ID (default: 0x1001)", + ) + parser.add_argument( + "--values", + nargs="+", + type=float, + help="Values for write commands (only for write operations)", + ) + + args = parser.parse_args() + + dev = find(vid=args.vid, pid=args.pid) + if not dev: + print("No device found") + sys.exit(1) + + try: + if args.values: + if PARAMETERS[args.command][3] == "ro": + print(f"Error: {args.command} is read-only and cannot be written to") + sys.exit(1) + + if ( + PARAMETERS[args.command][4] != "float" + and PARAMETERS[args.command][4] != "radians" + ): + args.values = [int(v) for v in args.values] + + if PARAMETERS[args.command][2] != len(args.values): + print( + f"Error: {args.command} value count is {PARAMETERS[args.command][2]}, but {len(args.values)} values provided" + ) + sys.exit(1) + + print(f"Writing to {args.command} with values: {args.values}") + dev.write(args.command, args.values) + time.sleep(0.1) + print("Write operation completed successfully") + else: + if PARAMETERS[args.command][3] == "wo": + print(f"Error: {args.command} is write-only and cannot be read") + sys.exit(1) + + result = dev.read(args.command) + print(f"{args.command}: {result}") + + except Exception as e: + print(f"Error executing command {args.command}: {e}") + sys.exit(1) + finally: + dev.close() + + +if __name__ == "__main__": + main() From 6ce40d34daf096258cc1602aac491b0889aa12a9 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Thu, 13 Nov 2025 17:50:53 +0100 Subject: [PATCH 06/10] bug #397: fix typo in variable name --- src/reachy_mini/media/audio_sounddevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reachy_mini/media/audio_sounddevice.py b/src/reachy_mini/media/audio_sounddevice.py index 386198ec..c8d63ed6 100644 --- a/src/reachy_mini/media/audio_sounddevice.py +++ b/src/reachy_mini/media/audio_sounddevice.py @@ -208,7 +208,7 @@ def _get_device_id( return idx # Return default output device if not found self.logger.warning( - f"No {device_io_type} device found containing '{name_contains}', using default." + f"No {device_io_type} device found containing '{names_contains}', using default." ) return self._safe_query_device(device_io_type) From 63fe94a2506b5e22ada7f5b6c2cc400c6e9adcd4 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Fri, 14 Nov 2025 09:32:48 +0100 Subject: [PATCH 07/10] bug #397: add sleep to give system time to reload the audio card --- src/reachy_mini/daemon/app/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/reachy_mini/daemon/app/main.py b/src/reachy_mini/daemon/app/main.py index 3ec74e56..bc70dc0e 100644 --- a/src/reachy_mini/daemon/app/main.py +++ b/src/reachy_mini/daemon/app/main.py @@ -158,6 +158,8 @@ def run_app(args: Args) -> None: """Temporary fix for audio issues with beta version.""" logging.info("Applying audio fix for beta version.") + import time + from reachy_mini.media.audio_control_utils import init_respeaker_usb respeaker = init_respeaker_usb() @@ -166,6 +168,7 @@ def run_app(args: Args) -> None: else: respeaker.write("REBOOT", [1]) respeaker.close() + time.sleep(1) logging.debug("Respeaker rebooted.") health_check_event = asyncio.Event() From 6c158f4cd7fd1ea1ead478a4aeeade8b58d4c4b2 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Mon, 17 Nov 2025 09:15:09 +0100 Subject: [PATCH 08/10] bug #397: remove --fix-audio. using fixed firmware --- docs/troubleshooting.md | 8 +----- .../firmware/99-reachy-mini-audio.rules | 5 ---- .../assets/firmware/install_udev_rules.sh | 20 --------------- .../reachymini_ua_io16_lin_v2.1.0.bin | 3 --- .../reachymini_ua_io16_lin_v2.1.2.bin | 3 --- src/reachy_mini/daemon/app/main.py | 25 ------------------- 6 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules delete mode 100755 src/reachy_mini/assets/firmware/install_udev_rules.sh delete mode 100644 src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.0.bin delete mode 100644 src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 349174cd..f0162e73 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -4,13 +4,7 @@ ## No Microphone Input *For beta only* -There is a known issue where the microphone may not initialize correctly. You can use the `--fix-audio` argument to force the audio card to reset before starting the daemon. - -Make sure your firmware is updated to version 2.1.2. You may need to run the [update script](../src/reachy_mini/assets/firmware/update.sh). Linux users should also install the proper udev rules using [this script](../src/reachy_mini/assets/firmware/install_udev_rules.sh). Then run: - -```bash -reachy-mini-daemon --fix-audio -``` +There is a known issue where the microphone may not initialize correctly. Please update to [firmware 2.1.3](../src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.3.bin). You may need to run the [update script](../src/reachy_mini/assets/firmware/update.sh). Afterwards, run [examples/debug/sound_record.py](../examples/debug/sound_record.py) to check that everything is working properly. diff --git a/src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules b/src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules deleted file mode 100644 index 73e2002b..00000000 --- a/src/reachy_mini/assets/firmware/99-reachy-mini-audio.rules +++ /dev/null @@ -1,5 +0,0 @@ -# usb 4mic array -SUBSYSTEM=="usb", ATTR{idVendor}=="2886", ATTR{idProduct}=="001a", MODE="0666" - -# reachy mini audio -SUBSYSTEM=="usb", ATTR{idVendor}=="38fb", ATTR{idProduct}=="1001", MODE="0666" diff --git a/src/reachy_mini/assets/firmware/install_udev_rules.sh b/src/reachy_mini/assets/firmware/install_udev_rules.sh deleted file mode 100755 index ed351ef2..00000000 --- a/src/reachy_mini/assets/firmware/install_udev_rules.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -e - -UDEV_RULE="99-reachy-mini-audio.rules" -SRC_DIR="$(dirname "$0")" -RULE_PATH="$SRC_DIR/$UDEV_RULE" -DEST_PATH="/etc/udev/rules.d/$UDEV_RULE" - -if [[ ! -f "$RULE_PATH" ]]; then - echo "Error: $RULE_PATH not found." - exit 1 -fi - -echo "Installing $UDEV_RULE to /etc/udev/rules.d/..." -sudo cp "$RULE_PATH" "$DEST_PATH" -sudo udevadm control --reload-rules -sudo udevadm trigger - -echo "udev rule installed successfully." \ No newline at end of file diff --git a/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.0.bin b/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.0.bin deleted file mode 100644 index eaea5ff2..00000000 --- a/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.0.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71b6b59a2a7382fc11d5f8953b9614348b7cf8f276c96cc212b393237d32c677 -size 933888 diff --git a/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin b/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin deleted file mode 100644 index 6314af04..00000000 --- a/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.2.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:661429a93b155cdbfed1e6c214820fd42e3372a08c3930f14fcd4e6db5be6796 -size 933888 diff --git a/src/reachy_mini/daemon/app/main.py b/src/reachy_mini/daemon/app/main.py index bc70dc0e..e12feb98 100644 --- a/src/reachy_mini/daemon/app/main.py +++ b/src/reachy_mini/daemon/app/main.py @@ -55,8 +55,6 @@ class Args: localhost_only: bool | None = None - fix_audio: bool = False - def create_app(args: Args, health_check_event: asyncio.Event | None = None) -> FastAPI: """Create and configure the FastAPI application.""" @@ -154,23 +152,6 @@ def run_app(args: Args) -> None: """Run the FastAPI app with Uvicorn.""" logging.basicConfig(level=logging.INFO) - if args.fix_audio: - """Temporary fix for audio issues with beta version.""" - logging.info("Applying audio fix for beta version.") - - import time - - from reachy_mini.media.audio_control_utils import init_respeaker_usb - - respeaker = init_respeaker_usb() - if respeaker is None: - logging.error("Respeaker device not found. Cannot apply audio fix.") - else: - respeaker.write("REBOOT", [1]) - respeaker.close() - time.sleep(1) - logging.debug("Respeaker rebooted.") - health_check_event = asyncio.Event() app = create_app(args, health_check_event) @@ -333,12 +314,6 @@ def main() -> None: help="Set the logging level (default: INFO).", ) - parser.add_argument( - "--fix-audio", - action="store_true", - help="Fix audio issues with beta version (default: False).", - ) - args = parser.parse_args() run_app(Args(**vars(args))) From 01daefd30dd322ae37522febed086bb94d999bf8 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Mon, 17 Nov 2025 09:31:50 +0100 Subject: [PATCH 09/10] bug #397: fix typo --- examples/debug/sound_doa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/debug/sound_doa.py b/examples/debug/sound_doa.py index c7e482ae..eed50ef4 100644 --- a/examples/debug/sound_doa.py +++ b/examples/debug/sound_doa.py @@ -26,7 +26,7 @@ def main() -> None: doa = mini.media.audio.get_DoA() print(f"DOA: {doa}") if doa[1] and np.abs(doa[0] - last_doa) > THRESHOLD: - print(f" Speech detected at {doa[0]:.1f}°") + print(f" Speech detected at {doa[0]:.1f} radians") p_head = [np.sin(doa[0]), np.cos(doa[0]), 0.0] print( f" Pointing to x={p_head[0]:.2f}, y={p_head[1]:.2f}, z={p_head[2]:.2f}" From 1e9a5c494439bbc80031577d48b35412e46d4577 Mon Sep 17 00:00:00 2001 From: Fabien Danieau Date: Mon, 17 Nov 2025 09:40:15 +0100 Subject: [PATCH 10/10] bug #397: pushing 2.13 firmware --- .../assets/firmware/reachymini_ua_io16_lin_v2.1.3.bin | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.3.bin diff --git a/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.3.bin b/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.3.bin new file mode 100644 index 00000000..96dcb9e0 --- /dev/null +++ b/src/reachy_mini/assets/firmware/reachymini_ua_io16_lin_v2.1.3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:134d4f119a5f3e9eda7fedd60b1eb0274ece9b8fafcfbe5b35117c40b31beb9a +size 933888