-
Notifications
You must be signed in to change notification settings - Fork 0
LED_displays_HD44780 i2c
Contributed by @SimonChelkowski and @davidfri
Due to licensing issues, we can not commit the code to the code base. See above link for details. You find the referenced files at the end of this document.
This pull request related to the open issue #498.
The following files allow using LCD displays based on HD44780 connected via i2c bus for this project. The following displays have been used for testing:
- 2x16 display
- 4x20 display (recommended as more information can be displayed)
Various informations such as artist, album, track_number, track_title, track_time and many more can be displayed see main script for more display options.
The required files are:
- components/displays/HD44780-i2c/i2c_lcd.py
- components/displays/HD44780-i2c/i2c_lcd_driver.py
- components/displays/HD44780-i2c/i2c-lcd.service.default.sample
- components/displays/HD44780-i2c/README.md
The first file is the main LCD script that makes use of I2C_LCD_driver.py. The second file is the library needed to drive the LCD via i2c, originates from DenisFromHR (Denis Pleic) see http://www.circuitbasics.com/raspberry-pi-i2c-lcd-set-up-and-programming The third is used as sample service file that runs the i2c_lcd.py main script at boot-up if the service is properly installed (install description can be found below.). The fourth file is this file which describes the features, usage and installation of the code.
- You need to install additional python libraries. Run the following two command in the command line:
sudo apt-get install i2c-tools python-smbus python3-numpy python-mpdclient python-mpd
pip install smbus numpy python-mpd2
- You need to know which I2C bus your Raspberry Pi has available on GPIOs:
ls /dev/i2c-*
It'll output "/dev/i2c-x", where x is your bus number. Note this bus number as you will need it in step 6.
- Now detect the adapter by using the i2cdetect command, inserting your bus number:
sudo i2cdetect -y bus_number
The I2C address of my LCD is 27. Take note of this number, it will be need in step 6.
- if i2cdetect is not found install i2c-tools
sudo apt-get update
sudo apt-get install i2c-tools
- Next we need to install SMBUS, which gives the Python library weβre going to use access to the I2C bus on the Pi. At the command prompt, enter
sudo apt-get install python-smbus
-
Modify "i2c_lcd_driver.py" line 19 which reads "I2CBUS = 1" and adapt it to your bus number (see step 2.) Furthermore modify line 22 which reads "ADDRESS = 0x27" and adapt it to your I2C address (see step 3.)
-
Modify "i2c_lcd.py" to adapt it yo your specific display e.g. 2x16 or 4x20 (default). The lines 15-19 look like the following:
## Display settings ##
n_cols = 20 # EDIT!!! <-- number of cols your display has ##
n_rows = 4 # EDIT!!! <-- number of rows your display has ##
val_delay = 0.4 # EDIT!!! <-- speed of the scolling text ##
Check if "n_cols" and "n_rows" need to be changed and modify them if necessary. The "val_delay" constant leave for the time being. Lower values will speed up things but will make the text less visible/readable.
- next install and start "i2c-lcd.service"
sudo cp /home/pi/RPi-Jukebox-RFID/components/displays/HD44780-i2c/i2c-lcd.service.default.sample /etc/systemd/system/i2c-lcd.service
- register service by running, it will thereby start on the next boot-up
sudo systemctl enable i2c-lcd
- reboot and enjoy!
For test purposes you can use the following command to start and stop the service without rebooting
to start the service instantly run
sudo systemctl start i2c-lcd
to stop the service instantly run
sudo systemctl stop i2c-lcd
[Unit]
Description=LCD Matrix Display Service
After=network.target phoniebox-rfid-reader.service
[Service]
User=pi
Group=pi
Restart=always
WorkingDirectory=/home/pi/RPi-Jukebox-RFID
ExecStart=/home/pi/RPi-Jukebox-RFID/components/displays/HD44780-i2c/i2c_lcd.py
[Install]
WantedBy=multi-user.target
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import i2c_lcd_driver
from time import *
import time
import subprocess
import numpy
#import datetime
from mpd import MPDClient
### constants
mylcd = i2c_lcd_driver.lcd()
info_at_lines_play = [" "]*4
info_at_lines_pause = [" "]*4
info_at_lines_stop = [" "]*4
info_at_lines_mpd_not_running = [" "]*4
################# CHANGE YOUR SETTINGS HERE!!! ###########################################
## Display settings ##
n_cols = 20 # EDIT!!! <-- number of cols your display has ##
n_rows = 4 # EDIT!!! <-- number of rows your display has ##
val_delay = 0.4 # EDIT!!! <-- speed of the scolling text ##
start_stop_sc_delay=4 ##
use_state_icons="yes" #choose "yes" if you want to use this ##
blinking_icons="no" #its an add-on for "use_state_icons", so "use_state_icons" must be "yes" ##
backlight_off_while_waiting="yes" #not active in state "play" and "pause" ##
backlight_off_delay= 10 # Delay in seconds ##
#### change the following strings to give your box a personal style ##
mpd_not_running_string = "MPD not running" ##
music_stopped_string = "Music stopped!" ##
music_paused_string = "paused!" ##
## ##
################## CHANGE YOUR INFOS, WHICH WILL BE SHOWN ON THE DISPLAY #################
## ##
## You can choose between the following infos: ##
## 'date_and_time', 'artist', 'nothing', 'track_title', 'track_artist_title' ##
## 'track_time', 'pause_string', 'stop_string', 'mpd_not_running_string' ##
## 'track_time_and_number' ##
## ##
## if you have less than 4 lines, you can leave the unused lines by default ##
## ##
## Choose infos while state is "play" ##
## ##
info_at_lines_play [0] = 'date_and_time' # <-- Choose your favorite ##
info_at_lines_play [1] = 'artist' # <-- Choose your favorite ##
info_at_lines_play [2] = 'title' # <-- Choose your favorite ##
info_at_lines_play [3] = 'track_time_and_number' # <-- Choose your favorite ##
## ##
## Choose infos while state is "pause" ##
## ##
info_at_lines_pause [0] = 'date_and_time' # <-- Choose your favorite ##
info_at_lines_pause [1] = 'artist' # <-- Choose your favorite ##
info_at_lines_pause [2] = 'title' # <-- Choose your favorite ##
info_at_lines_pause [3] = 'pause_string' # <-- Choose your favorite ##
## ##
## Choose infos while state is "stop" ##
## ##
info_at_lines_stop [0] = 'date_and_time' # <-- Choose your favorite ##
info_at_lines_stop [1] = 'nothing' # <-- Choose your favorite ##
info_at_lines_stop [2] = 'nothing' # <-- Choose your favorite ##
info_at_lines_stop [3] = 'stop_string' # <-- Choose your favorite ##
## ##
## Choose infos while state is "not_running" ##
## ##
info_at_lines_mpd_not_running [0] = 'date_and_time' # <-- Choose your favorite ##
info_at_lines_mpd_not_running [1] = 'nothing' # <-- Choose your favorite ##
info_at_lines_mpd_not_running [2] = 'nothing' # <-- Choose your favorite ##
info_at_lines_mpd_not_running [3] = 'mpd_not_running_string' # <-- Choose your favorite##
## ##
##########################################################################################
## DO NOT EDIT!!!
clearline = " " * n_cols
string_track_title = " "
string_track_artist_title = " "
string_date_time = " "
track_number = " "
title = " "
playlist_length=" "
last_title = " "
i_counter = 0
state = " "
last_state = "not_running"
track_time = " "
current_time = time.time()
last_time = time.time()
if n_cols >16: # select date_string dependent on how many columns the display has (usually either 16 or 20 rows)
date_string="%d.%m.%Y %H:%M"
else:
date_string = "%d.%m.%y %H:%M" # save two character spaces for displays showing only 16 characters per row
# lines that got to show
lines = [" "*n_cols]*n_rows
last_lines = [" "*n_cols]*n_rows
# User icons
user_icons = [
[0b10000, # Play
0b11000,
0b11100,
0b11110,
0b11100,
0b11000,
0b10000,
0b00000],
[0b00000, # Pause
0b11011,
0b11011,
0b11011,
0b11011,
0b11011,
0b11011,
0b00000],
[0b00000, # Stop
0b11111,
0b11111,
0b11111,
0b11111,
0b11111,
0b00000,
0b00000],
[0b00000, # Offline
0b00000,
0b01010,
0b00000,
0b01110,
0b10001,
0b00000,
0b00000]]
def print_changes (string_new,string_old,row):
for pos in range(len(string_new)):
if string_new[pos] != string_old[pos]:
mylcd.lcd_display_string(string_new[pos], row, pos)
def fill_with_spaces(string1,length):
if len(string1) <= length:
return (string1 + " " * (length - len(string1)))
else:
return string1
def loop_string(string1, string2):
my_long_string = string2
title_max_length = n_cols - len(string1) #max_len is dependent by len (track_number)
position = numpy.clip((i_counter % (len(string2)-(title_max_length-1)+2*start_stop_sc_delay))-start_stop_sc_delay,0,len(string2)-(title_max_length))
scroll_text = my_long_string[position:(position+title_max_length)]
return (string1+scroll_text)
def print_nothing():
return clearline
def print_pause_string():
return (fill_with_spaces(music_paused_string,n_cols))
def print_stop_string():
return (fill_with_spaces(music_stopped_string,n_cols))
def print_mpd_not_running_string():
return (fill_with_spaces(mpd_not_running_string,n_cols))
def print_artist():
if len(artist)<=n_cols:
return fill_with_spaces(artist,n_cols)
else:
return loop_string("", artist) ### SC version
def print_track_title():
## Write Track number & Title into the row of the display
string_track_title = track_number + ":" + title
if len(string_track_title)<=n_cols:
return fill_with_spaces(string_track_title,n_cols)
else:
return loop_string(track_number + ":", title) ### SC version
def print_title():
## Write Title into the row of the display
if len(title)<=n_cols:
return fill_with_spaces(title,n_cols)
else:
return loop_string("",title) ### SC version
def print_track_artist_title():
string_track_artist_title = track_number + ":" + artist + " - "+ title
if len(string_track_artist_title)<=n_cols:
return fill_with_spaces(string_track_artist_title,n_cols)
else:
return loop_string(track_number+":", artist + " - " + title) ### SC version
def print_artist_title():
string_artist_title = artist + " - "+ title
if len(string_artist_title)<=n_cols:
return fill_with_spaces(string_artist_title,n_cols)
else:
return loop_string("", artist + " - " + title) ### SC version
def print_track_time():
return fill_with_spaces(track_time,n_cols)
def print_track_time_and_number():
song_of_playlist = track_number + "/" + playlist_length
return (fill_with_spaces(track_time,n_cols))[:(n_cols-len(song_of_playlist))] + song_of_playlist
def print_date_time():
return fill_with_spaces(time.strftime(date_string),n_cols)
def choose_line(info_text):
switcher = {
'pause_string': print_pause_string(),
'stop_string': print_stop_string(),
'mpd_not_running_string': print_mpd_not_running_string(),
'track_title': print_track_title(),
'track_artist_title': print_track_artist_title(),
'artist_title': print_artist_title(),
'artist': print_artist(),
'title': print_title(),
'date_and_time': print_date_time(),
'nothing': print_nothing(),
'track_time': print_track_time(),
'track_time_and_number': print_track_time_and_number()
}
return switcher.get(info_text, fill_with_spaces("ERROR",n_cols))
def choose_icon(state):
switcher = {
'play': "\x00",
'pause': "\x01",
'stop': "\x02",
'not_running': "\x03",
}
return switcher.get(state, " ")
def sec_to_min_and_sec(seconds):
return (str('%d'%(int(seconds) / 60))+":"+str('%0.2d'%(int(seconds) % 60)))
######### BEGIN OF CODE ################################
## init mpd-client
try:
client = MPDClient()
client.timeout=0.3
client.connect("localhost", 6600) #
except Exception: # if reconnect isn't possible, client is not running #
print("mpd not avalible")
if use_state_icons=="yes":
mylcd.lcd_load_custom_chars(user_icons)
try:
while True:
##################### RESET ALL VALUES ##########################################
mpd_info = " " #
track_number = "0" #
title = " " #
album = " " #
artist = " " #
track_time = "0.0/0.0" #
#################################################################################
########################## GET STATE ############################################
try: #
client.ping() # test if client is up #
status=client.status() #
state=status['state'] #
current_song_infos=client.currentsong() #
except Exception: #
try: # if client is not connected, try to reconnect #
client = MPDClient() #
client.timeout=0.3 #
client.connect("localhost", 6600) #
status=client.status() #
state=status['state'] #
current_song_infos=client.currentsong() #
except Exception: # if reconnect isn't possible, client is not running #
state="not_running" #
# it is running, get more details #
#################################################################################
########### RESTART COUNTER, IF STATE CHANGED####################################
#
if last_state != state: # if state changed, the scrolltext starts at position 0 #
i_counter = 0 #
mylcd.backlight (1)
#################################################################################
########### GET INFOS, IF STATE IS "NOT_RUNNING" ################################
if state == "not_running": #
for row in range(n_rows): #
lines[row] = choose_line(info_at_lines_mpd_not_running [row]) #
#################################################################################
########### GET INFOS, IF STATE IS "STOP" #######################################
elif state == "stop": #
for row in range(n_rows): #
lines[row] = choose_line(info_at_lines_stop [row]) #
#################################################################################
else:
########## GET MORE SONG-INFOS ####################################################
# This section reads in certain album informations #
# #
## read in track number #
try: #
track_number = current_song_infos['track'] #
except KeyError: #
track_number = "1" #
## read in playlistlength #
try: #
playlist_length = status['playlistlength'] #
except KeyError: #
playlist_length = "1" #
## read in track title #
try: #
title = current_song_infos['title'] #
title = title.replace("\n", "").replace("Γ€", "\341").replace("ΓΆ", "\357").replace("ΓΌ", "\365").replace("Γ", "\342").replace("Γ", "\341").replace("Γ", "\357").replace("Γ", "\365") # weitere codes siehe https://www.mikrocontroller.net/topic/293125 #
except KeyError: #
title = "" #
## read in album title #
try: #
album = current_song_infos['album'] #
album = album.replace("\n", "").replace("Γ€", "\341").replace("ΓΆ", "\357").replace("ΓΌ", "\365").replace("Γ", "\342").replace("Γ", "\341").replace("Γ", "\357").replace("Γ", "\365") # weitere codes siehe https://www.mikrocontroller.net/topic/293125 #
except KeyError: #
album = "" #
## read in artist info #
try: #
artist = current_song_infos['artist'] #
artist = artist.replace("\n", "").replace("Γ€", "\341").replace("ΓΆ", "\357").replace("ΓΌ", "\365").replace("Γ", "\342").replace("Γ", "\341").replace("Γ", "\357").replace("Γ", "\365") # weitere codes siehe https://www.mikrocontroller.net/topic/293125 #
except KeyError: #
artist = "" #
if (client.mpd_version) >= "0.20":
try: #
elapsed=status['elapsed'].split(".")[0] #
duration=status['duration'].split(".")[0] #
track_time = sec_to_min_and_sec(elapsed)+"/"+sec_to_min_and_sec(duration) #
except KeyError: #
track_time = "" #
else: #
track_time = subprocess.check_output('mpc | head -n2 | tail -n1 | sed "s/ \+/ /g" | cut -d" " -f3', shell=True)
track_time = track_time.replace("\n", "") #
###########################################################################################
############# RESET GLOBAL COUNTER, IF TITLE CHANGED ############################
if last_title != title: # if title changed, the scrolltext starts at position 0 #
i_counter = 0 #
#################################################################################
########### GET INFOS, IF STATE IS "PAUSE" ######################################
if state == "pause": #
for row in range(n_rows): #
lines[row] = choose_line(info_at_lines_pause [row]) #
#################################################################################
########### GET INFOS, IF STATE IS "PLAY" #######################################
if state == "play": #
for row in range(n_rows): #
lines[row] = choose_line(info_at_lines_play [row]) #
#################################################################################
################## EXACT CYCLE TIME ##################################################
current_time=time.time() #
# calculate real sleep-time - if cycle to long, use 0.0 #
extra_sleep=(max(val_delay-(current_time-last_time),0.0)) # calculate real sleep #
time.sleep(extra_sleep) # time the display stays as is before re-running the loop #
current_time=time.time() # get current time after sleep #
# print ("Cycle-time:"+str(current_time-last_time)) #only for debugging #
last_time=current_time # save time for next cycle #
######################################################################################
######################## ADD STATE ICONS #############################################
## add blinking state icon in first row #
if use_state_icons=="yes": #
if blinking_icons =="yes": #
if i_counter%2==0: #
icon=choose_icon(state) #
else: #
icon=" " #
else: #
icon=choose_icon(state) #
lines[0]= lines[0][:n_cols-2]+" "+icon #
######################################################################################
######################## DISPLAY OFF AFTER A WHILE ################################
if (i_counter*val_delay)>=backlight_off_delay and backlight_off_while_waiting == "yes" and (state != "play" and state != "pause"):
mylcd.backlight (0)
else:
######################################################################################
######################## PRINT ALL CHANGES ON DISPLAY ################################
for row in range(n_rows): #
print_changes (lines[row],last_lines[row],row+1) #
######################################################################################
####################### UPDATE COUNTER ###############################################
i_counter += 1 #
if i_counter >= 65000: #
i_counter = 1000 #<-- not 0, cause the display could be off #
######################################################################################
####################### REMIND STUFF FOR NEXT CYCLE #################################
last_state=state #
last_title=title #
for row in range(n_rows): #
last_lines[row]=lines[row] #
######################################################################################
except KeyboardInterrupt:
lines[0]=print_date_time()
lines[1]=print_nothing()
if n_rows >= 3:
lines[2]=print_nothing()
if n_rows >= 4:
lines[3]=print_nothing()
for row in range(n_rows):
print_changes (lines[row],last_lines[row],row+1)
client.close() # send the close command
client.disconnect() # disconnect from the server
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Original code found at:
# https://gist.github.com/DenisFromHR/cc863375a6e19dce359d
"""
Compiled, mashed and generally mutilated 2014-2015 by Denis Pleic
Made available under GNU GENERAL PUBLIC LICENSE
# Modified Python I2C library for Raspberry Pi
# as found on http://www.recantha.co.uk/blog/?p=4849
# Joined existing 'i2c_lib.py' and 'lcddriver.py' into a single library
# added bits and pieces from various sources
# By DenisFromHR (Denis Pleic)
# 2015-02-10, ver 0.1
"""
# i2c bus (0 -- original Pi, 1 -- Rev 2 Pi)
I2CBUS = 1
# LCD Address
ADDRESS = 0x27
import smbus
from time import sleep
class i2c_device:
def __init__(self, addr, port=I2CBUS):
self.addr = addr
self.bus = smbus.SMBus(port)
# Write a single command
def write_cmd(self, cmd):
self.bus.write_byte(self.addr, cmd)
sleep(0.0001)
# Write a command and argument
def write_cmd_arg(self, cmd, data):
self.bus.write_byte_data(self.addr, cmd, data)
sleep(0.0001)
# Write a block of data
def write_block_data(self, cmd, data):
self.bus.write_block_data(self.addr, cmd, data)
sleep(0.0001)
# Read a single byte
def read(self):
return self.bus.read_byte(self.addr)
# Read
def read_data(self, cmd):
return self.bus.read_byte_data(self.addr, cmd)
# Read a block of data
def read_block_data(self, cmd):
return self.bus.read_block_data(self.addr, cmd)
# commands
LCD_CLEARDISPLAY = 0x01
LCD_RETURNHOME = 0x02
LCD_ENTRYMODESET = 0x04
LCD_DISPLAYCONTROL = 0x08
LCD_CURSORSHIFT = 0x10
LCD_FUNCTIONSET = 0x20
LCD_SETCGRAMADDR = 0x40
LCD_SETDDRAMADDR = 0x80
# flags for display entry mode
LCD_ENTRYRIGHT = 0x00
LCD_ENTRYLEFT = 0x02
LCD_ENTRYSHIFTINCREMENT = 0x01
LCD_ENTRYSHIFTDECREMENT = 0x00
# flags for display on/off control
LCD_DISPLAYON = 0x04
LCD_DISPLAYOFF = 0x00
LCD_CURSORON = 0x02
LCD_CURSOROFF = 0x00
LCD_BLINKON = 0x01
LCD_BLINKOFF = 0x00
# flags for display/cursor shift
LCD_DISPLAYMOVE = 0x08
LCD_CURSORMOVE = 0x00
LCD_MOVERIGHT = 0x04
LCD_MOVELEFT = 0x00
# flags for function set
LCD_8BITMODE = 0x10
LCD_4BITMODE = 0x00
LCD_2LINE = 0x08
LCD_1LINE = 0x00
LCD_5x10DOTS = 0x04
LCD_5x8DOTS = 0x00
# flags for backlight control
LCD_BACKLIGHT = 0x08
LCD_NOBACKLIGHT = 0x00
En = 0b00000100 # Enable bit
Rw = 0b00000010 # Read/Write bit
Rs = 0b00000001 # Register select bit
class lcd:
#initializes objects and lcd
def __init__(self):
self.lcd_device = i2c_device(ADDRESS)
self.lcd_write(0x03)
self.lcd_write(0x03)
self.lcd_write(0x03)
self.lcd_write(0x02)
self.lcd_write(LCD_FUNCTIONSET | LCD_2LINE | LCD_5x8DOTS | LCD_4BITMODE)
self.lcd_write(LCD_DISPLAYCONTROL | LCD_DISPLAYON)
self.lcd_write(LCD_CLEARDISPLAY)
self.lcd_write(LCD_ENTRYMODESET | LCD_ENTRYLEFT)
sleep(0.2)
# clocks EN to latch command
def lcd_strobe(self, data):
self.lcd_device.write_cmd(data | En | LCD_BACKLIGHT)
sleep(.0005)
self.lcd_device.write_cmd(((data & ~En) | LCD_BACKLIGHT))
sleep(.0001)
def lcd_write_four_bits(self, data):
self.lcd_device.write_cmd(data | LCD_BACKLIGHT)
self.lcd_strobe(data)
# write a command to lcd
def lcd_write(self, cmd, mode=0):
self.lcd_write_four_bits(mode | (cmd & 0xF0))
self.lcd_write_four_bits(mode | ((cmd << 4) & 0xF0))
# write a character to lcd (or character rom) 0x09: backlight | RS=DR<
# works!
def lcd_write_char(self, charvalue, mode=1):
self.lcd_write_four_bits(mode | (charvalue & 0xF0))
self.lcd_write_four_bits(mode | ((charvalue << 4) & 0xF0))
# put string function with optional char positioning
def lcd_display_string(self, string, line=1, pos=0):
if line == 1:
pos_new = pos
elif line == 2:
pos_new = 0x40 + pos
elif line == 3:
pos_new = 0x14 + pos
elif line == 4:
pos_new = 0x54 + pos
self.lcd_write(0x80 + pos_new)
for char in string:
self.lcd_write(ord(char), Rs)
# clear lcd and set to home
def lcd_clear(self):
self.lcd_write(LCD_CLEARDISPLAY)
self.lcd_write(LCD_RETURNHOME)
# define backlight on/off (lcd.backlight(1); off= lcd.backlight(0)
def backlight(self, state): # for state, 1 = on, 0 = off
if state == 1:
self.lcd_device.write_cmd(LCD_BACKLIGHT)
elif state == 0:
self.lcd_device.write_cmd(LCD_NOBACKLIGHT)
# add custom characters (0 - 7)
def lcd_load_custom_chars(self, fontdata):
self.lcd_write(0x40);
for char in fontdata:
for line in char:
self.lcd_write_char(line)
If you like Phoniebox, consider: buy me a coffee or PayPal
- Code: https://github.com/MiczFlor/RPi-Jukebox-RFID
- Phoniebox home page: English | Deutsch
Phoniebox is a contactless jukebox for the Raspberry Pi, playing audio files, playlists, podcasts, web streams and spotify triggered by RFID cards. All plug and play via USB, no soldering iron needed. Update: if you must, it now also features a howto for adding GPIO buttons controls.
Visit Phoniebox.de
π₯ Version 3
- β Releases
- π΅ Install Jukebox Version 3
- π Report a bug
- π Propose a feature
- βοΈ Feature Status
- π Documentation
- π©βπ» Development
- βοΈ Contributing
- π¦ Code
πΆ Version 2
- β Releases
- π΅ Install Jukebox Version 2
- π Report a bug
- βοΈ Features
- π Documentation
- βοΈ Contributing
- π¦ Code
Version 2 Pages
-
Setup / Upgrades
- Synchronising Phonieboxes in a local network
- Smart Home remote control with MQTT
- Hardware Pinout Overview
- Systemwide Equalizer
- Phoniebox with read-only Filesystem
- HiFiBerry Soundcard Details
- WM8960 Hi-Fi HAT
- PAM8403 Amplifier Power Off
- TPA3118 Amplifier Power Off and EMI improvement
- External Non USB Audio DAC ES9023, PCM5102, etc.
- On-board LEDs with fibre optics
- Setting GPIOs at boot time
- Stop on removal with USB RFID Reader
- Firmware update improves audio out
- Architecture