Acoupi batdetect2 with a Raspberry pi 4 and Audiomoth mic
As part of #mayke I made up a portable bat classifier using a Pi4, Acoupi and an audiomoth mic.
The idea eventually is to hook it up to LoRaWAN so we can see detections remotely.

I got a load of useful hints from Sarah on Mastodon (thank you!)
Acoupi is a framework for classifiers running on-device - it can use birdnet or batnet machine learning models, maybe others? The docs are pretty good but I want to preserve a few more details for my setup.
I started with bookworm 64bit lite installed using the Raspberry Pi imager. I set up my audiomoth using these instructions and attached it to the Pi via USB.
Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
Then, install acoupi -
curl -sSL https://github.com/acoupi/acoupi/raw/main/scripts/setup.sh | bash
Install pip3
sudo apt install python3-pip
Install acoupi-batdetect
pip3 install acoupi_batdetect2
--break-system-packages
Acoupi can send detections to an MQTT server. I want to run that server on the device itself because I want it to be self-contained and it's an easy way to get at the data.
So, install mosquitto
sudo apt install mosquitto mosquitto-clients
check it's running:
ps ax | grep mos
4032 Ss 0:00 /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf
A handy little pair of commands to check mqtt is working are -
mosquitto_pub -h localhost -p 1883 -m 'hello' -t 'acoupi'
mosquitto_sub -v -h localhost -p 1883 -t '#'
Set up acoupi-batdetect2 -
acoupi setup --program acoupi_batdetect2.program
Here are my answers. Note that it can take 15-30 secs to process a 3 second file sometimes, so if you record every 10 seconds they will build up. 20 seconds seems ok.
Collecting program files. It will take a minute or so, be patient...
Would you like to set timezone='Europe/London'? [Y/n]:
[at this point the pi farts out a load of stuff about missing audio things]
Available audio devices:
Index Name Channels Sample Rate
[ 1] 384kHz AudioMoth USB Microphone 1 384000.0
Select an audio device index (1): 1
Info of selected audio device:
index = 1
name = 384kHz AudioMoth USB Microphone
max channels = 1
default sample rate = 384000.0
Select the number of audio channels (1) [1]: 1
Select the samplerate. The default samplerate is recommended but your device might support other sampling rates. [384000.0]:
Would you like to set recording? [y/N]: y
Would you like to set recording.duration=3? [Y/n]:
Would you like to set recording.interval=20? [Y/n]:
Would you like to set recording.schedule_start=datetime.time(19, 0)? [Y/n]:
Would you like to set recording.schedule_end=datetime.time(7, 0)? [Y/n]:
Would you like to set paths? [y/N]: y
Would you like to set paths.tmp_audio=PosixPath('/run/shm')? [Y/n]:
Would you like to set paths.recordings=PosixPath('/home/pi/storages/recordings')? [Y/n]:
Would you like to set paths.db_metadata=PosixPath('/home/pi/storages/metadata.db')? [Y/n]:
Would you like to set messaging.messages_db=PosixPath('/home/pi/storages/messages.db')? [Y/n]:
Would you like to set messaging.message_send_interval=120? [Y/n]:
Would you like to set messaging.heartbeat_interval=3600? [Y/n]:
Would you like to set http? [y/N]:
Would you like to set mqtt? [y/N]: y
Please provide a value for messaging.mqtt.host.: localhost
Please provide a value for messaging.mqtt.username.: none
Please provide a value for messaging.mqtt.password.: none
Would you like to set messaging.mqtt.topic='acoupi'? [Y/n]:
Would you like to set messaging.mqtt.port=1884? [Y/n]: n
Please provide a value for messaging.mqtt.port. [1884]: 1883
Would you like to set messaging.mqtt.timeout=5? [Y/n]:
Would you like to set detections? [y/N]: y
Would you like to set detections.threshold=0.2? [Y/n]:
Would you like to set model? [y/N]: Y
Would you like to set model.detection_threshold=0.4? [Y/n]:
Would you like to set saving_filters? [y/N]:
Would you like to set saving_managers? [y/N]:
Would you like to set summariser_config? [y/N]:
(Note that it won't accept blank/no username / password for the mosquitto server. I edited the file after as I'm being lazy and not configuring mosquitto except for defaults - that explains 1883 vs 1884 as the port too.)
So after the 'none' mqtt u/p edit my config is now
cat ~/.acoupi/config/program.json
{
"timezone": "Europe/London",
"microphone": {
"device_name": "384kHz AudioMoth USB Microphone",
"samplerate": 384000,
"audio_channels": 1
},
"recording": {
"duration": 3,
"interval": 20,
"chunksize": 8192,
"schedule_start": "19:00:00",
"schedule_end": "07:00:00"
},
"paths": {
"tmp_audio": "/run/shm",
"recordings": "/home/pi/storages/recordings",
"db_metadata": "/home/pi/storages/metadata.db"
},
"messaging": {
"messages_db": "/home/pi/storages/messages.db",
"message_send_interval": 120,
"heartbeat_interval": 3600,
"http": null,
"mqtt": {
"host": "localhost",
"username": "",
"password": "",
"topic": "acoupi",
"port": 1883,
"timeout": 5
}
},
"detections": {
"threshold": 0.4
},
"model": {
"detection_threshold": 0.6
},
"saving_filters": {
"starttime": "19:00:00",
"endtime": "07:00:00",
"before_dawndusk_duration": 0,
"after_dawndusk_duration": 0,
"frequency_duration": 0,
"frequency_interval": 0,
"saving_threshold": 0.3
},
"saving_managers": {
"true_dir": "bats",
"false_dir": "no_bats",
"timeformat": "%Y%m%d_%H%M%S",
"bat_threshold": 0.5
},
"summariser_config": {
"interval": 3600,
"low_band_threshold": 0,
"mid_band_threshold": 0,
"high_band_threshold": 0
}
}
At that point you could do a test by setting it up like this (with your gps coordinates):
acoupi deployment start
Enter the name of the deployment: bats
Enter the latitude of the deployment: 51.45
Enter the longitude of the deployment: -2.59
and then doing this:
mosquitto_sub -v -h localhost -p 1883 -t '#'
to see the bat data come in, maybe also with
tail -f ~/.acoupi/log/*
The deployment will restart when you restart the device.
Of course proving that any of this is detecting real things is a bit tricky, particularly for bats. I tried installing acoupi-birdnet as at least I could hear what it was recording. It was all pretty similar to install and seemed to work well (although interestingly it includes gunshots, human speech, wolves, power tools and dogs as well as birds, and it seems very certain about some of those, and there are no wolves or gunshots round here).
With batnet I also managed to see (with my eyes) and detect (with the classifier) a Pipistrelle, and had a play with changing the audio recording so I could hear it, like this:
sox 20250528_214125.wav 20250528_214125_warp2.wav pitch -6000
(from here), and making a spectrogram
sox 20250528_214125.wav -n rate 384.0k spectrogram -l -m -X 160 -z 95 -Z 0 -r -Y 257 -o spectro.png

A participant in a planned hack in this area suggested running the audio though this bioacoustic pipeline to test it. Comments in this thread were really helpful on the mysterious Nyctalus I kept finding (preprint article).
As well as getting the data out I want to additionally display it on a little screen I have, displayHatMini. So -
pip3 install displayhatmini --break-system-packages
enable spi:sudo raspi-config nonint do_spi 0
cat mqtt_display.py
#!/usr/bin/env python3
import time
import paho.mqtt.client as mqtt
from paho.mqtt.client import CallbackAPIVersion
import random
from displayhatmini import DisplayHATMini
import json
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("""This example requires PIL/Pillow, try:
sudo apt install python3-pil
""")
width = DisplayHATMini.WIDTH
height = DisplayHATMini.HEIGHT
buffer = Image.new("RGB", (width, height))
draw = ImageDraw.Draw(buffer)
displayhatmini = DisplayHATMini(buffer)
draw.rectangle((0, 0, width, height), (0, 0, 0))
displayhatmini.display()
def text(draw, text, position, size, color):
#fnt = ImageFont.load_default()
fnt = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 28, encoding="unic")
draw.text(position, text, font=fnt, fill=color)
client = mqtt.Client(CallbackAPIVersion.VERSION2,
client_id="test-client")
def on_connect(client, userdata, flags, reason_code, properties):
print(f"Connected: reason_code={reason_code}, properties={properties}")
client.subscribe("acoupi", qos=1)
"""
data is like this {'tag': {'key': 'species', 'value': 'Pipistrellus pipistrellus'}, 'confidence_score': 0.526}
"""
def on_message(client, userdata, msg):
print(f"{msg.topic}: {msg.payload.decode()}")
draw.rectangle((0, 0, width, height), (0, 0, 0))
displayhatmini.display()
x = msg.payload.decode()
y = json.loads(x)
if(y["detections"]):
species = y["detections"][0]["tags"][0]["tag"]["value"]
score = y["detections"][0]["tags"][0]["confidence_score"]
dt = y["recording"]["created_on"]
print("species",species)
text(draw, species, (25, 25), 15, (255, 255, 255))
text(draw, str(score), (25, 100), 15, (255, 255, 255))
text(draw, str(dt), (25, 175), 15, (255, 255, 255))
displayhatmini.display()
client.on_connect = on_connect
client.on_message = on_message
client.connect("localhost",
port=1883,
keepalive=60)
client.loop_forever()
For completeness
cat display.service
[Unit]
Description=display
[Service]
Type=simple
WorkingDirectory=/home/pi/
ExecStart=/usr/bin/python3 mqtt_display.py
Restart=on-failure
StandardOutput=syslog
SyslogIdentifier=display
Type=idle
User=pi
[Install]
WantedBy=multi-user.target
sudo cp display.service /etc/systemd/system/
sudo systemctl enable display.service
sudo systemctl start display.service
sudo systemctl status display.service
Even more for completeness - I find that sometimes if I get a power failure or for other undetermined reasons the message.db and metadata.db seems to lose data or maybe get corrupted. So here's a tiny python script to log it all as well.
import paho.mqtt.client as mqtt #import the client1
import time
import json
import traceback
def on_message(client, userdata, message):
decoded_message=str(message.payload.decode("utf-8"))
msg=json.loads(decoded_message)
print("message received " ,decoded_message)
print("message topic=",message.topic)
try:
with open("message_log.txt", "a") as myfile:
myfile.write(decoded_message)
myfile.write('\n')
except Exception as e:
print(e)
print("Exception")
traceback.print_exc()
broker_address="127.0.0.1"
print("creating new instance")
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "P1")
client.on_message=on_message #attach function to callback
print("connecting to broker")
client.connect(broker_address) #connect to broker
print("Subscribing to topic","acoupi")
client.subscribe("acoupi")
client.loop_forever()