From 3d7916da8ec717eb74d18e15f8cea54487db23d9 Mon Sep 17 00:00:00 2001 From: Pit64 Date: Wed, 26 Nov 2025 13:09:31 +0100 Subject: [PATCH] feat(cases): add picade case support --- .../frontend/es-core/src/hardware/Case.cpp | 6 + projects/frontend/es-core/src/hardware/Case.h | 1 + .../src/hardware/boards/pis/PiBoard.cpp | 1 + projects/recalbox-hardware/case/cases.py | 1 + projects/recalbox-hardware/case/installer.py | 1 + .../case/installers/picade/__init__.py | 0 .../installers/picade/assets/10-picade.rules | 1 + .../case/installers/picade/assets/asound.conf | 7 + .../picade/assets/overlays/picade.dts | 203 ++++++++++++++++++ .../picade/assets/recalbox-user-config.txt | 41 ++++ .../case/installers/picade/install.py | 149 +++++++++++++ projects/recalbox-hardware/case/manage.py | 2 +- 12 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 projects/recalbox-hardware/case/installers/picade/__init__.py create mode 100644 projects/recalbox-hardware/case/installers/picade/assets/10-picade.rules create mode 100644 projects/recalbox-hardware/case/installers/picade/assets/asound.conf create mode 100644 projects/recalbox-hardware/case/installers/picade/assets/overlays/picade.dts create mode 100644 projects/recalbox-hardware/case/installers/picade/assets/recalbox-user-config.txt create mode 100644 projects/recalbox-hardware/case/installers/picade/install.py diff --git a/projects/frontend/es-core/src/hardware/Case.cpp b/projects/frontend/es-core/src/hardware/Case.cpp index db1bbe0f16..bc88650b3b 100644 --- a/projects/frontend/es-core/src/hardware/Case.cpp +++ b/projects/frontend/es-core/src/hardware/Case.cpp @@ -30,6 +30,7 @@ bool Case::Install() const case CaseModel::Nespi4CaseManual: case CaseModel::SuperPi4Case: case CaseModel::RaspberryPiTouchDisplay: + case CaseModel::Picade: { SetCaseInBoot(mShortName); break; @@ -110,6 +111,7 @@ Case Case::FromShortName(const String& value) if (value == "ArgonOneV3") return Create(CaseModel::ArgonOneV3); if (value == "RaspberryPiTouchDisplay") return Create(CaseModel::RaspberryPiTouchDisplay); if (value == "rgbhat") return Create(CaseModel::RecalboxRGBDualOrRGBHat); + if (value == "Picade") return Create(CaseModel::Picade); return Create(CaseModel::None); } @@ -163,6 +165,8 @@ Case Case::Create(CaseModel model) return Case(CaseModel::RaspberryPiTouchDisplay, CASE_DETECTION_AUTOMATIC, MENU_SHUTDOWN_ENABLED,CASE_ROTATION_SUPPORTED, "Raspberry Pi Touch Display", "RaspberryPiTouchDisplay", ""); case CaseModel::RecalboxRGBDualOrRGBHat: return Case(CaseModel::RecalboxRGBDualOrRGBHat, CASE_DETECTION_AUTOMATIC, MENU_SHUTDOWN_ENABLED, CASE_ROTATION_SUPPORTED, "Recalbox RGB Dual or RGB Hat", "rgbhat", ""); + case CaseModel::Picade: + return Case(CaseModel::Picade, !CASE_DETECTION_AUTOMATIC, MENU_SHUTDOWN_ENABLED, !CASE_ROTATION_SUPPORTED, "Picade (Pimoroni)", "Picade", ""); } return Case(CaseModel::None, !CASE_DETECTION_AUTOMATIC, MENU_SHUTDOWN_ENABLED, CASE_ROTATION_SUPPORTED, _("NONE"), "", ""); } @@ -197,6 +201,7 @@ std::vector Case::SupportedManualCases() list.push_back(Case::Create(Case::CaseModel::Nespi4CaseManual)); list.push_back(Case::Create(Case::CaseModel::SuperPi4Case)); list.push_back(Case::Create(Case::CaseModel::RaspberryPiTouchDisplay)); + list.push_back(Case::Create(Case::CaseModel::Picade)); list.push_back(Case::Create(Case::CaseModel::None)); } else if (Board::Instance().GetBoardType() == BoardType::Pi3plus || Board::Instance().GetBoardType() == BoardType::Pi3) @@ -205,6 +210,7 @@ std::vector Case::SupportedManualCases() list.push_back(Case::Create(Case::CaseModel::SuperPiCase)); list.push_back(Case::Create(Case::CaseModel::NespiCasePlus)); list.push_back(Case::Create(Case::CaseModel::RaspberryPiTouchDisplay)); + list.push_back(Case::Create(Case::CaseModel::Picade)); list.push_back(Case::Create(Case::CaseModel::None)); } return list; diff --git a/projects/frontend/es-core/src/hardware/Case.h b/projects/frontend/es-core/src/hardware/Case.h index 0e9716ecbf..8cfb7830d2 100644 --- a/projects/frontend/es-core/src/hardware/Case.h +++ b/projects/frontend/es-core/src/hardware/Case.h @@ -42,6 +42,7 @@ class Case ArgonOneV3, RaspberryPiTouchDisplay, RecalboxRGBDualOrRGBHat, + Picade, None, }; diff --git a/projects/frontend/es-core/src/hardware/boards/pis/PiBoard.cpp b/projects/frontend/es-core/src/hardware/boards/pis/PiBoard.cpp index 29a41a288a..a2aab16c8c 100644 --- a/projects/frontend/es-core/src/hardware/boards/pis/PiBoard.cpp +++ b/projects/frontend/es-core/src/hardware/boards/pis/PiBoard.cpp @@ -59,6 +59,7 @@ bool PiBoard::HasBattery() case Case::CaseModel::RecalboxRGBDualOrRGBHat: case Case::CaseModel::SixtyFourPiCase: case Case::CaseModel::DreamCase: + case Case::CaseModel::Picade: case Case::CaseModel::None: return false; break; diff --git a/projects/recalbox-hardware/case/cases.py b/projects/recalbox-hardware/case/cases.py index 7d24eea451..308a4a6864 100644 --- a/projects/recalbox-hardware/case/cases.py +++ b/projects/recalbox-hardware/case/cases.py @@ -18,4 +18,5 @@ DREAMCASE = "DreamCase" ARGONONE = "ArgonOne" ARGONONEV3 = "ArgonOneV3" RPITOUCHDISPLAY = "RaspberryPiTouchDisplay" +PICADE = "Picade" NONE = "none" diff --git a/projects/recalbox-hardware/case/installer.py b/projects/recalbox-hardware/case/installer.py index 0217c76ed8..8e626d377e 100644 --- a/projects/recalbox-hardware/case/installer.py +++ b/projects/recalbox-hardware/case/installer.py @@ -12,6 +12,7 @@ MODULES = { "installers.argononev3.install": (cases.ARGONONEV3,), "installers.retroflags.install": (cases.SUPERPI4CASE, cases.NESPI4, cases.NESPI4MANUAL, cases.PISTATION, cases.NESPICASEPLUS, cases.SUPERPICASE, cases.MEGAPICASE, cases.SIXTYFOURPICASE, cases.DREAMCASE), "installers.raspberrypi-touch-display.install": (cases.RPITOUCHDISPLAY,), + "installers.picade.install": (cases.PICADE,), } def processHardware(install, case, previousCase): diff --git a/projects/recalbox-hardware/case/installers/picade/__init__.py b/projects/recalbox-hardware/case/installers/picade/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/projects/recalbox-hardware/case/installers/picade/assets/10-picade.rules b/projects/recalbox-hardware/case/installers/picade/assets/10-picade.rules new file mode 100644 index 0000000000..29ecb920fc --- /dev/null +++ b/projects/recalbox-hardware/case/installers/picade/assets/10-picade.rules @@ -0,0 +1 @@ +SUBSYSTEM=="input", ATTRS{name}=="gpio_keys", ENV{ID_INPUT_KEYBOARD}="1" diff --git a/projects/recalbox-hardware/case/installers/picade/assets/asound.conf b/projects/recalbox-hardware/case/installers/picade/assets/asound.conf new file mode 100644 index 0000000000..33c46c82f0 --- /dev/null +++ b/projects/recalbox-hardware/case/installers/picade/assets/asound.conf @@ -0,0 +1,7 @@ +pcm.!default { + type hw card 0 +} + +ctl.!default { + type hw card 0 +} diff --git a/projects/recalbox-hardware/case/installers/picade/assets/overlays/picade.dts b/projects/recalbox-hardware/case/installers/picade/assets/overlays/picade.dts new file mode 100644 index 0000000000..938ac58ed9 --- /dev/null +++ b/projects/recalbox-hardware/case/installers/picade/assets/overlays/picade.dts @@ -0,0 +1,203 @@ +/dts-v1/; +/plugin/; + +/ { + compatible = "brcm,bcm2835", "brcm,bcm2708", "brcm,bcm2709", "brcm,bcm2711"; + + fragment@0 { + target = <&gpio>; + __overlay__ { + picade_pins: picade_pins { + brcm,pins = <17 5 6 8 9 10 11 12 16 20 22 23 24 25 27>; + brcm,function = <0 0 0 0 0 0 0 0 0 0 0 0 0 0 0>; + brcm,pull = <2 2 2 2 2 2 2 2 2 2 2 2 2 2 2>; + }; + power_ctrl_pins: power_ctrl_pins { + brcm,pins = <4>; + brcm,function = <1>; + }; + i2s_pins: i2s_pins { + /* + We cannot share BCM 20 between gpio keys (Left) and i2s (PCM_DIN) + Assign PCM_DIN to GPIO 26, which is unused by Picade HAT X. + */ + brcm,pins = <18 19 26 21>; + brcm,function = <4 4 4 4>; + }; + }; + }; + + fragment@1 { + target-path = "/"; + __overlay__ { + power_ctrl: power_ctrl { + compatible = "gpio-poweroff"; + gpios = <&gpio 4 1>; + force; + }; + }; + }; + + fragment@2 { + target-path = "/"; + __overlay__ { + gpio_keys: gpio_keys { + compatible = "gpio-keys"; + pinctrl-names = "default"; + pinctrl-0 = <&picade_pins>; + status = "okay"; + + up: up { + label = "Up"; + linux,code = <103>; + gpios = <&gpio 12 1>; + }; + + down: down { + label = "Down"; + linux,code = <108>; + gpios = <&gpio 6 1>; + }; + + left: left { + label = "Left"; + linux,code = <105>; + gpios = <&gpio 20 1>; + }; + + right: right { + label = "Right"; + linux,code = <106>; + gpios = <&gpio 16 1>; + }; + + button1: button1 { + label = "Button 1"; + linux,code = <29>; + gpios = <&gpio 5 1>; + }; + + button2: button2 { + label = "Button 2"; + linux,code = <56>; + gpios = <&gpio 11 1>; + }; + + button3: button3 { + label = "Button 3"; + linux,code = <57>; + gpios = <&gpio 8 1>; + }; + + button4: button4 { + label = "Button 4"; + linux,code = <42>; + gpios = <&gpio 25 1>; + }; + + button5: button5 { + label = "Button 5"; + linux,code = <44>; + gpios = <&gpio 9 1>; + }; + + button6: button6 { + label = "Button 6"; + linux,code = <45>; + gpios = <&gpio 10 1>; + }; + + enter: enter { + label = "Enter"; + linux,code = <28>; + gpios = <&gpio 27 1>; + }; + + escape: escape { + label = "Escape"; + linux,code = <1>; + gpios = <&gpio 22 1>; + }; + + coin: coin { + label = "Coin"; + linux,code = <23>; + gpios = <&gpio 23 1>; + }; + + start: start { + label = "Start"; + linux,code = <24>; + gpios = <&gpio 24 1>; + }; + + power: power { + label = "Power"; + linux,code = <116>; // KEY_POWER + gpios = <&gpio 17 1>; + }; + }; + }; + }; + + fragment@3 { + target = <&i2s>; + __overlay__ { + status = "okay"; + pinctrl-names = "default"; + pinctrl-0 = <&i2s_pins>; + }; + }; + + fragment@4 { + target-path = "/"; + __overlay__ { + pcm5102a-codec { + #sound-dai-cells = <0>; + compatible = "ti,pcm5102a"; + status = "okay"; + }; + }; + }; + + fragment@5 { + target = <&sound>; + __overlay__ { + compatible = "hifiberry,hifiberry-dac"; + i2s-controller = <&i2s>; + status = "okay"; + }; + }; + + fragment@6 { + target-path = "/"; + __overlay__ { + act_led: act_led { + compatible = "gpio-leds"; + gpios = <&gpio 13 1>; + linux,default-trigger = "default-on"; + }; + }; + }; + + __overrides__ { + up = <&up>,"linux,code:0"; + down = <&down>,"linux,code:0"; + left = <&left>,"linux,code:0"; + right = <&right>,"linux,code:0"; + button1 = <&button1>,"linux,code:0"; + button2 = <&button2>,"linux,code:0"; + button3 = <&button3>,"linux,code:0"; + button4 = <&button4>,"linux,code:0"; + button5 = <&button5>,"linux,code:0"; + button6 = <&button6>,"linux,code:0"; + enter = <&enter>,"linux,code:0"; + escape = <&escape>,"linux,code:0"; + coin = <&coin>,"linux,code:0"; + start = <&start>,"linux,code:0"; + led-trigger = <&act_led>,"linux,default-trigger"; + noaudio = <0>,"-3-4-5"; + noactled = <0>,"-6"; + nopoweroff = <0>,"-1"; + }; +}; diff --git a/projects/recalbox-hardware/case/installers/picade/assets/recalbox-user-config.txt b/projects/recalbox-hardware/case/installers/picade/assets/recalbox-user-config.txt new file mode 100644 index 0000000000..2d0b1d7566 --- /dev/null +++ b/projects/recalbox-hardware/case/installers/picade/assets/recalbox-user-config.txt @@ -0,0 +1,41 @@ +# Change to your needs + +# uncomment if you get no picture on HDMI for a default "safe" mode +#hdmi_safe=1 + +disable_overscan=1 + +# uncomment to force a specific HDMI mode (this will force VGA) +#hdmi_group=1 +#hdmi_mode=1 + +# Sound output. Set to 0 or comment for autodetect, 1 for DVI, 2 to force HDMI. +#hdmi_drive=2 + +config_hdmi_boost=0 + +# uncomment for composite PAL +#sdtv_mode=2 + +# uncomment for lirc-rpi +#dtoverlay=lirc-rpi + +# uncomment if you have chinese TV display and display is garbled or slow +#hdmi_ignore_edid=0xa5000080 + +############################# PICADE ############################ +dtoverlay=picade +dtparam=up= +dtparam=down= +dtparam=left= +dtparam=right= +dtparam=button1= +dtparam=button2= +dtparam=button3= +dtparam=button4= +dtparam=button5= +dtparam=button6= +dtparam=enter= +# dtparam=escape= +dtparam=coin= +dtparam=start= diff --git a/projects/recalbox-hardware/case/installers/picade/install.py b/projects/recalbox-hardware/case/installers/picade/install.py new file mode 100644 index 0000000000..bbf6a18349 --- /dev/null +++ b/projects/recalbox-hardware/case/installers/picade/install.py @@ -0,0 +1,149 @@ +import os +import logger +import shutil +from installers.base.install import InstallBase +from settings import keyValueSettings + +class Install(InstallBase): + + BASE_SOURCE_FOLDER = InstallBase.BASE_SOURCE_FOLDER + "picade/" + DTBO_FILE = "/boot/overlays/picade.dtbo" + SOUND_FILE = "/etc/asound.conf" + UDEV_FILE = "/etc/udev/rules.d/10-picade.rules" + RECALBOX_CONF = "/recalbox/share/system/recalbox.conf" + + def __init__(self): + InstallBase.__init__(self) + + def InstallHardware(self, case): + logger.hardlog("Installing Picade Case hardware") + try: + os.system("mount -o remount,rw /boot") + + # Install /boot/recalbox-user-config.txt + files = { + '/boot/recalbox-user-config.txt': '/boot/recalbox-user-config.txt.backup', + self.BASE_SOURCE_FOLDER + 'assets/recalbox-user-config.txt': '/boot/recalbox-user-config.txt', + } + for source_file, dest_file in files.items(): + installed_file = shutil.copy(source_file, dest_file) + logger.hardlog(f"Picade: {installed_file} installed") + + except Exception as e: + logger.hardlog("Picade: Exception = {}".format(e)) + return False + + finally: + os.system("mount -o remount,ro /boot") + + logger.hardlog("Picade Case hardware installed successfully!") + return True + + def InstallSoftware(self, case): + logger.hardlog("Installing Picade Case software") + try: + os.system("mount -o remount,rw /") + + # Load recalbox.conf + recalboxConf = keyValueSettings(self.RECALBOX_CONF, False) + recalboxConf.loadFile() + + # Install /etc/asound.conf + if os.system('cp {}assets/asound.conf {}'.format(self.BASE_SOURCE_FOLDER, self.SOUND_FILE)) != 0: + logger.hardlog("Picade: error copying asound.conf") + return False + logger.hardlog("Picade: asound.conf installed") + + # Install /etc/udev/rules.d/10-picade.rules + if os.system('cp {}assets/10-picade.rules {}'.format(self.BASE_SOURCE_FOLDER, self.UDEV_FILE)) != 0: + logger.hardlog("Picade: error copying 10-picade.rules") + return False + logger.hardlog("Picade: 10-picade.rules installed") + + # Switch audio device + recalboxConf.setOption("audio.device", "alsa_card.platform-soc_sound:analog-output:output:stereo-fallback") + recalboxConf.saveFile() + logger.hardlog("Picade: set audio device") + + # Enable GPIO controllers + recalboxConf.setOption("controllers.gpio.enabled", "1") + recalboxConf.saveFile() + logger.hardlog("Picade: enable gpio controllers") + + # Define default GPIO controllers arguments + recalboxConf.setOption("controllers.gpio.args", "map=5 gpio=12,6,20,16,24,23,9,25,10,5,11,8,27") + recalboxConf.saveFile() + logger.hardlog("Picade: set gpio controllers arguments") + + except Exception as e: + logger.hardlog("Picade: Exception = {}".format(e)) + return "" + + finally: + os.system("mount -o remount,ro /") + + logger.hardlog("Picade Case software installed successfully!") + return case + + def UninstallHardware(self, case): # NOQA + logger.hardlog("Uninstalling Picade Case hardware") + try: + os.system("mount -o remount,rw /") + os.system("mount -o remount,rw /boot") + + # Uninstall /boot/recalbox-user-config.txt + if os.system("cp /boot/recalbox-user-config.txt.backup /boot/recalbox-user-config.txt") != 0: + logger.hardlog("Picade: Error uninstalling recalbox-user-config.txt") + return False + logger.hardlog("Picade: recalbox-user-config.txt uninstalled") + + except Exception as e: + logger.hardlog("Picade: Exception = {}".format(e)) + return False + + finally: + os.system("mount -o remount,ro /") + os.system("mount -o remount,ro /boot") + + return True + + def UninstallSoftware(self, case): + logger.hardlog("Uninstalling Picade Case software") + try: + os.system("mount -o remount,rw /") + os.system("mount -o remount,rw /boot") + + # Uninstall /etc/asound.conf + if os.system("rm -f {}".format(self.SOUND_FILE)) != 0: + logger.hardlog("Picade: error removing asound.conf") + return False + logger.hardlog("Picade: asound.conf uninstalled") + + # Uninstall /etc/udev/rules.d/10-picade.rules + if os.system("rm -f {}".format(self.UDEV_FILE)) != 0: + logger.hardlog("Picade: error removing 10-picade.rules") + return False + logger.hardlog("Picade: 10-picade.rules uninstalled") + + # Load recalbox.conf + recalboxConf = keyValueSettings(self.RECALBOX_CONF, False) + recalboxConf.loadFile() + + # Disable GPIO controllers + recalboxConf.setOption("controllers.gpio.enabled", "0") + recalboxConf.saveFile() + logger.hardlog("Picade: GPIO controllers disabled") + + except Exception as e: + logger.hardlog("Picade: Exception = {}".format(e)) + return False + + finally: + os.system("mount -o remount,ro /") + os.system("mount -o remount,ro /boot") + + return True + + def GetInstallScript(self, case): # NOQA + + return None diff --git a/projects/recalbox-hardware/case/manage.py b/projects/recalbox-hardware/case/manage.py index b808824579..40a2024f49 100644 --- a/projects/recalbox-hardware/case/manage.py +++ b/projects/recalbox-hardware/case/manage.py @@ -152,7 +152,7 @@ def DetectRaspberryPiTouchDisplay(): return cases.NONE # --------- Main -manualCases = (cases.SUPERPI4CASE, cases.NESPI4MANUAL, cases.PISTATION, cases.ARGONONE, cases.ARGONONEV3, cases.NESPICASEPLUS, cases.SUPERPICASE, cases.MEGAPICASE, cases.RPITOUCHDISPLAY) +manualCases = (cases.SUPERPI4CASE, cases.NESPI4MANUAL, cases.PISTATION, cases.ARGONONE, cases.ARGONONEV3, cases.NESPICASEPLUS, cases.SUPERPICASE, cases.MEGAPICASE, cases.RPITOUCHDISPLAY, cases.PICADE) # Main identification routine def Identify(previousCase): case = cases.NONE -- GitLab