• WANTED: Happy members who like to discuss audio and other topics related to our interest. Desire to learn and share knowledge of science required. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

Internet Radio Project (radiokj)

kingj

New Member
Joined
Oct 2, 2024
Messages
1
Likes
0
Hi everyone,

I'm new here, and pretty new to programming too.

I'm trying to build a "simple" internet radio, with minimal controls and options.

Here is a list of parts I'm using :

Raspberry Pi 5
AudioBerry Amp2 (DAC + 20W Amp)
Dayton Audio DMA 70-4 Speakers (20W @ 4 ohms)
AZDelivery OLED Screen 1.3 inches, 128x64 px, SSH1106 Driver
2x KY-040 Rotary Encoders with push button and resistor
1x KY-004 Push Button Module with resistor (J2 Power extension button)
19V 3.4 Amps Power Adapter

Here are the OS and libraries I'm using :

Raspberry OS Bookworm
lgpio (GPIO)
mpd (music player daemon)
luma.oled sh1106 (display driver)

Software goal :

Power ON Raspberry
Autostart of the "radio" program (Extension power button)
1x Encoder to scroll throught a small preset of internet radio stations (Push for future function)
1x Encoder to set Volume (Push for Mute)
Power OFF Raspberry Safely (Extension power button)


I'm really new to programming.
I watched all the videos that I could and read a ton of things online, mostly from https://bobrathbone.com/.
I tried to implement his software in my project, which is very similar.
Unfortunately, I haven't been able to make it work.
I suspect some compatibility issues to use his program straightforward with Raspberry PI 5.

So I tried to write it from scratch with the help of AI (please don't judge me, I'm trying my best to understand the answers...).
After a very long process of trial and errors, I modified a lot of CONFIG files and tried a lot of differents scripts and drivers, I made a nearly functionnal version of the software.
I could start the RPI, and everything was more or less working as expected when started from Thonny's GUI on the PI.
Then, I tried to modify the script to make it boot with the PI, to not have to use the GUI and run it "headless" (I mean, only on the little OLED Screen).

I don't really know how I managed to do it, but I nearly "broke" all the program.
Now, when I launch it from Thonny, the Radio starts OK and the text displays OK on the OLED.
I can turn Station or Volume encoder for one or two clicks and then my program stops.
The radio is still running in the background because MPD doesn't stop, but as the software crashes, I can't control anything and I have to reboot.
It has been a nightmare figuring out what's going wrong with my script (mostly because of my lack of experience) but I can also feel that I'm pretty close to make it work.
I mean all in all, it should be quite a basic program.

What I would like to do now is :
- Start the software at bootup automatically.
- Stop MPD when I stop the program from GUI, or when it crashes.
- Stop the OLED screen when I stop the program from GUI, or when it crashes.
- Have a Safe Shutdown when I press the extension power button (maybe this one is ok).
- Have a stable software that keeps running and where I can easily change stations and volume.

Please, could someone help me understand what's wrong with my script ?
Any help would be very much appreciated.
I will try to understand anything to make it work.
If it's not the perfect place for this topic, you can move it, or point me to a better direction.

Thanks !



import time
import lgpio
import subprocess # Import subprocess to use system commands
import signal
from mpd import MPDClient
from luma.core.interface.serial import i2c
from luma.oled.device import sh1106
from luma.core.render import canvas
from PIL import ImageFont

# GPIO pin configuration for the two rotary encoders
clk_station = 5 # Clock pin for changing station
dt_station = 6 # Data pin for changing station
btn_station = 12 # GPIO for station button
clk_volume = 13 # Clock pin for changing volume
dt_volume = 22 # Data pin for changing volume
btn_volume = 26 # GPIO for volume button

# Initial states for encoders
station_counter = 0
volume_counter = 20 # Initial volume set to 20%
last_state_clk_station = None
last_state_clk_volume = None
debounce_time = 0.002 # 2ms debounce time

# Open GPIO chip
h = lgpio.gpiochip_open(0)

# Set the pins as input
lgpio.gpio_claim_input(h, clk_station)
lgpio.gpio_claim_input(h, dt_station)
lgpio.gpio_claim_input(h, clk_volume)
lgpio.gpio_claim_input(h, dt_volume)
lgpio.gpio_claim_input(h, btn_volume) # Claim volume button as input
lgpio.gpio_claim_input(h, btn_station) # Claim station button as input

# Setup OLED display (I2C)
serial = i2c(port=1, address=0x3C) # Default I2C address for SH1106
device = sh1106(serial)
font = ImageFont.load_default()

# Connect to MPD
client = MPDClient()
client.connect("localhost", 6600)

# List of preset radio stations with custom names
stations = [
("[URL]http://icecast.radiofrance.fr/franceinter-midfi.mp3[/URL]", "France Inter"),
("[URL]http://icecast.radiofrance.fr/fip-midfi.mp3[/URL]", "FIP"),
("[URL]http://icecast.radiofrance.fr/franceculture-midfi.mp3[/URL]", "France Culture"),
("[URL]http://icecast.radiofrance.fr/franceinfo-midfi.mp3[/URL]", "France Info"),
("[URL]http://radionova.ice.infomaniak.ch/radionova-256.aac[/URL]", "NOVA"),
("[URL]https://stream-relay-geo.ntslive.net/stream[/URL]", "NTS 1"),
("[URL]https://stream.subfm.sk/subfmhi[/URL]", "Sub.FM"),
("[URL]http://alphaboys-live.streamguys1.com/alphaboys.mp3[/URL]", "Alpha Boys School"),
]

# Function to display station and volume on the OLED, centered
def display_info(station_index, volume_level):
station_name = stations[station_index][1] # Get the station name
with canvas(device) as draw:
# Calculate position for centered text using textbbox
station_text = f"{station_index + 1} - {station_name}"
volume_text = f"Volume: {volume_level}%"

# Get bounding box for the text
bbox_station = draw.textbbox((0, 0), station_text, font=font)
bbox_volume = draw.textbbox((0, 0), volume_text, font=font)

w_station = bbox_station[2] - bbox_station[0]
w_volume = bbox_volume[2] - bbox_volume[0]

# Center the text horizontally
draw.text(((device.width - w_station) // 2, 20), station_text, font=font, fill="white")
draw.text(((device.width - w_volume) // 2, 40), volume_text, font=font, fill="white")

# Function to select and play the station at the given index
def play_station(index):
global stations
client.clear() # Clear the current MPD playlist
client.add(stations[index][0]) # Add the selected station (URL)
client.play() # Start playing the station
display_info(index, volume_counter) # Update the OLED display

# Function to stop MPD playback using systemctl without sudo
def stop_playback():
try:
# Stop MPD without asking for password
subprocess.run(["systemctl", "stop", "mpd.service"], check=True)
print("MPD stopped successfully.")
except subprocess.CalledProcessError as e:
print(f"Failed to stop MPD: {e}")

# Function to clean up resources and stop the program
def cleanup(signum, frame):
stop_playback() # Stop MPD playback
device.hide() # Turn off the OLED display
lgpio.gpiochip_close(h) # Close the GPIO
print("Cleaned up resources and exiting.")
exit(0) # Ensure the program exits cleanly

# Register signal handler for stopping the program via Thonny or terminal stop
signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup) # SIGINT is for Ctrl+C or Thonny stop

# Initial display
play_station(station_counter)

# Main loop
try:
last_state_clk_station = lgpio.gpio_read(h, clk_station)
last_state_clk_volume = lgpio.gpio_read(h, clk_volume)

while True:
# === Station Encoder ===
current_state_clk_station = lgpio.gpio_read(h, clk_station)

if current_state_clk_station != last_state_clk_station and current_state_clk_station == 1:
# Detect the direction based on the dt pin
if lgpio.gpio_read(h, dt_station) != current_state_clk_station:
station_counter = (station_counter + 1) % len(stations)
else:
station_counter = (station_counter - 1) % len(stations)

# Play the new station
play_station(station_counter)

last_state_clk_station = current_state_clk_station

# === Volume Encoder ===
current_state_clk_volume = lgpio.gpio_read(h, clk_volume)

if current_state_clk_volume != last_state_clk_volume and current_state_clk_volume == 1:
# Detect the direction based on the dt pin
if lgpio.gpio_read(h, dt_volume) != current_state_clk_volume:
volume_counter = min(100, volume_counter + 5) # Increase volume by 5%
else:
volume_counter = max(0, volume_counter - 5) # Decrease volume by 5%

# Set the new volume
client.setvol(volume_counter)
display_info(station_counter, volume_counter)

last_state_clk_volume = current_state_clk_volume

# === Check Volume Button ===
if lgpio.gpio_read(h, btn_volume) == 0: # Button pressed
print("Volume button pressed") # Placeholder for future functionality

# === Check Station Button ===
if lgpio.gpio_read(h, btn_station) == 0: # Button pressed
print("Station button pressed") # Placeholder for future functionality

# Heartbeat: Update display periodically to keep it alive
display_info(station_counter, volume_counter)

# Small delay to debounce
time.sleep(0.1)

except KeyboardInterrupt:
cleanup(signal.SIGINT, None) # Cleanup on user interrupt
 
Back
Top Bottom