├── .gitmodules ├── stage3 ├── SKIP ├── prerun.sh ├── 01-tweaks │ └── 00-run.sh └── 00-install-packages │ ├── 00-debconf │ ├── 00-packages-nr │ ├── 01-run.sh │ └── 00-packages ├── stage4 ├── SKIP ├── SKIP_IMAGES ├── 03-bookshelf │ ├── files │ │ └── .gitignore │ └── 00-run.sh ├── 05-print-support │ ├── 00-packages │ └── 01-run.sh ├── 00-install-packages │ ├── 00-packages-nr │ ├── 02-packages │ ├── 00-debconf │ ├── 00-packages │ └── 01-packages ├── prerun.sh ├── 04-enable-xcompmgr │ └── 00-run.sh ├── EXPORT_IMAGE ├── EXPORT_NOOBS ├── 01-console-autologin │ └── 00-run.sh └── 02-extras │ └── 00-run.sh ├── stage5 ├── SKIP ├── SKIP_IMAGES ├── prerun.sh ├── 00-install-libreoffice │ └── 00-packages ├── EXPORT_IMAGE ├── EXPORT_NOOBS └── 00-install-extras │ └── 00-packages ├── .gitattributes ├── stage0 ├── 01-locale │ ├── 00-packages │ └── 00-debconf ├── 00-configure-apt │ ├── files │ │ ├── 51cache │ │ ├── sources.list │ │ ├── raspi.list │ │ └── raspberrypi.gpg.key │ └── 00-run.sh ├── 02-firmware │ └── 01-packages ├── files │ └── raspberrypi.gpg └── prerun.sh ├── stage1 ├── 02-net-tweaks │ ├── 00-packages │ └── 00-run.sh ├── 01-sys-tweaks │ ├── 00-patches │ │ ├── series │ │ └── 01-bashrc.diff │ ├── files │ │ ├── noclear.conf │ │ └── fstab │ └── 00-run.sh ├── 03-install-packages │ └── 00-packages ├── prerun.sh └── 00-boot-files │ ├── files │ ├── cmdline.txt │ └── config.txt │ └── 00-run.sh ├── stage2 ├── 01-sys-tweaks │ ├── 00-packages-nr │ ├── files │ │ ├── ttyoutput.conf │ │ ├── 90-qemu.rules │ │ ├── 50raspi │ │ ├── console-setup │ │ ├── rc.local │ │ └── resize2fs_once │ ├── 00-patches │ │ ├── series │ │ ├── 07-resize-init.diff │ │ ├── 04-inputrc.diff │ │ ├── 02-swap.diff │ │ ├── 01-useradd.diff │ │ └── 05-path.diff │ ├── 00-packages │ ├── 01-run.sh │ └── 00-debconf ├── 04-pirate-radio │ ├── files │ │ ├── transistor_first_boot │ │ ├── etc │ │ │ ├── hostname │ │ │ ├── systemd │ │ │ │ ├── network │ │ │ │ │ ├── 08-wifi.network │ │ │ │ │ └── 12-ap.network │ │ │ │ └── system │ │ │ │ │ ├── wpa_supplicant@wlan0.service.d │ │ │ │ │ └── override.conf │ │ │ │ │ ├── bluetooth-discovery.service │ │ │ │ │ ├── bluetooth-agent.service │ │ │ │ │ ├── accesspoint@.service │ │ │ │ │ ├── pulseaudio.service │ │ │ │ │ ├── ympd.service │ │ │ │ │ ├── podcasts-updater.service │ │ │ │ │ ├── radio-browser.service │ │ │ │ │ ├── radio-settings.service │ │ │ │ │ └── radio-interface.service │ │ │ ├── hosts │ │ │ ├── hostapd │ │ │ │ └── hostapd.conf │ │ │ ├── wpa_supplicant │ │ │ │ └── wpa_supplicant-wlan0.conf │ │ │ ├── pulse │ │ │ │ ├── client.conf │ │ │ │ └── default.pa │ │ │ ├── bluetooth │ │ │ │ └── main.conf │ │ │ └── mpd.conf │ │ ├── usr │ │ │ └── local │ │ │ │ ├── lib │ │ │ │ ├── radio-settings │ │ │ │ │ ├── podcasts │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── management │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ └── commands │ │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ │ ├── Pipfile │ │ │ │ │ │ │ │ └── startjobs.py │ │ │ │ │ │ ├── migrations │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ ├── 0002_episode_local_filename.py │ │ │ │ │ │ │ └── 0001_initial.py │ │ │ │ │ │ ├── apps.py │ │ │ │ │ │ ├── static │ │ │ │ │ │ │ ├── imgs │ │ │ │ │ │ │ │ └── pycasts.png │ │ │ │ │ │ │ └── main.css │ │ │ │ │ │ ├── forms.py │ │ │ │ │ │ ├── urls.py │ │ │ │ │ │ ├── admin.py │ │ │ │ │ │ ├── views.py │ │ │ │ │ │ ├── tests.py │ │ │ │ │ │ └── models.py │ │ │ │ │ ├── wifi │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── migrations │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ ├── models.py │ │ │ │ │ │ ├── tests.py │ │ │ │ │ │ ├── admin.py │ │ │ │ │ │ ├── apps.py │ │ │ │ │ │ ├── forms.py │ │ │ │ │ │ ├── urls.py │ │ │ │ │ │ ├── views.py │ │ │ │ │ │ └── helpers.py │ │ │ │ │ ├── content_aggregator │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── asgi.py │ │ │ │ │ │ ├── wsgi.py │ │ │ │ │ │ ├── urls.py │ │ │ │ │ │ └── settings.py │ │ │ │ │ ├── requirements.txt │ │ │ │ │ ├── templates │ │ │ │ │ │ ├── podcasts │ │ │ │ │ │ │ ├── feed_confirm_delete.html │ │ │ │ │ │ │ └── feed_form.html │ │ │ │ │ │ ├── wifi │ │ │ │ │ │ │ └── wifi_settings.html │ │ │ │ │ │ └── homepage.html │ │ │ │ │ ├── Pipfile │ │ │ │ │ ├── manage.py │ │ │ │ │ └── Pipfile.lock │ │ │ │ ├── radio-interface │ │ │ │ │ ├── sleep-timer.wav │ │ │ │ │ ├── interface.py │ │ │ │ │ ├── system.py │ │ │ │ │ ├── helpers.py │ │ │ │ │ ├── display.py │ │ │ │ │ └── audio.py │ │ │ │ └── radio-browser │ │ │ │ │ └── radio-browser.py │ │ │ │ └── sbin │ │ │ │ ├── bt-discovery │ │ │ │ └── bluetooth-agent │ │ │ │ ├── bluezutils.py │ │ │ │ └── simple-agent │ │ └── lib │ │ │ └── systemd │ │ │ └── system-shutdown │ │ │ └── gpio-poweroff │ ├── 03-run.sh │ ├── 00-packages │ ├── 02-run-chroot.sh │ └── 01-run.sh ├── 00-copies-and-fills │ ├── 01-packages │ └── 02-run.sh ├── prerun.sh ├── 02-net-tweaks │ ├── files │ │ ├── wait.conf │ │ └── wpa_supplicant.conf │ ├── 00-packages │ └── 01-run.sh ├── EXPORT_IMAGE ├── EXPORT_NOOBS ├── 03-accept-mathematica-eula │ └── 00-debconf └── 03-set-timezone │ └── 02-run.sh ├── export-image ├── 02-network │ ├── files │ │ └── resolv.conf │ └── 01-run.sh ├── 01-set-sources │ └── 01-run.sh ├── 00-allow-rerun │ └── 00-run.sh ├── 03-set-partuuid │ └── 00-run.sh ├── prerun.sh └── 04-finalise │ └── 01-run.sh ├── .dockerignore ├── Transistor5.stl ├── Legend-Pinout.png ├── Transistor5-cover.stl ├── Transistor_Pinout.png ├── pictures ├── Transistor-3D.jpg ├── Transistor-inside.jpg └── Transistor_interface.png ├── Quick_Setup_Guide+User_Manual.pdf ├── export-noobs ├── 00-release │ ├── files │ │ ├── OS.png │ │ ├── marketing │ │ │ └── slides_vga │ │ │ │ ├── A.png │ │ │ │ ├── B.png │ │ │ │ ├── C.png │ │ │ │ ├── D.png │ │ │ │ ├── E.png │ │ │ │ ├── F.png │ │ │ │ └── G.png │ │ ├── os.json │ │ ├── partitions.json │ │ └── partition_setup.sh │ └── 00-run.sh └── prerun.sh ├── Mise-en-route-rapide+manuel-utilisation.pdf ├── docker-compose.yml ├── .gitignore ├── depends ├── config.example ├── scripts ├── remove-comments.sed ├── dependencies_check ├── common └── qcow2_handling ├── Dockerfile ├── Vagrantfile ├── LICENSE ├── .travis.yml ├── imagetool.sh └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage3/SKIP: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage4/SKIP: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage5/SKIP: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage4/SKIP_IMAGES: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage5/SKIP_IMAGES: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | README.md merge=ours 2 | -------------------------------------------------------------------------------- /stage0/01-locale/00-packages: -------------------------------------------------------------------------------- 1 | locales 2 | -------------------------------------------------------------------------------- /stage1/02-net-tweaks/00-packages: -------------------------------------------------------------------------------- 1 | netbase 2 | -------------------------------------------------------------------------------- /stage4/03-bookshelf/files/.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-packages-nr: -------------------------------------------------------------------------------- 1 | cifs-utils 2 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/transistor_first_boot: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage1/01-sys-tweaks/00-patches/series: -------------------------------------------------------------------------------- 1 | 01-bashrc.diff 2 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/hostname: -------------------------------------------------------------------------------- 1 | transistor 2 | -------------------------------------------------------------------------------- /export-image/02-network/files/resolv.conf: -------------------------------------------------------------------------------- 1 | nameserver 8.8.8.8 2 | -------------------------------------------------------------------------------- /stage2/00-copies-and-fills/01-packages: -------------------------------------------------------------------------------- 1 | raspi-copies-and-fills 2 | -------------------------------------------------------------------------------- /stage4/05-print-support/00-packages: -------------------------------------------------------------------------------- 1 | cups 2 | system-config-printer 3 | -------------------------------------------------------------------------------- /stage1/01-sys-tweaks/files/noclear.conf: -------------------------------------------------------------------------------- 1 | [Service] 2 | TTYVTDisallocate=no 3 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/files/ttyoutput.conf: -------------------------------------------------------------------------------- 1 | [Service] 2 | StandardOutput=tty 3 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/wifi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage0/00-configure-apt/files/51cache: -------------------------------------------------------------------------------- 1 | Acquire::http { Proxy "APT_PROXY"; }; 2 | -------------------------------------------------------------------------------- /stage0/02-firmware/01-packages: -------------------------------------------------------------------------------- 1 | raspberrypi-bootloader 2 | raspberrypi-kernel 3 | -------------------------------------------------------------------------------- /stage4/00-install-packages/00-packages-nr: -------------------------------------------------------------------------------- 1 | pi-package 2 | realvnc-vnc-server 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | output/ 2 | work/ 3 | deploy/ 4 | apt-cacher-ng/ 5 | .git/objects/* 6 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/content_aggregator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/wifi/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage1/03-install-packages/00-packages: -------------------------------------------------------------------------------- 1 | libraspberrypi-bin libraspberrypi0 raspi-config 2 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Transistor5.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pirateradiohack/Transistor_Deprecated/HEAD/Transistor5.stl -------------------------------------------------------------------------------- /stage4/00-install-packages/02-packages: -------------------------------------------------------------------------------- 1 | hunspell-en-gb 2 | hyphen-en-gb 3 | wamerican 4 | wbritish 5 | -------------------------------------------------------------------------------- /Legend-Pinout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pirateradiohack/Transistor_Deprecated/HEAD/Legend-Pinout.png -------------------------------------------------------------------------------- /stage1/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -d "${ROOTFS_DIR}" ]; then 4 | copy_previous 5 | fi 6 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/03-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # cleanup 4 | rm -rf "${ROOTFS_DIR}/root/ympd" 5 | -------------------------------------------------------------------------------- /stage2/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -d "${ROOTFS_DIR}" ]; then 4 | copy_previous 5 | fi 6 | -------------------------------------------------------------------------------- /stage3/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -d "${ROOTFS_DIR}" ]; then 4 | copy_previous 5 | fi 6 | -------------------------------------------------------------------------------- /stage4/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -d "${ROOTFS_DIR}" ]; then 4 | copy_previous 5 | fi 6 | -------------------------------------------------------------------------------- /stage5/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -d "${ROOTFS_DIR}" ]; then 4 | copy_previous 5 | fi 6 | -------------------------------------------------------------------------------- /stage2/02-net-tweaks/files/wait.conf: -------------------------------------------------------------------------------- 1 | [Service] 2 | ExecStart= 3 | ExecStart=/usr/lib/dhcpcd5/dhcpcd -q -w 4 | -------------------------------------------------------------------------------- /Transistor5-cover.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pirateradiohack/Transistor_Deprecated/HEAD/Transistor5-cover.stl -------------------------------------------------------------------------------- /Transistor_Pinout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pirateradiohack/Transistor_Deprecated/HEAD/Transistor_Pinout.png -------------------------------------------------------------------------------- /export-image/02-network/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | install -m 644 files/resolv.conf "${ROOTFS_DIR}/etc/" 4 | -------------------------------------------------------------------------------- /stage5/00-install-libreoffice/00-packages: -------------------------------------------------------------------------------- 1 | libreoffice-pi 2 | libreoffice-help-en-gb 3 | libreoffice-l10n-en-gb 4 | -------------------------------------------------------------------------------- /stage3/01-tweaks/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | rm -f "${ROOTFS_DIR}/etc/systemd/system/dhcpcd.service.d/wait.conf" 4 | -------------------------------------------------------------------------------- /pictures/Transistor-3D.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pirateradiohack/Transistor_Deprecated/HEAD/pictures/Transistor-3D.jpg -------------------------------------------------------------------------------- /stage2/02-net-tweaks/files/wpa_supplicant.conf: -------------------------------------------------------------------------------- 1 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 2 | update_config=1 3 | -------------------------------------------------------------------------------- /stage4/00-install-packages/00-debconf: -------------------------------------------------------------------------------- 1 | # Enable realtime process priority? 2 | jackd2 jackd/tweak_rt_limits boolean true 3 | -------------------------------------------------------------------------------- /stage4/04-enable-xcompmgr/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | on_chroot << EOF 4 | raspi-config nonint do_xcompmgr 0 5 | EOF 6 | -------------------------------------------------------------------------------- /stage4/05-print-support/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | on_chroot < "${ROOTFS_DIR}/etc/timezone" 4 | rm "${ROOTFS_DIR}/etc/localtime" 5 | 6 | on_chroot << EOF 7 | dpkg-reconfigure -f noninteractive tzdata 8 | EOF 9 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/wifi/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WifiConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'wifi' 7 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/network/12-ap.network: -------------------------------------------------------------------------------- 1 | [Match] 2 | Name=ap@* 3 | [Network] 4 | LLMNR=no 5 | MulticastDNS=yes 6 | Address=192.168.179.1/24 7 | DHCPServer=yes 8 | [DHCPServer] 9 | DNS=84.200.69.80 1.1.1.1 10 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PodcastsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.AutoField' 6 | name = 'podcasts' 7 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/wifi/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class WifiSettingsForm(forms.Form): 5 | name = forms.CharField(max_length=255) 6 | password = forms.CharField(max_length=255) 7 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/wifi/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import wifi_settings 4 | 5 | urlpatterns = [ 6 | path("wifi_settings", wifi_settings, name='wifi_settings'), 7 | ] 8 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/static/imgs/pycasts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pirateradiohack/Transistor_Deprecated/HEAD/stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/static/imgs/pycasts.png -------------------------------------------------------------------------------- /stage1/02-net-tweaks/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | echo "${TARGET_HOSTNAME}" > "${ROOTFS_DIR}/etc/hostname" 4 | echo "127.0.1.1 ${TARGET_HOSTNAME}" >> "${ROOTFS_DIR}/etc/hosts" 5 | 6 | ln -sf /dev/null "${ROOTFS_DIR}/etc/systemd/network/99-default.link" 7 | -------------------------------------------------------------------------------- /stage3/00-install-packages/00-packages-nr: -------------------------------------------------------------------------------- 1 | xserver-xorg-video-fbdev xserver-xorg xinit xserver-xorg-video-fbturbo 2 | mousepad 3 | lxde lxtask menu-xdg 4 | zenity xdg-utils 5 | gvfs-backends gvfs-fuse 6 | lightdm gnome-themes-standard-data gnome-icon-theme 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | apt-cacher-ng: 5 | restart: unless-stopped 6 | image: sameersbn/apt-cacher-ng:latest 7 | ports: 8 | - "3142:3142" 9 | volumes: 10 | - ./apt-cacher-ng:/var/cache/apt-cacher-ng 11 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Feed 3 | 4 | 5 | class FeedCreate(forms.ModelForm): 6 | 7 | class Meta: 8 | model = Feed 9 | fields = 'url' 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deploy/* 2 | work/* 3 | config 4 | config.mine 5 | stage2/04-pirate-radio/files/usr/local/lib/radio-browser/.mypy_cache/ 6 | postrun.sh 7 | SKIP 8 | SKIP_IMAGES 9 | .pc 10 | *-pc 11 | apt-cacher-ng/ 12 | my-playlist.m3u 13 | db.sqlite3 14 | .vagrant 15 | -------------------------------------------------------------------------------- /export-image/01-set-sources/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | rm -f "${ROOTFS_DIR}/etc/apt/apt.conf.d/51cache" 4 | find "${ROOTFS_DIR}/var/lib/apt/lists/" -type f -delete 5 | on_chroot << EOF 6 | apt-get update 7 | apt-get -y dist-upgrade 8 | apt-get clean 9 | EOF 10 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/hostapd/hostapd.conf: -------------------------------------------------------------------------------- 1 | interface=wlan 2 | ssid=Transistor 3 | country_code=FR 4 | hw_mode=g 5 | channel=1 6 | auth_algs=1 7 | wpa=2 8 | wpa_passphrase=Transistor 9 | wpa_key_mgmt=WPA-PSK 10 | wpa_pairwise=TKIP 11 | rsn_pairwise=CCMP 12 | -------------------------------------------------------------------------------- /stage0/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -d "${ROOTFS_DIR}" ] || [ "${USE_QCOW2}" = "1" ]; then 4 | bootstrap ${RELEASE} "${ROOTFS_DIR}" http://raspbian.raspberrypi.org/raspbian/ 5 | # bootstrap ${RELEASE} "${ROOTFS_DIR}" http://mirrors.ircam.fr/pub/raspbian/raspbian/ 6 | fi 7 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/management/commands/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | 10 | [requires] 11 | python_version = "3.10" 12 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/templates/podcasts/feed_confirm_delete.html: -------------------------------------------------------------------------------- 1 |
{% csrf_token %} 2 | 3 | 4 | 5 |

Are you sure you want to delete "{{ object }}"?

6 | 7 | 8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /stage3/00-install-packages/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | on_chroot << EOF 4 | update-alternatives --install /usr/bin/x-www-browser \ 5 | x-www-browser /usr/bin/chromium-browser 86 6 | update-alternatives --install /usr/bin/gnome-www-browser \ 7 | gnome-www-browser /usr/bin/chromium-browser 86 8 | EOF 9 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/bluetooth-discovery.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Bluetooth Device Discovery 3 | After=bluetooth.service 4 | PartOf=bluetooth.service 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/local/sbin/bt-discovery 9 | 10 | [Install] 11 | WantedBy=bluetooth.target 12 | -------------------------------------------------------------------------------- /depends: -------------------------------------------------------------------------------- 1 | quilt 2 | parted 3 | realpath:coreutils 4 | qemu-arm-static:qemu-user-static 5 | debootstrap 6 | zerofree 7 | zip 8 | mkdosfs:dosfstools 9 | capsh:libcap2-bin 10 | bsdtar 11 | grep 12 | rsync 13 | xz:xz-utils 14 | curl 15 | xxd 16 | file 17 | git 18 | lsmod:kmod 19 | bc 20 | qemu-nbd:qemu-utils 21 | kpartx 22 | -------------------------------------------------------------------------------- /export-image/00-allow-rerun/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ ! -x "${ROOTFS_DIR}/usr/bin/qemu-arm-static" ]; then 4 | cp /usr/bin/qemu-arm-static "${ROOTFS_DIR}/usr/bin/" 5 | fi 6 | 7 | if [ -e "${ROOTFS_DIR}/etc/ld.so.preload" ]; then 8 | mv "${ROOTFS_DIR}/etc/ld.so.preload" "${ROOTFS_DIR}/etc/ld.so.preload.disabled" 9 | fi 10 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/bluetooth-agent.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Bluetooth Auth Agent 3 | After=bluetooth.service 4 | PartOf=bluetooth.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/local/sbin/bluetooth-agent/simple-agent -c NoInputNoOutput 9 | 10 | [Install] 11 | WantedBy=bluetooth.target 12 | -------------------------------------------------------------------------------- /config.example: -------------------------------------------------------------------------------- 1 | # set the image name 2 | IMG_NAME="Transistor" 3 | 4 | # If build breaks uncomment this 5 | # USE_QCOW2="0" 6 | 7 | # set to 1 to zip the image 8 | DEPLOY_ZIP="1" 9 | 10 | # system user (changing this will break) 11 | FIRST_USER_NAME="transistor" 12 | FIRST_USER_PASS="transistor" 13 | 14 | # set to 1 to enable SSH 15 | ENABLE_SSH="0" 16 | -------------------------------------------------------------------------------- /scripts/remove-comments.sed: -------------------------------------------------------------------------------- 1 | # Deletes comments and collapses whitespace in ##-packages files 2 | 3 | # Append (N)ext line to buffer 4 | # if (!)not ($)buffer is EOF, (b)ranch to (:)label loop 5 | :loop 6 | N 7 | $ !b loop 8 | 9 | # Buffer is "line1\nline2\n...lineN", del comments and collapse whitespace 10 | s/#[^\n]*//g 11 | s/[[:space:]]\{1,\}/ /g 12 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/wpa_supplicant/wpa_supplicant-wlan0.conf: -------------------------------------------------------------------------------- 1 | country=your country goes here (eg: US, GB, DE, FR, ES...) 2 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 3 | update_config=1 4 | 5 | network={ 6 | ssid="your wifi name goes here" 7 | psk="your wifi password goes here" 8 | key_mgmt=WPA-PSK # see ref (4) 9 | } 10 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/templates/podcasts/feed_form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {% csrf_token %} 5 | 6 | 7 | {{ form.as_p }} 8 | 9 | (Podcasts are updated every 30 minutes) 10 |
11 | -------------------------------------------------------------------------------- /stage0/00-configure-apt/files/sources.list: -------------------------------------------------------------------------------- 1 | deb http://raspbian.raspberrypi.org/raspbian/ RELEASE main contrib non-free rpi 2 | # Uncomment line below then 'apt-get update' to enable 'apt-get source' 3 | #deb-src http://raspbian.raspberrypi.org/raspbian/ RELEASE main contrib non-free rpi 4 | #deb http://mirrors.ircam.fr/pub/raspbian/raspbian/ RELEASE main contrib non-free rpi 5 | -------------------------------------------------------------------------------- /stage0/00-configure-apt/files/raspi.list: -------------------------------------------------------------------------------- 1 | deb http://archive.raspberrypi.org/debian/ RELEASE main 2 | # Uncomment line below then 'apt-get update' to enable 'apt-get source' 3 | #deb-src http://archive.raspberrypi.org/debian/ RELEASE main 4 | #deb [trusted=yes] http://archive.raspberrypi.org/debian/ RELEASE main 5 | #deb [trusted=yes] http://mirrors.ircam.fr/pub/debian/ RELEASE main 6 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/files/console-setup: -------------------------------------------------------------------------------- 1 | # CONFIGURATION FILE FOR SETUPCON 2 | 3 | # Consult the console-setup(5) manual page. 4 | 5 | ACTIVE_CONSOLES="/dev/tty[1-6]" 6 | 7 | CHARMAP="UTF-8" 8 | 9 | CODESET="guess" 10 | FONTFACE="" 11 | FONTSIZE="" 12 | 13 | VIDEOMODE= 14 | 15 | # The following is an example how to use a braille font 16 | # FONT='lat9w-08.psf.gz brl-8x8.psf' 17 | -------------------------------------------------------------------------------- /stage5/00-install-extras/00-packages: -------------------------------------------------------------------------------- 1 | mu-editor 2 | sonic-pi 3 | scratch nuscratch scratch3 4 | smartsim 5 | 6 | minecraft-pi python-minecraftpi python-picraft python3-picraft 7 | python-sense-emu sense-emu-tools python-sense-emu-doc 8 | 9 | wolfram-engine 10 | claws-mail 11 | greenfoot-unbundled bluej 12 | nodered 13 | realvnc-vnc-viewer 14 | 15 | python-games 16 | code-the-classics 17 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django-apscheduler = "==0.6.0" 8 | feedparser = "==6.0.8" 9 | python-dateutil = "==2.8.2" 10 | Django = "==3.2.6" 11 | requests = "==2.26.0" 12 | 13 | [dev-packages] 14 | 15 | [requires] 16 | python_version = "3.10" 17 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/accesspoint@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=accesspoint with hostapd (interface-specific version) 3 | 4 | [Service] 5 | ExecStartPre=/sbin/iw dev %i interface add ap@%i type __ap 6 | ExecStart=/usr/sbin/hostapd -i ap@%i /etc/hostapd/hostapd.conf 7 | ExecStopPost=-/sbin/iw dev ap@%i del 8 | 9 | [Install] 10 | WantedBy=sys-subsystem-net-devices-%i.device 11 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-patches/07-resize-init.diff: -------------------------------------------------------------------------------- 1 | --- stage2.orig/rootfs/boot/cmdline.txt 2 | +++ stage2/rootfs/boot/cmdline.txt 3 | @@ -1 +1 @@ 4 | -console=serial0,115200 console=tty1 root=ROOTDEV rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait 5 | +console=serial0,115200 console=tty1 root=ROOTDEV rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet init=/usr/lib/raspi-config/init_resize.sh 6 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/pulseaudio.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=PulseAudio Daemon 3 | After=sound.target 4 | Requires=sound.target 5 | 6 | [Install] 7 | WantedBy=default.target 8 | 9 | [Service] 10 | Restart=always 11 | Type=simple 12 | PrivateTmp=false 13 | ExecStart=/usr/bin/pulseaudio --realtime --disallow-exit --no-cpu-limit --log-target=syslog 14 | ExecStop=/usr/bin/pulseaudio --kill 15 | -------------------------------------------------------------------------------- /stage4/02-extras/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | #Alacarte fixes 4 | install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/.local" 5 | install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/.local/share" 6 | install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/.local/share/applications" 7 | install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/.local/share/desktop-directories" 8 | -------------------------------------------------------------------------------- /stage3/00-install-packages/00-packages: -------------------------------------------------------------------------------- 1 | gstreamer1.0-x gstreamer1.0-omx gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-alsa gstreamer1.0-libav 2 | qpdfview gtk2-engines alsa-utils 3 | desktop-base 4 | git 5 | omxplayer 6 | raspberrypi-artwork 7 | policykit-1 8 | gvfs 9 | rfkill 10 | chromium-browser rpi-chromium-mods 11 | gldriver-test 12 | fonts-droid-fallback 13 | fonts-liberation2 14 | obconf 15 | arandr 16 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-patches/04-inputrc.diff: -------------------------------------------------------------------------------- 1 | Index: jessie-stage2/rootfs/etc/inputrc 2 | =================================================================== 3 | --- jessie-stage2.orig/rootfs/etc/inputrc 4 | +++ jessie-stage2/rootfs/etc/inputrc 5 | @@ -65,3 +65,7 @@ $endif 6 | # "\e[F": end-of-line 7 | 8 | $endif 9 | + 10 | +# mappings for up and down arrows search history 11 | +# "\e[B": history-search-forward 12 | +# "\e[A": history-search-backward 13 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/ympd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ympd server daemon 3 | Requires=network.target local-fs.target 4 | 5 | [Service] 6 | Environment=MPD_HOST=localhost 7 | Environment=MPD_PORT=6600 8 | Environment=WEB_PORT=80 9 | Environment=YMPD_USER=nobody 10 | ExecStart=/usr/bin/ympd --user $YMPD_USER --webport $WEB_PORT --host $MPD_HOST --port $MPD_PORT 11 | Type=simple 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/podcasts-updater.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Update the podcasts content 3 | After=network.target 4 | 5 | [Service] 6 | WorkingDirectory=/usr/local/lib/radio-settings 7 | ExecStart=/usr/bin/python3 manage.py startjobs 8 | # Disable Python's buffering of STDOUT and STDERR, so that output from the 9 | # service shows up immediately in systemd's logs 10 | Environment=PYTHONUNBUFFERED=1 11 | Restart=always 12 | Type=simple 13 | 14 | [Install] 15 | WantedBy=default.target 16 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/files/rc.local: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # 3 | # rc.local 4 | # 5 | # This script is executed at the end of each multiuser runlevel. 6 | # Make sure that the script will "exit 0" on success or any other 7 | # value on error. 8 | # 9 | # In order to enable or disable this script just change the execution 10 | # bits. 11 | # 12 | # By default this script does nothing. 13 | 14 | # Print the IP address 15 | _IP=$(hostname -I) || true 16 | if [ "$_IP" ]; then 17 | printf "My IP address is %s\n" "$_IP" 18 | fi 19 | 20 | exit 0 21 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/radio-browser.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=radio-browser.info service interface 3 | After=network.target 4 | 5 | [Service] 6 | Environment=FLASK_APP=/usr/local/lib/radio-browser/radio-browser 7 | ExecStart=/usr/bin/flask run --host=0.0.0.0 8 | # Disable Python's buffering of STDOUT and STDERR, so that output from the 9 | # service shows up immediately in systemd's logs 10 | Environment=PYTHONUNBUFFERED=1 11 | Restart=always 12 | Type=simple 13 | 14 | [Install] 15 | WantedBy=default.target 16 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import HomePageView, CreatePodcastView, DeletePodcastView, UpdatePodcastView 4 | 5 | urlpatterns = [ 6 | path("new_podcast", CreatePodcastView.as_view(), name="new_podcast"), 7 | path("", HomePageView.as_view(), name="homepage"), 8 | path("/update_podcast", UpdatePodcastView.as_view(), name="update_podcast"), 9 | path("/delete_podcast", DeletePodcastView.as_view(), name="delete_podcast"), 10 | ] 11 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/radio-settings.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=http service to serve radio settings to users 3 | After=network.target 4 | 5 | [Service] 6 | WorkingDirectory=/usr/local/lib/radio-settings 7 | ExecStart=/usr/bin/python3 manage.py runserver 0.0.0.0:8000 8 | # Disable Python's buffering of STDOUT and STDERR, so that output from the 9 | # service shows up immediately in systemd's logs 10 | Environment=PYTHONUNBUFFERED=1 11 | Restart=always 12 | Type=simple 13 | 14 | [Install] 15 | WantedBy=default.target 16 | -------------------------------------------------------------------------------- /stage1/01-sys-tweaks/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | install -d "${ROOTFS_DIR}/etc/systemd/system/getty@tty1.service.d" 4 | install -m 644 files/noclear.conf "${ROOTFS_DIR}/etc/systemd/system/getty@tty1.service.d/noclear.conf" 5 | install -v -m 644 files/fstab "${ROOTFS_DIR}/etc/fstab" 6 | 7 | on_chroot << EOF 8 | if ! id -u ${FIRST_USER_NAME} >/dev/null 2>&1; then 9 | adduser --disabled-password --gecos "" ${FIRST_USER_NAME} 10 | fi 11 | echo "${FIRST_USER_NAME}:${FIRST_USER_PASS}" | chpasswd 12 | echo "root:root" | chpasswd 13 | EOF 14 | 15 | 16 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/content_aggregator/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for content_aggregator project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'content_aggregator.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/content_aggregator/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for content_aggregator project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'content_aggregator.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=debian:buster 2 | FROM ${BASE_IMAGE} 3 | 4 | ENV DEBIAN_FRONTEND noninteractive 5 | 6 | RUN apt-get -y update && \ 7 | apt-get -y install --no-install-recommends \ 8 | git vim parted \ 9 | quilt coreutils qemu-user-static debootstrap zerofree zip dosfstools \ 10 | bsdtar libcap2-bin rsync grep udev xz-utils curl xxd file kmod bc\ 11 | binfmt-support ca-certificates qemu-utils kpartx \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | COPY . /pi-gen/ 15 | 16 | VOLUME [ "/pi-gen/work", "/pi-gen/deploy"] 17 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Feed, Episode 4 | 5 | 6 | @admin.register(Feed) 7 | class FeedAdmin(admin.ModelAdmin): 8 | list_display = ("title", "subscribe_date") 9 | 10 | 11 | @admin.register(Episode) 12 | class EpisodeAdmin(admin.ModelAdmin): 13 | list_display = ("title", "get_feed_title", "pub_date") 14 | 15 | @admin.display(description='Feed Title', ordering='feed__title') 16 | def get_feed_title(self, obj): 17 | return obj.feed.title 18 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/migrations/0002_episode_local_filename.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2022-01-06 14:25 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('podcasts', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='episode', 15 | name='local_filename', 16 | field=models.CharField(blank=True, max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/templates/wifi/wifi_settings.html: -------------------------------------------------------------------------------- 1 |

Enter your wifi name and password here:

2 |

(only 2.4Ghz wifi, 5Ghz is not supported yet) 3 |

4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | Press OK and your Transistor will soon appear on your wifi network (be patient!).
9 | Press and hold the play / pause button and you will see its IP address appear on its screen.
10 | Copy that IP address in your browser to see your Transistor interface. 11 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "debian/contrib-buster64" 6 | 7 | config.vm.synced_folder ".", "/pigen" 8 | 9 | config.vm.provider "virtualbox" do |vb| 10 | vb.memory = "4096" 11 | end 12 | config.vm.provision "shell", inline: <<-SHELL 13 | apt-get update 14 | apt-get install -y coreutils quilt parted qemu-user-static debootstrap zerofree zip \ 15 | dosfstools libarchive-tools libcap2-bin grep rsync xz-utils file git curl bc \ 16 | qemu-utils kpartx gpg pigz 17 | SHELL 18 | end 19 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/00-packages: -------------------------------------------------------------------------------- 1 | raspi-gpio 2 | python-rpi.gpio 3 | python3-rpi.gpio 4 | python3-gpiozero 5 | python-spidev 6 | wiringpi 7 | pulseaudio 8 | pulseaudio-module-bluetooth 9 | bluez-tools 10 | python-dbus 11 | autoconf 12 | automake 13 | libtool 14 | libasound2-dev 15 | python3-alsaaudio 16 | libfftw3-dev 17 | mpd 18 | mpc 19 | ncmpcpp 20 | python3-flask 21 | python3-flask-cors 22 | python3-setuptools 23 | python-pip 24 | python3-pip 25 | python3-systemd 26 | cmake 27 | libmpdclient-dev 28 | libssl-dev 29 | python3-pil 30 | python3-numpy 31 | python3-netifaces 32 | libnss-resolve 33 | hostapd 34 | -------------------------------------------------------------------------------- /export-image/03-set-partuuid/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ "${NO_PRERUN_QCOW2}" = "0" ]; then 4 | 5 | IMG_FILE="${STAGE_WORK_DIR}/${IMG_FILENAME}${IMG_SUFFIX}.img" 6 | 7 | IMGID="$(dd if="${IMG_FILE}" skip=440 bs=1 count=4 2>/dev/null | xxd -e | cut -f 2 -d' ')" 8 | 9 | BOOT_PARTUUID="${IMGID}-01" 10 | ROOT_PARTUUID="${IMGID}-02" 11 | 12 | sed -i "s/BOOTDEV/PARTUUID=${BOOT_PARTUUID}/" "${ROOTFS_DIR}/etc/fstab" 13 | sed -i "s/ROOTDEV/PARTUUID=${ROOT_PARTUUID}/" "${ROOTFS_DIR}/etc/fstab" 14 | 15 | sed -i "s/ROOTDEV/PARTUUID=${ROOT_PARTUUID}/" "${ROOTFS_DIR}/boot/cmdline.txt" 16 | 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/wifi/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from .forms import WifiSettingsForm 3 | from .helpers import set_wifi 4 | 5 | 6 | def wifi_settings(request): 7 | if request.method == 'POST': 8 | form = WifiSettingsForm(request.POST) 9 | if form.is_valid(): 10 | SSID = form.cleaned_data.get('name') 11 | password = form.cleaned_data.get('password') 12 | set_wifi(SSID, password) 13 | else: 14 | form = WifiSettingsForm() 15 | return render(request, "wifi/wifi_settings.html", {'form': form}) 16 | -------------------------------------------------------------------------------- /export-noobs/00-release/files/os.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "NOOBS_DESCRIPTION", 3 | "feature_level": 35120124, 4 | "kernel": "KERNEL", 5 | "name": "NOOBS_NAME", 6 | "password": "raspberry", 7 | "release_date": "UNRELEASED", 8 | "supported_hex_revisions": "2,3,4,5,6,7,8,9,d,e,f,10,11,12,14,19,1040,1041,0092,0093,2082", 9 | "supported_models": [ 10 | "Pi Model", 11 | "Pi 2", 12 | "Pi Zero", 13 | "Pi 3", 14 | "Pi Compute Module 3", 15 | "Pi 4" 16 | ], 17 | "url": "http://www.raspbian.org/", 18 | "username": "pi", 19 | "version": "RELEASE" 20 | } 21 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-patches/02-swap.diff: -------------------------------------------------------------------------------- 1 | Index: jessie-stage2/rootfs/etc/dphys-swapfile 2 | =================================================================== 3 | --- jessie-stage2.orig/rootfs/etc/dphys-swapfile 4 | +++ jessie-stage2/rootfs/etc/dphys-swapfile 5 | @@ -13,7 +13,7 @@ 6 | 7 | # set size to absolute value, leaving empty (default) then uses computed value 8 | # you most likely don't want this, unless you have an special disk situation 9 | -#CONF_SWAPSIZE= 10 | +CONF_SWAPSIZE=100 11 | 12 | # set size to computed value, this times RAM size, dynamically adapts, 13 | # guarantees that there is enough swap without wasting disk space on excess 14 | -------------------------------------------------------------------------------- /stage4/03-bookshelf/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | BOOKSHELF_URL="https://magpi.raspberrypi.org/bookshelf.xml" 4 | GUIDE_URL="$(curl -s "$BOOKSHELF_URL" | awk -F '[<>]' "/Raspberry Pi Beginner's Guide v3<\/TITLE>/ {f=1; next} f==1 && /PDF/ {print \$3; exit}")" 5 | OUTPUT="$(basename "$GUIDE_URL" | cut -f1 -d'?')" 6 | 7 | if [ ! -f "files/$OUTPUT" ]; then 8 | rm files/*.pdf -f 9 | curl -s "$GUIDE_URL" -o "files/$OUTPUT" 10 | fi 11 | 12 | file "files/$OUTPUT" | grep -q "PDF document" 13 | 14 | install -v -o 1000 -g 1000 -d "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/Bookshelf" 15 | install -v -o 1000 -g 1000 -m 644 "files/$OUTPUT" "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/Bookshelf/" 16 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/wifi/helpers.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | 3 | def set_wifi(SSID, password): 4 | """Write wpa_supplicant config file to disk.""" 5 | lines = [ 6 | 'country=FR', 7 | 'ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev', 8 | 'update_config=1', 9 | '', 10 | 'network={', 11 | f' ssid="{SSID}"', 12 | f' psk="{password}"', 13 | ' key_mgmt=WPA-PSK', 14 | '}', 15 | ] 16 | with open('/etc/wpa_supplicant/wpa_supplicant-wlan0.conf', 'w') as f: 17 | f.write('\n'.join(lines)) 18 | 19 | command = """reboot""" 20 | run(command.split()) 21 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/files/resize2fs_once: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: resize2fs_once 4 | # Required-Start: 5 | # Required-Stop: 6 | # Default-Start: 3 7 | # Default-Stop: 8 | # Short-Description: Resize the root filesystem to fill partition 9 | # Description: 10 | ### END INIT INFO 11 | . /lib/lsb/init-functions 12 | case "$1" in 13 | start) 14 | log_daemon_msg "Starting resize2fs_once" 15 | ROOT_DEV=$(findmnt / -o source -n) && 16 | resize2fs $ROOT_DEV && 17 | update-rc.d resize2fs_once remove && 18 | rm /etc/init.d/resize2fs_once && 19 | log_end_msg $? 20 | ;; 21 | *) 22 | echo "Usage: $0 start" >&2 23 | exit 3 24 | ;; 25 | esac 26 | -------------------------------------------------------------------------------- /stage4/00-install-packages/00-packages: -------------------------------------------------------------------------------- 1 | python python3-pygame python-pygame python-tk 2 | python3 python3-tk thonny 3 | python3-pgzero 4 | python-serial python3-serial 5 | python-picamera python3-picamera 6 | debian-reference-en dillo 7 | raspberrypi-net-mods raspberrypi-ui-mods 8 | python-pip python3-pip 9 | python3-numpy 10 | pypy 11 | alacarte rc-gui sense-hat 12 | tree 13 | libgl1-mesa-dri libgles1 libgles2-mesa xcompmgr 14 | geany 15 | piclone 16 | wiringpi pigpio python-pigpio python3-pigpio raspi-gpio python-gpiozero python3-gpiozero python3-rpi.gpio 17 | python-spidev python3-spidev 18 | python-twython python3-twython 19 | python-smbus python3-smbus 20 | python-flask python3-flask 21 | pprompt 22 | piwiz 23 | rp-prefapps 24 | ffmpeg 25 | vlc 26 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-packages: -------------------------------------------------------------------------------- 1 | ssh less fbset sudo psmisc strace ed ncdu crda 2 | console-setup keyboard-configuration debconf-utils parted unzip 3 | build-essential manpages-dev python bash-completion gdb pkg-config 4 | python-rpi.gpio v4l-utils 5 | avahi-daemon 6 | lua5.1 7 | luajit 8 | hardlink ca-certificates curl 9 | fake-hwclock nfs-common usbutils 10 | libraspberrypi-dev libraspberrypi-doc libfreetype6-dev 11 | dosfstools 12 | dphys-swapfile 13 | raspberrypi-sys-mods 14 | pi-bluetooth 15 | apt-listchanges 16 | usb-modeswitch 17 | libpam-chksshpwd 18 | rpi-update 19 | libmtp-runtime 20 | rsync 21 | htop 22 | man-db 23 | policykit-1 24 | ssh-import-id 25 | rng-tools 26 | ethtool 27 | vl805fw 28 | ntfs-3g 29 | pciutils 30 | rpi-eeprom 31 | raspinfo 32 | -------------------------------------------------------------------------------- /stage0/00-configure-apt/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | install -m 644 files/sources.list "${ROOTFS_DIR}/etc/apt/" 4 | install -m 644 files/raspi.list "${ROOTFS_DIR}/etc/apt/sources.list.d/" 5 | sed -i "s/RELEASE/${RELEASE}/g" "${ROOTFS_DIR}/etc/apt/sources.list" 6 | sed -i "s/RELEASE/${RELEASE}/g" "${ROOTFS_DIR}/etc/apt/sources.list.d/raspi.list" 7 | 8 | if [ -n "$APT_PROXY" ]; then 9 | install -m 644 files/51cache "${ROOTFS_DIR}/etc/apt/apt.conf.d/51cache" 10 | sed "${ROOTFS_DIR}/etc/apt/apt.conf.d/51cache" -i -e "s|APT_PROXY|${APT_PROXY}|" 11 | else 12 | rm -f "${ROOTFS_DIR}/etc/apt/apt.conf.d/51cache" 13 | fi 14 | 15 | on_chroot apt-key add - < files/raspberrypi.gpg.key 16 | on_chroot << EOF 17 | apt-get update 18 | apt-get dist-upgrade -y 19 | EOF 20 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/static/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Color Palette: https://coolors.co/e63946-f1faee-a8dadc-457b9d-1d3557 4 | 5 | */ 6 | 7 | :root { 8 | --red: #e63946; 9 | --off-white: #f9f9f9; 10 | --blue: #457b9d; 11 | --dark-blue: #1d3557; 12 | } 13 | 14 | body { 15 | min-height: 100vh; 16 | background-color: var(--off-white); 17 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, 18 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 19 | } 20 | 21 | a { 22 | color: var(--red); 23 | } 24 | 25 | a:hover, 26 | a:focus { 27 | color: var(--blue); 28 | text-decoration: none; 29 | } 30 | 31 | #site-logo { 32 | max-height: 200px; 33 | } 34 | -------------------------------------------------------------------------------- /export-noobs/00-release/files/partitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "partitions": [ 3 | { 4 | "filesystem_type": "FAT", 5 | "label": "boot", 6 | "mkfs_options": "-F 32", 7 | "partition_size_nominal": BOOT_NOM, 8 | "uncompressed_tarball_size": BOOT_SIZE, 9 | "want_maximised": false, 10 | "sha256sum": "BOOT_SHASUM" 11 | }, 12 | { 13 | "filesystem_type": "ext4", 14 | "label": "root", 15 | "mkfs_options": "-O ^huge_file", 16 | "partition_size_nominal": ROOT_NOM, 17 | "uncompressed_tarball_size": ROOT_SIZE, 18 | "want_maximised": true, 19 | "sha256sum": "ROOT_SHASUM" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'content_aggregator.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /stage4/00-install-packages/01-packages: -------------------------------------------------------------------------------- 1 | python-automationhat python3-automationhat 2 | python-blinkt python3-blinkt 3 | python-cap1xxx python3-cap1xxx 4 | python-drumhat python3-drumhat 5 | python-envirophat python3-envirophat 6 | python-explorerhat python3-explorerhat 7 | python-fourletterphat python3-fourletterphat 8 | python-microdotphat python3-microdotphat 9 | python-mote python3-mote 10 | python-motephat python3-motephat 11 | python-phatbeat python3-phatbeat 12 | python-pianohat python3-pianohat 13 | python-piglow python3-piglow 14 | python-rainbowhat python3-rainbowhat 15 | python-scrollphat python3-scrollphat 16 | python-scrollphathd python3-scrollphathd 17 | python-sn3218 python3-sn3218 18 | python-skywriter python3-skywriter 19 | python-touchphat python3-touchphat 20 | python-buttonshim python3-buttonshim 21 | python-unicornhathd python3-unicornhathd 22 | python-pantilthat python3-pantilthat 23 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/systemd/system/radio-interface.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | 3 | # Human readable name of the unit 4 | Description=controller for the radio buttons and LEDs 5 | 6 | 7 | [Service] 8 | 9 | # Command to execute when the service is started 10 | ExecStart=/usr/bin/python3 /usr/local/lib/radio-interface/interface.py 11 | 12 | # Disable Python's buffering of STDOUT and STDERR, so that output from the 13 | # service shows up immediately in systemd's logs 14 | Environment=PYTHONUNBUFFERED=1 15 | 16 | # Automatically restart the service if it crashes 17 | Restart=on-failure 18 | 19 | # Our service will notify systemd once it is up and running 20 | Type=notify 21 | 22 | # Use a dedicated user to run our service 23 | # User=radio-interface 24 | 25 | 26 | [Install] 27 | 28 | # Tell systemd to automatically start this service when the system boots 29 | # (assuming the service is enabled) 30 | WantedBy=default.target 31 | 32 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-patches/01-useradd.diff: -------------------------------------------------------------------------------- 1 | Index: jessie-stage2/rootfs/etc/default/useradd 2 | =================================================================== 3 | --- jessie-stage2.orig/rootfs/etc/default/useradd 4 | +++ jessie-stage2/rootfs/etc/default/useradd 5 | @@ -5,7 +5,7 @@ 6 | # Similar to DHSELL in adduser. However, we use "sh" here because 7 | # useradd is a low level utility and should be as general 8 | # as possible 9 | -SHELL=/bin/sh 10 | +SHELL=/bin/bash 11 | # 12 | # The default group for users 13 | # 100=users on Debian systems 14 | @@ -29,7 +29,7 @@ SHELL=/bin/sh 15 | # The SKEL variable specifies the directory containing "skeletal" user 16 | # files; in other words, files such as a sample .profile that will be 17 | # copied to the new user's home directory when it is created. 18 | -# SKEL=/etc/skel 19 | +SKEL=/etc/skel 20 | # 21 | # Defines whether the mail spool should be created while 22 | # creating the account 23 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-browser/radio-browser.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from flask_cors import CORS, cross_origin 3 | from mpd import MPDClient 4 | from pyradios import RadioBrowser 5 | 6 | app = Flask(__name__) 7 | cors = CORS(app) 8 | app.config['CORS_HEADERS'] = 'Content-Type' 9 | 10 | rb = RadioBrowser() 11 | 12 | 13 | @app.route("/add_radio") 14 | @cross_origin() 15 | def add_stream(): 16 | stream_name = request.args.get("name") 17 | stream_details = rb.search(name=stream_name) 18 | try: 19 | stream_uri = stream_details[0].get("url") 20 | mpd_client = MPDClient() 21 | mpd_client.timeout = 10 22 | mpd_client.idletimeout = None 23 | mpd_client.connect("localhost", 6600) 24 | mpd_client.add(stream_uri) 25 | mpd_client.close() 26 | mpd_client.disconnect() 27 | return "" 28 | except IndexError: 29 | return "No stream found" 30 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/content_aggregator/urls.py: -------------------------------------------------------------------------------- 1 | """content_aggregator URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | urlpatterns = [ 20 | path("admin/", admin.site.urls), 21 | path("", include("podcasts.urls")), 22 | path("", include("wifi.urls")), 23 | ] 24 | -------------------------------------------------------------------------------- /scripts/dependencies_check: -------------------------------------------------------------------------------- 1 | # dependencies_check 2 | # $@ Dependency files to check 3 | # 4 | # Each dependency is in the form of a tool to test for, optionally followed by 5 | # a : and the name of a package if the package on a Debian-ish system is not 6 | # named for the tool (i.e., qemu-user-static). 7 | dependencies_check() 8 | { 9 | local depfile deps missing 10 | 11 | for depfile in "$@"; do 12 | if [[ -e "$depfile" ]]; then 13 | deps="$(sed -f "${SCRIPT_DIR}/remove-comments.sed" < "${BASE_DIR}/depends")" 14 | 15 | fi 16 | for dep in $deps; do 17 | if ! hash "${dep%:*}" 2>/dev/null; then 18 | missing="${missing:+$missing }${dep#*:}" 19 | fi 20 | done 21 | done 22 | 23 | if [[ "$missing" ]]; then 24 | echo "Required dependencies not installed" 25 | echo 26 | echo "This can be resolved on Debian/Raspbian systems by installing:" 27 | echo "$missing" 28 | false 29 | fi 30 | 31 | 32 | if ! grep -q "/proc/sys/fs/binfmt_misc" /proc/mounts; then 33 | echo "Module binfmt_misc not loaded in host" 34 | echo "Please run:" 35 | echo " sudo modprobe binfmt_misc" 36 | exit 1 37 | fi 38 | } 39 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-patches/05-path.diff: -------------------------------------------------------------------------------- 1 | Index: jessie-stage2/rootfs/etc/login.defs 2 | =================================================================== 3 | --- jessie-stage2.orig/rootfs/etc/login.defs 4 | +++ jessie-stage2/rootfs/etc/login.defs 5 | @@ -100,7 +100,7 @@ HUSHLOGIN_FILE .hushlogin 6 | # 7 | # (they are minimal, add the rest in the shell startup files) 8 | ENV_SUPATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 9 | -ENV_PATH PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games 10 | +ENV_PATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games 11 | 12 | # 13 | # Terminal permissions 14 | Index: jessie-stage2/rootfs/etc/profile 15 | =================================================================== 16 | --- jessie-stage2.orig/rootfs/etc/profile 17 | +++ jessie-stage2/rootfs/etc/profile 18 | @@ -4,7 +4,7 @@ 19 | if [ "`id -u`" -eq 0 ]; then 20 | PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 21 | else 22 | - PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games" 23 | + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games" 24 | fi 25 | export PATH 26 | 27 | -------------------------------------------------------------------------------- /export-noobs/00-release/files/partition_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #supports_backup in PINN 3 | 4 | set -ex 5 | 6 | # shellcheck disable=SC2154 7 | if [ -z "$part1" ] || [ -z "$part2" ]; then 8 | printf "Error: missing environment variable part1 or part2\n" 1>&2 9 | exit 1 10 | fi 11 | 12 | mkdir -p /tmp/1 /tmp/2 13 | 14 | mount "$part1" /tmp/1 15 | mount "$part2" /tmp/2 16 | 17 | sed /tmp/1/cmdline.txt -i -e "s|root=[^ ]*|root=${part2}|" 18 | sed /tmp/2/etc/fstab -i -e "s|^[^#].* / |${part2} / |" 19 | sed /tmp/2/etc/fstab -i -e "s|^[^#].* /boot |${part1} /boot |" 20 | 21 | # shellcheck disable=SC2154 22 | if [ -z "$restore" ]; then 23 | if [ -f /mnt/ssh ]; then 24 | cp /mnt/ssh /tmp/1/ 25 | fi 26 | 27 | if [ -f /mnt/ssh.txt ]; then 28 | cp /mnt/ssh.txt /tmp/1/ 29 | fi 30 | 31 | if [ -f /settings/wpa_supplicant.conf ]; then 32 | cp /settings/wpa_supplicant.conf /tmp/1/ 33 | fi 34 | 35 | if ! grep -q resize /proc/cmdline; then 36 | if ! grep -q splash /tmp/1/cmdline.txt; then 37 | sed -i "s| quiet||g" /tmp/1/cmdline.txt 38 | fi 39 | sed -i 's| init=/usr/lib/raspi-config/init_resize.sh||' /tmp/1/cmdline.txt 40 | else 41 | sed -i '1 s|.*|& sdhci.debug_quirks2=4|' /tmp/1/cmdline.txt 42 | fi 43 | fi 44 | 45 | umount /tmp/1 46 | umount /tmp/2 47 | -------------------------------------------------------------------------------- /stage1/01-sys-tweaks/00-patches/01-bashrc.diff: -------------------------------------------------------------------------------- 1 | --- a/rootfs/etc/skel/.bashrc 2 | +++ b/rootfs/etc/skel/.bashrc 3 | @@ -43,7 +43,7 @@ 4 | # uncomment for a colored prompt, if the terminal has the capability; turned 5 | # off by default to not distract the user: the focus in a terminal window 6 | # should be on the output of commands, not on the prompt 7 | -#force_color_prompt=yes 8 | +force_color_prompt=yes 9 | 10 | if [ -n "$force_color_prompt" ]; then 11 | if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then 12 | @@ -57,7 +57,7 @@ 13 | fi 14 | 15 | if [ "$color_prompt" = yes ]; then 16 | - PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' 17 | + PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w \$\[\033[00m\] ' 18 | else 19 | PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' 20 | fi 21 | @@ -79,9 +79,9 @@ 22 | #alias dir='dir --color=auto' 23 | #alias vdir='vdir --color=auto' 24 | 25 | - #alias grep='grep --color=auto' 26 | - #alias fgrep='fgrep --color=auto' 27 | - #alias egrep='egrep --color=auto' 28 | + alias grep='grep --color=auto' 29 | + alias fgrep='fgrep --color=auto' 30 | + alias egrep='egrep --color=auto' 31 | fi 32 | 33 | # colored GCC warnings and errors 34 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.shortcuts import render 3 | from django.views.generic import ListView 4 | from django.views.generic.edit import CreateView, DeleteView, UpdateView 5 | from django.urls import reverse_lazy 6 | 7 | from .models import Feed 8 | 9 | 10 | class CreatePodcastView(CreateView): 11 | model = Feed 12 | fields = ['url', 'max_entries'] 13 | success_url = reverse_lazy("homepage") 14 | 15 | 16 | class HomePageView(ListView): 17 | template_name = "homepage.html" 18 | model = Feed 19 | 20 | def get_context_data(self, **kwargs): 21 | context = super().get_context_data(**kwargs) 22 | context["feeds"] = Feed.objects.filter().order_by("-subscribe_date") 23 | statvfs = os.statvfs('/') 24 | free = statvfs.f_frsize * statvfs.f_bavail 25 | total = statvfs.f_frsize * statvfs.f_blocks 26 | free_space = free / total * 100 27 | free_space = float("{:.2f}".format(free_space)) 28 | context["free_space"] = free_space 29 | return context 30 | 31 | 32 | class UpdatePodcastView(UpdateView): 33 | model = Feed 34 | fields = ['max_entries'] 35 | success_url = reverse_lazy("homepage") 36 | 37 | 38 | class DeletePodcastView(DeleteView): 39 | model = Feed 40 | success_url = reverse_lazy("homepage") 41 | -------------------------------------------------------------------------------- /stage2/02-net-tweaks/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | install -v -d "${ROOTFS_DIR}/etc/systemd/system/dhcpcd.service.d" 4 | install -v -m 644 files/wait.conf "${ROOTFS_DIR}/etc/systemd/system/dhcpcd.service.d/" 5 | 6 | install -v -d "${ROOTFS_DIR}/etc/wpa_supplicant" 7 | install -v -m 600 files/wpa_supplicant.conf "${ROOTFS_DIR}/etc/wpa_supplicant/" 8 | 9 | if [ -v WPA_COUNTRY ]; then 10 | echo "country=${WPA_COUNTRY}" >> "${ROOTFS_DIR}/etc/wpa_supplicant/wpa_supplicant.conf" 11 | fi 12 | 13 | if [ -v WPA_ESSID ] && [ -v WPA_PASSWORD ]; then 14 | on_chroot <<EOF 15 | set -o pipefail 16 | wpa_passphrase "${WPA_ESSID}" "${WPA_PASSWORD}" | tee -a "/etc/wpa_supplicant/wpa_supplicant.conf" 17 | EOF 18 | elif [ -v WPA_ESSID ]; then 19 | cat >> "${ROOTFS_DIR}/etc/wpa_supplicant/wpa_supplicant.conf" << EOL 20 | 21 | network={ 22 | ssid="${WPA_ESSID}" 23 | key_mgmt=NONE 24 | } 25 | EOL 26 | fi 27 | 28 | # Disable wifi on 5GHz models if WPA_COUNTRY is not set 29 | mkdir -p "${ROOTFS_DIR}/var/lib/systemd/rfkill/" 30 | if [ -n "$WPA_COUNTRY" ]; then 31 | echo 0 > "${ROOTFS_DIR}/var/lib/systemd/rfkill/platform-3f300000.mmcnr:wlan" 32 | echo 0 > "${ROOTFS_DIR}/var/lib/systemd/rfkill/platform-fe300000.mmcnr:wlan" 33 | else 34 | echo 1 > "${ROOTFS_DIR}/var/lib/systemd/rfkill/platform-3f300000.mmcnr:wlan" 35 | echo 1 > "${ROOTFS_DIR}/var/lib/systemd/rfkill/platform-fe300000.mmcnr:wlan" 36 | fi 37 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/pulse/client.conf: -------------------------------------------------------------------------------- 1 | # This file is part of PulseAudio. 2 | # 3 | # PulseAudio is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # PulseAudio is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public License 14 | # along with PulseAudio; if not, see <http://www.gnu.org/licenses/>. 15 | 16 | ## Configuration file for PulseAudio clients. See pulse-client.conf(5) for 17 | ## more information. Default values are commented out. Use either ; or # for 18 | ## commenting. 19 | 20 | ; default-sink = 21 | ; default-source = 22 | ; default-server = 23 | ; default-dbus-server = 24 | 25 | ; autospawn = yes 26 | ; daemon-binary = /usr/bin/pulseaudio 27 | ; extra-arguments = --log-target=syslog 28 | 29 | ; cookie-file = 30 | 31 | ; enable-shm = yes 32 | ; shm-size-bytes = 0 # setting this 0 will use the system-default, usually 64 MiB 33 | 34 | ; auto-connect-localhost = no 35 | ; auto-connect-display = no 36 | autospawn = no 37 | default-server = unix:/tmp/pulseaudio.socket 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Raspberry Pi (Trading) Ltd. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/lib/systemd/system-shutdown/gpio-poweroff: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # file: /lib/systemd/system-shutdown/gpio-poweroff 4 | # $1 will be either "halt", "poweroff", "reboot" or "kexec" 5 | 6 | poweroff_pin="4" 7 | led_pin="" 8 | config_file=/etc/cleanshutd.conf 9 | 10 | if [ -f "$config_file" ]; then 11 | /bin/echo "Reading config file $config_file" 12 | poweroff_pin=`grep -r '^poweroff_pin=[0-9]*$' "$config_file" | cut -f2- -d=` 13 | led_pin=`grep -r '^led_pin=[0-9]*$' "$config_file" | cut -f2- -d=` 14 | fi 15 | 16 | case "$1" in 17 | poweroff) 18 | if [ "$poweroff_pin" = "" ]; then 19 | /bin/echo "Skipping GPIO power-off" && exit 0 20 | else 21 | /bin/echo "Using power off pin $poweroff_pin" 22 | fi 23 | if [ ! "$led_pin" = "" ]; then 24 | /bin/echo "Using LED pin $led_pin" 25 | /bin/echo $led_pin > /sys/class/gpio/export 26 | /bin/echo out > /sys/class/gpio/gpio$led_pin/direction 27 | for iteration in 1 2 3; do 28 | /bin/echo 0 > /sys/class/gpio/gpio$led_pin/value 29 | /bin/sleep 0.2 30 | /bin/echo 1 > /sys/class/gpio/gpio$led_pin/value 31 | /bin/sleep 0.2 32 | done 33 | fi 34 | /bin/echo $poweroff_pin > /sys/class/gpio/export 35 | /bin/echo out > /sys/class/gpio/gpio$poweroff_pin/direction 36 | /bin/echo 0 > /sys/class/gpio/gpio$poweroff_pin/value 37 | /bin/sleep 0.5 38 | ;; 39 | esac 40 | : 41 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls.base import reverse 3 | from django.utils import timezone 4 | 5 | from .models import Episode 6 | 7 | 8 | class PodCastsTests(TestCase): 9 | def setUp(self): 10 | self.episode = Episode.objects.create( 11 | title="My Awesome Podcast Episode", 12 | description="Look mom, I made it!", 13 | pub_date=timezone.now(), 14 | link="https://myawesomeshow.com", 15 | image="https://image.myawesomeshow.com", 16 | podcast_name="My Python Podcast", 17 | guid="de194720-7b4c-49e2-a05f-432436d3fetr", 18 | ) 19 | 20 | def test_episode_content(self): 21 | self.assertEqual(self.episode.description, "Look mom, I made it!") 22 | self.assertEqual(self.episode.link, "https://myawesomeshow.com") 23 | self.assertEqual(self.episode.guid, "de194720-7b4c-49e2-a05f-432436d3fetr") 24 | 25 | def test_episode_str_representation(self): 26 | self.assertEqual( 27 | str(self.episode), "My Python Podcast: My Awesome Podcast Episode" 28 | ) 29 | 30 | def test_home_page_status_code(self): 31 | response = self.client.get("/") 32 | self.assertEqual(response.status_code, 200) 33 | 34 | def test_home_page_uses_correct_template(self): 35 | response = self.client.get(reverse("homepage")) 36 | self.assertTemplateUsed(response, "homepage.html") 37 | 38 | def test_homepage_list_contents(self): 39 | response = self.client.get(reverse("homepage")) 40 | self.assertContains(response, "My Awesome Podcast Episode") 41 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-interface/interface.py: -------------------------------------------------------------------------------- 1 | """This module is a systemd service that takes care of interfacing the 2 | physical interaction devices (buttons, LEDs...) and the controls of the 3 | radio. 4 | """ 5 | import asyncio 6 | 7 | import systemd.daemon 8 | from gpiozero import Button 9 | 10 | from audio import Audio 11 | from display import Display 12 | from system import System 13 | 14 | audio = Audio() 15 | display = Display() 16 | system = System() 17 | 18 | # Set the audio volume to a reasonable amount for the first boot 19 | if system.first_boot(): 20 | audio.set_volume(35) 21 | 22 | # Make sure the client is playing and in repeat mode on startup 23 | audio.play() 24 | audio.repeat(True) 25 | 26 | # all initialization is considered done after this point and we tell 27 | # systemd that we are ready to serve 28 | systemd.daemon.notify("READY=1") 29 | 30 | # Assign the buttons to their corresponding GPIOs 31 | fast_forward_button = Button(26) 32 | rewind_button = Button(5) 33 | play_pause_button = Button(6) 34 | volume_up_button = Button(16) 35 | volume_down_button = Button(24) 36 | on_off_button = Button(17) 37 | 38 | # Define what actions to set for each button event 39 | fast_forward_button.when_pressed = audio.next 40 | rewind_button.when_pressed = audio.previous 41 | play_pause_button.when_released = audio.play_pause 42 | play_pause_button.when_held = display.ip_address 43 | volume_up_button.when_pressed = audio.volume_up 44 | volume_down_button.when_pressed = audio.volume_down 45 | on_off_button.when_released = system.shutdown 46 | on_off_button.when_held = system.sleep_timer 47 | 48 | 49 | async def main(): 50 | await asyncio.gather( 51 | audio.volume_knob(), 52 | display.current_stream(), 53 | display.screen(), 54 | ) 55 | 56 | 57 | asyncio.run(main()) 58 | -------------------------------------------------------------------------------- /stage0/00-configure-apt/files/raspberrypi.gpg.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.4.12 (GNU/Linux) 3 | 4 | mQENBE/d7o8BCACrwqQacGJfn3tnMzGui6mv2lLxYbsOuy/+U4rqMmGEuo3h9m92 5 | 30E2EtypsoWczkBretzLUCFv+VUOxaA6sV9+puTqYGhhQZFuKUWcG7orf7QbZRuu 6 | TxsEUepW5lg7MExmAu1JJzqM0kMQX8fVyWVDkjchZ/is4q3BPOUCJbUJOsE+kK/6 7 | 8kW6nWdhwSAjfDh06bA5wvoXNjYoDdnSZyVdcYCPEJXEg5jfF/+nmiFKMZBraHwn 8 | eQsepr7rBXxNcEvDlSOPal11fg90KXpy7Umre1UcAZYJdQeWcHu7X5uoJx/MG5J8 9 | ic6CwYmDaShIFa92f8qmFcna05+lppk76fsnABEBAAG0IFJhc3BiZXJyeSBQaSBB 10 | cmNoaXZlIFNpZ25pbmcgS2V5iQE4BBMBAgAiBQJP3e6PAhsDBgsJCAcDAgYVCAIJ 11 | CgsEFgIDAQIeAQIXgAAKCRCCsSmSf6MwPk6vB/9pePB3IukU9WC9Bammh3mpQTvL 12 | OifbkzHkmAYxzjfK6D2I8pT0xMxy949+ThzJ7uL60p6T/32ED9DR3LHIMXZvKtuc 13 | mQnSiNDX03E2p7lIP/htoxW2hDP2n8cdlNdt0M9IjaWBppsbO7IrDppG2B1aRLni 14 | uD7v8bHRL2mKTtIDLX42Enl8aLAkJYgNWpZyPkDyOqamjijarIWjGEPCkaURF7g4 15 | d44HvYhpbLMOrz1m6N5Bzoa5+nq3lmifeiWKxioFXU+Hy5bhtAM6ljVb59hbD2ra 16 | X4+3LXC9oox2flmQnyqwoyfZqVgSQa0B41qEQo8t1bz6Q1Ti7fbMLThmbRHiuQEN 17 | BE/d7o8BCADNlVtBZU63fm79SjHh5AEKFs0C3kwa0mOhp9oas/haDggmhiXdzeD3 18 | 49JWz9ZTx+vlTq0s+I+nIR1a+q+GL+hxYt4HhxoA6vlDMegVfvZKzqTX9Nr2VqQa 19 | S4Kz3W5ULv81tw3WowK6i0L7pqDmvDqgm73mMbbxfHD0SyTt8+fk7qX6Ag2pZ4a9 20 | ZdJGxvASkh0McGpbYJhk1WYD+eh4fqH3IaeJi6xtNoRdc5YXuzILnp+KaJyPE5CR 21 | qUY5JibOD3qR7zDjP0ueP93jLqmoKltCdN5+yYEExtSwz5lXniiYOJp8LWFCgv5h 22 | m8aYXkcJS1xVV9Ltno23YvX5edw9QY4hABEBAAGJAR8EGAECAAkFAk/d7o8CGwwA 23 | CgkQgrEpkn+jMD5Figf/dIC1qtDMTbu5IsI5uZPX63xydaExQNYf98cq5H2fWF6O 24 | yVR7ERzA2w33hI0yZQrqO6pU9SRnHRxCFvGv6y+mXXXMRcmjZG7GiD6tQWeN/3wb 25 | EbAn5cg6CJ/Lk/BI4iRRfBX07LbYULCohlGkwBOkRo10T+Ld4vCCnBftCh5x2OtZ 26 | TOWRULxP36y2PLGVNF+q9pho98qx+RIxvpofQM/842ZycjPJvzgVQsW4LT91KYAE 27 | 4TVf6JjwUM6HZDoiNcX6d7zOhNfQihXTsniZZ6rky287htsWVDNkqOi5T3oTxWUo 28 | m++/7s3K3L0zWopdhMVcgg6Nt9gcjzqN1c0gy55L/g== 29 | =mNSj 30 | -----END PGP PUBLIC KEY BLOCK----- 31 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2022-01-04 12:00 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Feed', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(blank=True, max_length=200)), 20 | ('slug', models.SlugField(max_length=255, unique=True)), 21 | ('description', models.TextField(blank=True)), 22 | ('subscribe_date', models.DateTimeField(auto_now_add=True)), 23 | ('url', models.URLField()), 24 | ('link', models.URLField(blank=True)), 25 | ('max_entries', models.PositiveSmallIntegerField(default=3, verbose_name='Number of episodes to keep')), 26 | ('image', models.URLField(blank=True)), 27 | ('fs_path', models.CharField(max_length=255)), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name='Episode', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('title', models.CharField(max_length=200)), 35 | ('pub_date', models.DateTimeField()), 36 | ('link', models.URLField()), 37 | ('audio', models.URLField(blank=True)), 38 | ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='podcasts.feed')), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-interface/system.py: -------------------------------------------------------------------------------- 1 | """ Controller for system actions.""" 2 | import os 3 | from subprocess import run 4 | 5 | from audio import Audio 6 | from helpers import notify 7 | 8 | audio = Audio() 9 | 10 | 11 | class System: 12 | """Implements the system actions.""" 13 | 14 | def __init__(self) -> None: 15 | self.power_button_was_held: bool = False 16 | 17 | @notify 18 | def sleep_timer(self) -> None: 19 | """Shutdown button tells the system to shutdown 20 minutes from now.""" 20 | # use the trick described here: 21 | # https://gpiozero.readthedocs.io/en/stable/faq.html 22 | # #how-do-i-use-button-when-pressed-and-button-when-held-together 23 | self.power_button_was_held = True 24 | command = """shutdown 25 | -h +20 26 | """ 27 | run(command.split()) 28 | command = """wall 29 | -n Sleep timer was triggered. System is shutting down in 20 30 | minutes. 31 | """ 32 | run(command.split()) 33 | 34 | def shutdown(self, button) -> None: 35 | """Shutdown button tells the system to shutdown now.""" 36 | if not self.power_button_was_held: 37 | audio.stop() 38 | command = """shutdown 39 | -h now 40 | """ 41 | run(command.split()) 42 | command = """wall 43 | -n Power off was triggered by user. 44 | """ 45 | run(command.split()) 46 | self.power_button_was_held = False 47 | 48 | def first_boot(self) -> bool: 49 | """Return True if the system is booted for the first time. 50 | False otherwise. 51 | """ 52 | if os.path.exists("/transistor_first_boot"): 53 | os.remove("/transistor_first_boot") 54 | return True 55 | return False 56 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/sbin/bluetooth-agent/bluezutils.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | 3 | SERVICE_NAME = "org.bluez" 4 | ADAPTER_INTERFACE = SERVICE_NAME + ".Adapter1" 5 | DEVICE_INTERFACE = SERVICE_NAME + ".Device1" 6 | 7 | 8 | def get_managed_objects(): 9 | bus = dbus.SystemBus() 10 | manager = dbus.Interface(bus.get_object("org.bluez", "/"), 11 | "org.freedesktop.DBus.ObjectManager") 12 | return manager.GetManagedObjects() 13 | 14 | 15 | def find_adapter(pattern=None): 16 | return find_adapter_in_objects(get_managed_objects(), pattern) 17 | 18 | 19 | def find_adapter_in_objects(objects, pattern=None): 20 | bus = dbus.SystemBus() 21 | for path, ifaces in objects.iteritems(): 22 | adapter = ifaces.get(ADAPTER_INTERFACE) 23 | if adapter is None: 24 | continue 25 | if not pattern or pattern == adapter["Address"] or \ 26 | path.endswith(pattern): 27 | obj = bus.get_object(SERVICE_NAME, path) 28 | return dbus.Interface(obj, ADAPTER_INTERFACE) 29 | raise Exception("Bluetooth adapter not found") 30 | 31 | 32 | def find_device(device_address, adapter_pattern=None): 33 | return find_device_in_objects(get_managed_objects(), device_address, 34 | adapter_pattern) 35 | 36 | 37 | def find_device_in_objects(objects, device_address, adapter_pattern=None): 38 | bus = dbus.SystemBus() 39 | path_prefix = "" 40 | if adapter_pattern: 41 | adapter = find_adapter_in_objects(objects, adapter_pattern) 42 | path_prefix = adapter.object_path 43 | for path, ifaces in objects.iteritems(): 44 | device = ifaces.get(DEVICE_INTERFACE) 45 | if device is None: 46 | continue 47 | if (device["Address"] == device_address and 48 | path.startswith(path_prefix)): 49 | obj = bus.get_object(SERVICE_NAME, path) 50 | return dbus.Interface(obj, DEVICE_INTERFACE) 51 | 52 | raise Exception("Bluetooth device not found") 53 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/02-run-chroot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # pulseaudio 4 | systemctl enable pulseaudio 5 | 6 | # pulseaudio for python 7 | pip3 install --upgrade pip setuptools 8 | pip3 install pulsectl 9 | 10 | # mpd 11 | mkdir -p /home/transistor/podcasts 12 | chown -R transistor:transistor /home/transistor/podcasts 13 | pip3 install python-mpd2 14 | 15 | # physical interface 16 | useradd -r -s /bin/false radio-interface 17 | adduser radio-interface gpio 18 | echo 'radio-interface ALL=(ALL) NOPASSWD: /sbin/shutdown' > /etc/sudoers.d/010_radio-interface-shutdown 19 | chmod 0440 /etc/sudoers.d/010_radio-interface-shutdown 20 | systemctl enable radio-interface 21 | 22 | # sound in python 23 | pip3 install simpleaudio 24 | 25 | # web interface 26 | cd /root/ympd 27 | mkdir build 28 | cd build 29 | cmake .. -DCMAKE_INSTALL_PREFIX:PATH=/usr 30 | make 31 | make install 32 | systemctl enable ympd 33 | 34 | # pirate audio screen 35 | pip3 install st7789 36 | 37 | # Radio browser 38 | pip3 install pyradios 39 | systemctl enable radio-browser 40 | 41 | # Podcasts 42 | pip3 install -r /usr/local/lib/radio-settings/requirements.txt 43 | systemctl enable podcasts-updater 44 | 45 | # Radio settings interface 46 | cd /usr/local/lib/radio-settings 47 | python3 manage.py makemigrations && python3 manage.py migrate 48 | systemctl enable radio-settings 49 | 50 | # bluetooth 51 | systemctl enable bluetooth-agent 52 | systemctl enable bluetooth-discovery 53 | 54 | # wifi 55 | systemctl disable dhcpcd rsyslog avahi-daemon 56 | apt --autoremove -y purge ifupdown dhcpcd5 isc-dhcp-client isc-dhcp-common rsyslog avahi-daemon 57 | rm -r /etc/network /etc/dhcp 58 | ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf 59 | apt-mark hold avahi-daemon dhcpcd dhcpcd5 ifupdown isc-dhcp-client isc-dhcp-common libnss-mdns openresolv raspberrypi-net-mods rsyslog 60 | systemctl enable systemd-networkd.service systemd-resolved.service 61 | systemctl enable accesspoint@wlan0.service 62 | rfkill unblock wlan 63 | systemctl disable wpa_supplicant.service 64 | rm -rf /etc/wpa_supplicant/wpa_supplicant.conf 65 | systemctl enable wpa_supplicant@wlan0.service 66 | -------------------------------------------------------------------------------- /export-noobs/00-release/00-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | NOOBS_DIR="${STAGE_WORK_DIR}/${IMG_DATE}-${IMG_NAME}${IMG_SUFFIX}" 4 | 5 | install -v -m 744 files/partition_setup.sh "${NOOBS_DIR}/" 6 | install -v files/partitions.json "${NOOBS_DIR}/" 7 | install -v files/os.json "${NOOBS_DIR}/" 8 | install -v files/OS.png "${NOOBS_DIR}/" 9 | install -v files/release_notes.txt "${NOOBS_DIR}/" 10 | 11 | tar -v -c -C files/marketing -f "${NOOBS_DIR}/marketing.tar" . 12 | 13 | BOOT_SHASUM="$(sha256sum "${NOOBS_DIR}/boot.tar.xz" | cut -f1 -d' ')" 14 | ROOT_SHASUM="$(sha256sum "${NOOBS_DIR}/root.tar.xz" | cut -f1 -d' ')" 15 | 16 | BOOT_SIZE="$(xz --robot -l "${NOOBS_DIR}/boot.tar.xz" | grep totals | cut -f 5)" 17 | ROOT_SIZE="$(xz --robot -l "${NOOBS_DIR}/root.tar.xz" | grep totals | cut -f 5)" 18 | 19 | BOOT_SIZE="$(( BOOT_SIZE / 1024 / 1024 + 1))" 20 | ROOT_SIZE="$(( ROOT_SIZE / 1024 / 1024 + 1))" 21 | 22 | BOOT_NOM="256" 23 | ROOT_NOM="$(echo "$ROOT_SIZE" | awk '{printf "%.0f", (($1 + 400) * 1.2) + 0.5 }')" 24 | 25 | mv "${NOOBS_DIR}/OS.png" "${NOOBS_DIR}/${NOOBS_NAME// /_}.png" 26 | 27 | sed "${NOOBS_DIR}/partitions.json" -i -e "s|BOOT_SHASUM|${BOOT_SHASUM}|" 28 | sed "${NOOBS_DIR}/partitions.json" -i -e "s|ROOT_SHASUM|${ROOT_SHASUM}|" 29 | 30 | sed "${NOOBS_DIR}/partitions.json" -i -e "s|BOOT_SIZE|${BOOT_SIZE}|" 31 | sed "${NOOBS_DIR}/partitions.json" -i -e "s|ROOT_SIZE|${ROOT_SIZE}|" 32 | 33 | sed "${NOOBS_DIR}/partitions.json" -i -e "s|BOOT_NOM|${BOOT_NOM}|" 34 | sed "${NOOBS_DIR}/partitions.json" -i -e "s|ROOT_NOM|${ROOT_NOM}|" 35 | 36 | sed "${NOOBS_DIR}/os.json" -i -e "s|UNRELEASED|${IMG_DATE}|" 37 | sed "${NOOBS_DIR}/os.json" -i -e "s|NOOBS_NAME|${NOOBS_NAME}|" 38 | sed "${NOOBS_DIR}/os.json" -i -e "s|NOOBS_DESCRIPTION|${NOOBS_DESCRIPTION}|" 39 | sed "${NOOBS_DIR}/os.json" -i -e "s|RELEASE|${RELEASE}|" 40 | sed "${NOOBS_DIR}/os.json" -i -e "s|KERNEL|$(cat "${STAGE_WORK_DIR}/kernel_version")|" 41 | 42 | sed "${NOOBS_DIR}/release_notes.txt" -i -e "s|UNRELEASED|${IMG_DATE}|" 43 | 44 | if [ "${USE_QCOW2}" = "1" ]; then 45 | mv "${NOOBS_DIR}" "${DEPLOY_DIR}/" 46 | else 47 | cp -a "${NOOBS_DIR}" "${DEPLOY_DIR}/" 48 | fi 49 | -------------------------------------------------------------------------------- /stage1/00-boot-files/files/config.txt: -------------------------------------------------------------------------------- 1 | # For more options and information see 2 | # http://rpf.io/configtxt 3 | # Some settings may impact device functionality. See link above for details 4 | 5 | # uncomment if you get no picture on HDMI for a default "safe" mode 6 | #hdmi_safe=1 7 | 8 | # uncomment this if your display has a black border of unused pixels visible 9 | # and your display can output without overscan 10 | #disable_overscan=1 11 | 12 | # uncomment the following to adjust overscan. Use positive numbers if console 13 | # goes off screen, and negative if there is too much border 14 | #overscan_left=16 15 | #overscan_right=16 16 | #overscan_top=16 17 | #overscan_bottom=16 18 | 19 | # uncomment to force a console size. By default it will be display's size minus 20 | # overscan. 21 | #framebuffer_width=1280 22 | #framebuffer_height=720 23 | 24 | # uncomment if hdmi display is not detected and composite is being output 25 | #hdmi_force_hotplug=1 26 | 27 | # uncomment to force a specific HDMI mode (this will force VGA) 28 | #hdmi_group=1 29 | #hdmi_mode=1 30 | 31 | # uncomment to force a HDMI mode rather than DVI. This can make audio work in 32 | # DMT (computer monitor) modes 33 | #hdmi_drive=2 34 | 35 | # uncomment to increase signal to HDMI, if you have interference, blanking, or 36 | # no display 37 | #config_hdmi_boost=4 38 | 39 | # uncomment for composite PAL 40 | #sdtv_mode=2 41 | 42 | #uncomment to overclock the arm. 700 MHz is the default. 43 | #arm_freq=800 44 | 45 | # Uncomment some or all of these to enable the optional hardware interfaces 46 | dtparam=i2c_arm=on 47 | dtparam=i2s=on 48 | dtparam=spi=on 49 | 50 | # pirate audio 3w amp DAC enable 51 | gpio=25=op,dh 52 | 53 | # Uncomment this to enable infrared communication. 54 | #dtoverlay=gpio-ir,gpio_pin=17 55 | #dtoverlay=gpio-ir-tx,gpio_pin=18 56 | 57 | # Additional overlays and parameters are documented /boot/overlays/README 58 | 59 | # Enable audio (loads snd_bcm2835) 60 | # dtparam=audio=on 61 | 62 | [pi4] 63 | # Enable DRM VC4 V3D driver on top of the dispmanx display stack 64 | dtoverlay=vc4-fkms-v3d 65 | max_framebuffers=2 66 | 67 | [all] 68 | #dtoverlay=vc4-fkms-v3d 69 | 70 | max_usb_current=1 71 | dtoverlay=i2s-mmap 72 | dtoverlay=hifiberry-dac 73 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | install -m 755 files/resize2fs_once "${ROOTFS_DIR}/etc/init.d/" 4 | 5 | install -d "${ROOTFS_DIR}/etc/systemd/system/rc-local.service.d" 6 | install -m 644 files/ttyoutput.conf "${ROOTFS_DIR}/etc/systemd/system/rc-local.service.d/" 7 | 8 | install -m 644 files/50raspi "${ROOTFS_DIR}/etc/apt/apt.conf.d/" 9 | 10 | install -m 644 files/console-setup "${ROOTFS_DIR}/etc/default/" 11 | 12 | install -m 755 files/rc.local "${ROOTFS_DIR}/etc/" 13 | 14 | if [ -n "${PUBKEY_SSH_FIRST_USER}" ]; then 15 | install -v -m 0700 -o 1000 -g 1000 -d "${ROOTFS_DIR}"/home/"${FIRST_USER_NAME}"/.ssh 16 | echo "${PUBKEY_SSH_FIRST_USER}" >"${ROOTFS_DIR}"/home/"${FIRST_USER_NAME}"/.ssh/authorized_keys 17 | chown 1000:1000 "${ROOTFS_DIR}"/home/"${FIRST_USER_NAME}"/.ssh/authorized_keys 18 | chmod 0600 "${ROOTFS_DIR}"/home/"${FIRST_USER_NAME}"/.ssh/authorized_keys 19 | fi 20 | 21 | if [ "${PUBKEY_ONLY_SSH}" = "1" ]; then 22 | sed -i -Ee 's/^#?[[:blank:]]*PubkeyAuthentication[[:blank:]]*no[[:blank:]]*$/PubkeyAuthentication yes/ 23 | s/^#?[[:blank:]]*PasswordAuthentication[[:blank:]]*yes[[:blank:]]*$/PasswordAuthentication no/' "${ROOTFS_DIR}"/etc/ssh/sshd_config 24 | fi 25 | 26 | on_chroot << EOF 27 | systemctl disable hwclock.sh 28 | systemctl disable nfs-common 29 | systemctl disable rpcbind 30 | if [ "${ENABLE_SSH}" == "1" ]; then 31 | systemctl enable ssh 32 | else 33 | systemctl disable ssh 34 | fi 35 | systemctl enable regenerate_ssh_host_keys 36 | EOF 37 | 38 | if [ "${USE_QEMU}" = "1" ]; then 39 | echo "enter QEMU mode" 40 | install -m 644 files/90-qemu.rules "${ROOTFS_DIR}/etc/udev/rules.d/" 41 | on_chroot << EOF 42 | systemctl disable resize2fs_once 43 | EOF 44 | echo "leaving QEMU mode" 45 | else 46 | on_chroot << EOF 47 | systemctl enable resize2fs_once 48 | EOF 49 | fi 50 | 51 | on_chroot <<EOF 52 | for GRP in input spi i2c gpio; do 53 | groupadd -f -r "\$GRP" 54 | done 55 | for GRP in adm dialout cdrom audio users sudo video games plugdev input gpio spi i2c netdev; do 56 | adduser $FIRST_USER_NAME \$GRP 57 | done 58 | EOF 59 | 60 | on_chroot << EOF 61 | setupcon --force --save-only -v 62 | EOF 63 | 64 | on_chroot << EOF 65 | usermod --pass='*' root 66 | EOF 67 | 68 | rm -f "${ROOTFS_DIR}/etc/ssh/"ssh_host_*_key* 69 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: shell 2 | services: 3 | - docker 4 | before_script: 5 | - | 6 | if ! git describe --exact-match --tags HEAD &> /dev/null 7 | then 8 | echo "Not a tagged release, not deploying." 9 | exit 10 | fi 11 | - cp config.example config 12 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 13 | script: 14 | - "./build-docker.sh" 15 | deploy: 16 | provider: releases 17 | api_key: 18 | secure: LCOtqXO4zazwRbtYxqFkxataB7nH68bPX9w4oKzHkZ1uHy2NIjCdLGREGfzcRClg035Lwk/7oUyyWianDfZzriOijBjwj4B4xsYZAKuPz07GOmF6akoe7ejMY47OqI3j3WP9Bl5lQ58T3uIfAkvphrqSI2j8Wdf0QV6KZx5Pz/xayOSCaCal//UvrDhzIzoQ71l/EanW2aS1Sp2SlHe1A3uvahHUS78vOgDMaf+rpHibvh4d/3XsGCHxHtuzQNqrIq7lqnc5vMJzVjQ6rgLSUYH4Ec31JUhJOgCO1YHl2VVs+OmeOYIiyQWkGkinttx6wpC0o+NPqqm1AtgUvqIlmTTiqkimQ7uRpwO2tNt9swMfh5qVcJ5P2Jom8mftpi0tHZrS/xP0mYdYutYag/AGnbrEvlY1DQYejeFB/VMxXwOTGC0nQ1WvUO4AwmBaiZKSJq11xpyuzN/DjQvRZKWQYScBpIVZP0J4k1HjV0fgyAG1SAW2Vtb2ID5tuqwxN6PMbHRhwhZIlyu2I4DuZfKKZVDlum/68Kt2dOX5NkUAGnSm+qj6s19PPaij/fjbTJ7hvPP+eZiK/cC1vSvDuUWkrM6jNgpaOslGQKZ8n1KAuFB5do6yqvINT43ZNndAVb8xwkrWM+8u4OJWcq+4FIlaUvfQYQegbei+of1l2zujCqo= 19 | edge: true 20 | on: 21 | all_branches: true 22 | tags: true 23 | file: deploy/image_`date +"%Y-%m-%d"`-Transistor-lite.zip 24 | env: 25 | global: 26 | - secure: i+FXYnykjOSIR8pF1lnjjykUu4EoLnCUmq3A8rAv33G1uhF3yI+CCpglXgZacV7/sz6l2h56ydy9TQEk1Iijve067zo/FRRxD5wkP0DCZxAUb1ZPBNf2L80fCK7hK4f0t1JwTQ4DEerjN6CQ2hH9793ir62bXZXY+YxUsx/HYTyP3OA1ObFtqk3pQLOl6V764L7qLbRAdoWmD6bjHW1XT2XbURivQebIHHcRhy1ycwPKVCqI+XYUEr2R9OnMqZJ6oVVChQPpiqtZKUfHXabbJc7D8FL/0pFtVU1wRvBeK/t1E0if2WLp8sH+fbkOCyze+yBy2P/fpAKsWhzk/48xVQFicGmE0Y+s8XoexSSkr7OQAQjiVzfoN/C8Y2inB0gDiE9ws7cqi0zscHlz6FauCEYAy7lMqWDa/RkRkzsZ2oNn1w06WIdDmU62qW/W3GYxELtaJF6PTljThCh2jQEQq3OxmDSqCagaD1amAZ4qSxYDN3wPnub7TgxaMazQqbIukIUKJ4mZ07C+FSvicau97frOZGAKoGSv9t4mDSP/NCO9zFLFAkyZQ52YMSVaBfQ8F/2nbTDLvQ9xT/vGC7KRW9ZLaUIOlI9XmjmaEhfZLqYfnlOQNKPOB7Y7f4MiVSc4/HkcDyT56yWlK86oM1UnZzMVtrGxWGSaCNPyxDTTp7k= 27 | - secure: Jc2e8VoLbOycoS1IXfUj2xceHEtmBRm+GCAn8SzPvDfVpmQ6EcDeYyS1Fv61daaqr07vMm+bmPO9KPGW/8Spobx4ZJY/ttAlP8BWuhknc5TNkv5cfosbEdgS3w4mBZw9pn2IOrXqN8RrxiHe1TMmDtj+Zok5klvX///aL6RiHazoY2GnQSzmp+lxrhQjpVoia+PXbHuH8Ei10Zn4Wc0FWPLtJSsJjbzh5/ypIq14EnyWiDOTpMxcbx8tOcdCdIArV8OIWEsBjsmcGT6dzJys9+m6jisQw4p5od6HcroUQKZTLNLwKCD6RQo7D+5r/bY+AWu+HeRPanqDARDeS/3M/6WGCMMVz93QA4g8S8UpmR6Syib93DDBgR5j7v/t/mXaFmsJt8An9NELRvV5E6IMpF2Y+W3s/Xup8wW3Eog8ZN5Y/ZwI5UCyx7EbuaHrDnPbs/z88T0Idy7Ejo0wdrKXw9qeHeFP1RLNaysj4z+Q7iuV1DUTXshX571LGAdW2ulpm6Q1Uk87qE2+XtVO/FAb0H+/n0JQRv/ryIATlP+zAD9iBygCuRHZbSexp6iHQe97ELBJZBBBAsp32RgBgalKMnbN50cVF3QRlnZ8IVypApwXdlgS/+d6zR46YsJ2HwBR7BkVs2jOvvNouOW7J6I+29SFlJBzr7gtkxGUX5DVLr4= 28 | -------------------------------------------------------------------------------- /export-noobs/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | NOOBS_DIR="${STAGE_WORK_DIR}/${IMG_DATE}-${IMG_NAME}${IMG_SUFFIX}" 4 | mkdir -p "${STAGE_WORK_DIR}" 5 | 6 | if [ "${DEPLOY_ZIP}" == "1" ]; then 7 | IMG_FILE="${WORK_DIR}/export-image/${IMG_FILENAME}${IMG_SUFFIX}.img" 8 | else 9 | IMG_FILE="${DEPLOY_DIR}/${IMG_FILENAME}${IMG_SUFFIX}.img" 10 | fi 11 | 12 | unmount_image "${IMG_FILE}" 13 | 14 | rm -rf "${NOOBS_DIR}" 15 | 16 | PARTED_OUT=$(parted -sm "${IMG_FILE}" unit b print) 17 | BOOT_OFFSET=$(echo "$PARTED_OUT" | grep -e '^1:' | cut -d':' -f 2 | tr -d B) 18 | BOOT_LENGTH=$(echo "$PARTED_OUT" | grep -e '^1:' | cut -d':' -f 4 | tr -d B) 19 | 20 | ROOT_OFFSET=$(echo "$PARTED_OUT" | grep -e '^2:' | cut -d':' -f 2 | tr -d B) 21 | ROOT_LENGTH=$(echo "$PARTED_OUT" | grep -e '^2:' | cut -d':' -f 4 | tr -d B) 22 | 23 | echo "Mounting BOOT_DEV..." 24 | cnt=0 25 | until BOOT_DEV=$(losetup --show -f -o "${BOOT_OFFSET}" --sizelimit "${BOOT_LENGTH}" "${IMG_FILE}"); do 26 | if [ $cnt -lt 5 ]; then 27 | cnt=$((cnt + 1)) 28 | echo "Error in losetup for BOOT_DEV. Retrying..." 29 | sleep 5 30 | else 31 | echo "ERROR: losetup for BOOT_DEV failed; exiting" 32 | exit 1 33 | fi 34 | done 35 | 36 | echo "Mounting ROOT_DEV..." 37 | cnt=0 38 | until ROOT_DEV=$(losetup --show -f -o "${ROOT_OFFSET}" --sizelimit "${ROOT_LENGTH}" "${IMG_FILE}"); do 39 | if [ $cnt -lt 5 ]; then 40 | cnt=$((cnt + 1)) 41 | echo "Error in losetup for ROOT_DEV. Retrying..." 42 | sleep 5 43 | else 44 | echo "ERROR: losetup for ROOT_DEV failed; exiting" 45 | exit 1 46 | fi 47 | done 48 | 49 | echo "/boot: offset $BOOT_OFFSET, length $BOOT_LENGTH" 50 | echo "/: offset $ROOT_OFFSET, length $ROOT_LENGTH" 51 | 52 | mkdir -p "${STAGE_WORK_DIR}/rootfs" 53 | mkdir -p "${NOOBS_DIR}" 54 | 55 | mount "$ROOT_DEV" "${STAGE_WORK_DIR}/rootfs" 56 | mount "$BOOT_DEV" "${STAGE_WORK_DIR}/rootfs/boot" 57 | 58 | ln -sv "/lib/systemd/system/apply_noobs_os_config.service" "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/apply_noobs_os_config.service" 59 | 60 | KERNEL_VER="$(zgrep -oPm 1 "Linux version \K(.*)$" "${STAGE_WORK_DIR}/rootfs/usr/share/doc/raspberrypi-kernel/changelog.Debian.gz" | cut -f-2 -d.)" 61 | echo "$KERNEL_VER" > "${STAGE_WORK_DIR}/kernel_version" 62 | 63 | bsdtar --numeric-owner --format gnutar -C "${STAGE_WORK_DIR}/rootfs/boot" -cpf - . | xz -T0 > "${NOOBS_DIR}/boot.tar.xz" 64 | umount "${STAGE_WORK_DIR}/rootfs/boot" 65 | bsdtar --numeric-owner --format gnutar -C "${STAGE_WORK_DIR}/rootfs" --one-file-system -cpf - . | xz -T0 > "${NOOBS_DIR}/root.tar.xz" 66 | 67 | if [ "${USE_QCOW2}" = "1" ]; then 68 | rm "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/apply_noobs_os_config.service" 69 | fi 70 | 71 | unmount_image "${IMG_FILE}" 72 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-interface/helpers.py: -------------------------------------------------------------------------------- 1 | """Helper functions for the controllers.""" 2 | from contextlib import contextmanager 3 | from functools import wraps 4 | 5 | import netifaces 6 | import pulsectl 7 | import simpleaudio as sa 8 | from mpd import MPDClient 9 | 10 | SLEEP_TIMER_SOUND = "/usr/local/lib/radio-interface/sleep-timer.wav" 11 | MPD_HOST, MPD_PORT = "localhost", 6600 12 | 13 | 14 | def notify(function): 15 | """ 16 | Play a sound to inform the user that an action has been registered. 17 | To be used as a decorator. 18 | """ 19 | 20 | @wraps(function) 21 | def wrapper(*args, **kwargs): 22 | sleep_timer_sound = sa.WaveObject.from_wave_file(SLEEP_TIMER_SOUND) 23 | notification = sleep_timer_sound.play() 24 | return function(*args, **kwargs) 25 | notification.wait_done() 26 | 27 | return wrapper 28 | 29 | 30 | @contextmanager 31 | def connection_to_mpd(): 32 | """Context manager to establish the connection with MPD.""" 33 | mpd = MPDClient() 34 | try: 35 | mpd.timeout = 10 36 | mpd.idletimeout = None 37 | mpd.connect(MPD_HOST, MPD_PORT) 38 | yield mpd 39 | finally: 40 | mpd.close() 41 | mpd.disconnect() 42 | 43 | 44 | @contextmanager 45 | def connection_to_pulseaudio(max_volume): 46 | """Context manager to establish a connection with Pulse Audio. 47 | 48 | Should be used each time getting the volume is necessary since the 49 | client is not synchronized when the volume is changed on another 50 | client. 51 | That is necessary for instance when changing the volume as the current 52 | volume is needed in order to apply a change. 53 | 54 | We also make sure we don't go above the maximum audio volume 55 | threshold set in the audio module. 56 | """ 57 | try: 58 | pulse_client = pulsectl.Pulse("radio-interface") 59 | pulse_sink = pulse_client.sink_list()[0] 60 | yield {"client": pulse_client, "sink": pulse_sink} 61 | finally: 62 | if pulse_client.volume_get_all_chans(pulse_sink) > max_volume: 63 | pulse_client.volume_set_all_chans(pulse_sink, max_volume) 64 | pulse_client.close() 65 | 66 | 67 | def local_ip_address() -> str: 68 | """Return local IP address.""" 69 | ip_address = netifaces.ifaddresses("wlan0") 70 | ip_address = ip_address[netifaces.AF_INET] 71 | ip_address = ip_address[0].get("addr") 72 | return ip_address 73 | 74 | 75 | def playing(content: str) -> str: 76 | """Fetch the currently playing content. 77 | 78 | Available content is: 79 | - name 80 | - album 81 | - artist 82 | - title 83 | """ 84 | with connection_to_mpd() as mpd: 85 | playing = mpd.currentsong() 86 | return playing.get(content) 87 | -------------------------------------------------------------------------------- /imagetool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$(id -u)" != "0" ]; then 4 | echo "Please run as root" 1>&2 5 | exit 1 6 | fi 7 | 8 | progname=$(basename $0) 9 | 10 | function usage() 11 | { 12 | cat << HEREDOC 13 | 14 | Usage: 15 | Mount Image : $progname [--mount] [--image-name <path to qcow2 image>] [--mount-point <mount point>] 16 | Umount Image: $progname [--umount] [--mount-point <mount point>] 17 | Cleanup NBD : $progname [--cleanup] 18 | 19 | arguments: 20 | -h, --help show this help message and exit 21 | -c, --cleanup cleanup orphaned device mappings 22 | -m, --mount mount image 23 | -u, --umount umount image 24 | -i, --image-name path to qcow2 image 25 | -p, --mount-point mount point for image 26 | 27 | This tool will use /dev/nbd1 as default for mounting an image. If you want to use another device, execute like this: 28 | NBD_DEV=/dev/nbd2 ./$progname --mount --image <your image> --mount-point <your path> 29 | 30 | HEREDOC 31 | } 32 | 33 | MOUNT=0 34 | UMOUNT=0 35 | IMAGE="" 36 | MOUNTPOINT="" 37 | 38 | nbd_cleanup() { 39 | DEVS="$(lsblk | grep nbd | grep disk | cut -d" " -f1)" 40 | if [ ! -z "${DEVS}" ]; then 41 | for d in $DEVS; do 42 | if [ ! -z "${d}" ]; then 43 | QDEV="$(ps xa | grep $d | grep -v grep)" 44 | if [ -z "${QDEV}" ]; then 45 | kpartx -d /dev/$d && echo "Unconnected device map removed: /dev/$d" 46 | fi 47 | fi 48 | done 49 | fi 50 | } 51 | 52 | # As long as there is at least one more argument, keep looping 53 | while [[ $# -gt 0 ]]; do 54 | key="$1" 55 | case "$key" in 56 | -h|--help) 57 | usage 58 | exit 59 | ;; 60 | -c|--cleanup) 61 | nbd_cleanup 62 | ;; 63 | -m|--mount) 64 | MOUNT=1 65 | ;; 66 | -u|--umount) 67 | UMOUNT=1 68 | ;; 69 | -i|--image-name) 70 | shift 71 | IMAGE="$1" 72 | ;; 73 | -p|--mount-point) 74 | shift 75 | MOUNTPOINT="$1" 76 | ;; 77 | *) 78 | echo "Unknown option '$key'" 79 | usage 80 | exit 81 | ;; 82 | esac 83 | # Shift after checking all the cases to get the next option 84 | shift 85 | done 86 | 87 | if [ "${MOUNT}" = "1" ] && [ "${UMOUNT}" = "1" ]; then 88 | usage 89 | echo "Concurrent mount options not possible." 90 | exit 91 | fi 92 | 93 | if [ "${MOUNT}" = "1" ] && ([ -z "${IMAGE}" ] || [ -z "${MOUNTPOINT}" ]); then 94 | usage 95 | echo "Can not mount image. Image path and/or mount point missing." 96 | exit 97 | fi 98 | 99 | if [ "${UMOUNT}" = "1" ] && [ -z "${MOUNTPOINT}" ]; then 100 | usage 101 | echo "Can not umount. Mount point parameter missing." 102 | exit 103 | fi 104 | 105 | export NBD_DEV="${NBD_DEV:-/dev/nbd1}" 106 | source scripts/qcow2_handling 107 | 108 | if [ "${MOUNT}" = "1" ]; then 109 | mount_qimage "${MOUNTPOINT}" "${IMAGE}" 110 | elif [ "${UMOUNT}" = "1" ]; then 111 | umount_qimage "${MOUNTPOINT}" 112 | fi 113 | -------------------------------------------------------------------------------- /scripts/common: -------------------------------------------------------------------------------- 1 | log (){ 2 | date +"[%T] $*" | tee -a "${LOG_FILE}" 3 | } 4 | export -f log 5 | 6 | bootstrap(){ 7 | local BOOTSTRAP_CMD=debootstrap 8 | local BOOTSTRAP_ARGS=() 9 | 10 | export http_proxy=${APT_PROXY} 11 | 12 | if [ "$(dpkg --print-architecture)" != "armhf" ] && [ "$(dpkg --print-architecture)" != "aarch64" ]; then 13 | BOOTSTRAP_CMD=qemu-debootstrap 14 | fi 15 | 16 | BOOTSTRAP_ARGS+=(--arch armhf) 17 | BOOTSTRAP_ARGS+=(--components "main,contrib,non-free") 18 | BOOTSTRAP_ARGS+=(--keyring "${STAGE_DIR}/files/raspberrypi.gpg") 19 | BOOTSTRAP_ARGS+=("$@") 20 | printf -v BOOTSTRAP_STR '%q ' "${BOOTSTRAP_ARGS[@]}" 21 | 22 | setarch linux32 capsh --drop=cap_setfcap -- -c "'${BOOTSTRAP_CMD}' $BOOTSTRAP_STR" || true 23 | 24 | if [ -d "$2/debootstrap" ]; then 25 | rmdir "$2/debootstrap" 26 | fi 27 | } 28 | export -f bootstrap 29 | 30 | copy_previous(){ 31 | if [ ! -d "${PREV_ROOTFS_DIR}" ]; then 32 | echo "Previous stage rootfs not found" 33 | false 34 | fi 35 | mkdir -p "${ROOTFS_DIR}" 36 | rsync -aHAXx --exclude var/cache/apt/archives "${PREV_ROOTFS_DIR}/" "${ROOTFS_DIR}/" 37 | } 38 | export -f copy_previous 39 | 40 | unmount(){ 41 | if [ -z "$1" ]; then 42 | DIR=$PWD 43 | else 44 | DIR=$1 45 | fi 46 | 47 | while mount | grep -q "$DIR"; do 48 | local LOCS 49 | LOCS=$(mount | grep "$DIR" | cut -f 3 -d ' ' | sort -r) 50 | for loc in $LOCS; do 51 | umount "$loc" 52 | done 53 | done 54 | } 55 | export -f unmount 56 | 57 | unmount_image(){ 58 | sync 59 | sleep 1 60 | local LOOP_DEVICES 61 | LOOP_DEVICES=$(losetup --list | grep "$(basename "${1}")" | cut -f1 -d' ') 62 | for LOOP_DEV in ${LOOP_DEVICES}; do 63 | if [ -n "${LOOP_DEV}" ]; then 64 | local MOUNTED_DIR 65 | MOUNTED_DIR=$(mount | grep "$(basename "${LOOP_DEV}")" | head -n 1 | cut -f 3 -d ' ') 66 | if [ -n "${MOUNTED_DIR}" ] && [ "${MOUNTED_DIR}" != "/" ]; then 67 | unmount "$(dirname "${MOUNTED_DIR}")" 68 | fi 69 | sleep 1 70 | losetup -d "${LOOP_DEV}" 71 | fi 72 | done 73 | } 74 | export -f unmount_image 75 | 76 | on_chroot() { 77 | if ! mount | grep -q "$(realpath "${ROOTFS_DIR}"/proc)"; then 78 | mount -t proc proc "${ROOTFS_DIR}/proc" 79 | fi 80 | 81 | if ! mount | grep -q "$(realpath "${ROOTFS_DIR}"/dev)"; then 82 | mount --bind /dev "${ROOTFS_DIR}/dev" 83 | fi 84 | 85 | if ! mount | grep -q "$(realpath "${ROOTFS_DIR}"/dev/pts)"; then 86 | mount --bind /dev/pts "${ROOTFS_DIR}/dev/pts" 87 | fi 88 | 89 | if ! mount | grep -q "$(realpath "${ROOTFS_DIR}"/sys)"; then 90 | mount --bind /sys "${ROOTFS_DIR}/sys" 91 | fi 92 | 93 | setarch linux32 capsh --drop=cap_setfcap "--chroot=${ROOTFS_DIR}/" -- -e "$@" 94 | } 95 | export -f on_chroot 96 | 97 | update_issue() { 98 | echo -e "Raspberry Pi reference ${IMG_DATE}\nGenerated using ${PI_GEN}, ${PI_GEN_REPO}, ${GIT_HASH}, ${1}" > "${ROOTFS_DIR}/etc/rpi-issue" 99 | } 100 | export -f update_issue 101 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-interface/display.py: -------------------------------------------------------------------------------- 1 | """Controller for the screen display device.""" 2 | import asyncio 3 | import time 4 | from colorsys import hsv_to_rgb 5 | 6 | from PIL import Image, ImageDraw, ImageFont 7 | from ST7789 import ST7789 8 | 9 | from audio import Audio 10 | from helpers import local_ip_address, notify, playing 11 | 12 | BG_COLOR = (255, 255, 0) 13 | TEXT_COLOR = (0, 0, 0) 14 | SCROLL_SPEED = 90 15 | SPI_SPEED_MHZ = 80 16 | 17 | audio = Audio() 18 | 19 | 20 | class Display: 21 | """Implements the display.""" 22 | 23 | def __init__(self) -> None: 24 | self.display = ST7789( 25 | rotation=90, # Needed to display the right way up on Pirate Audio 26 | port=0, # SPI port 27 | cs=1, # SPI port Chip-select channel 28 | dc=9, # BCM pin used for data/command 29 | backlight=13, 30 | spi_speed_hz=SPI_SPEED_MHZ * 1000 * 1000, 31 | ) 32 | self.image = Image.new("RGB", (240, 240), BG_COLOR) 33 | self.draw = ImageDraw.Draw(self.image) 34 | self.font = ImageFont.truetype( 35 | "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 30 36 | ) 37 | self.current_display = "" 38 | self.display_switch = "stream" 39 | 40 | async def screen(self) -> None: 41 | """ 42 | Listens to the various display queues and displays their 43 | messages on the screen. 44 | """ 45 | text_x = self.display.width 46 | time_start = time.time() 47 | while True: 48 | text = self.current_display 49 | x = (time.time() - time_start) * SCROLL_SPEED 50 | size_x, size_y = self.draw.textsize(text, self.font) 51 | x %= size_x + self.display.width 52 | self.draw.rectangle( 53 | (0, 0, self.display.width, self.display.height), BG_COLOR 54 | ) 55 | text_y = (self.display.height - size_y) // 2 56 | self.draw.text( 57 | (int(text_x - x), text_y), text, font=self.font, fill=TEXT_COLOR 58 | ) 59 | self.display.display(self.image) 60 | await asyncio.sleep(0.1) 61 | 62 | async def current_stream(self) -> None: 63 | """ 64 | Update the currently playing display from MPD. 65 | """ 66 | while True: 67 | if self.display_switch == "stream": 68 | name = playing("name") 69 | title = playing("title") 70 | text = "" 71 | if name: 72 | text += name + " // " 73 | if title: 74 | text += title 75 | self.current_display = text 76 | await asyncio.sleep(1) 77 | 78 | def ip_address(self) -> None: 79 | """ 80 | Display the local IP address on screen. 81 | """ 82 | Audio.play_pause_button_was_held = True 83 | self.display_switch = "ip" 84 | self.current_display = local_ip_address() 85 | time.sleep(5) 86 | self.display_switch = "stream" 87 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/models.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | 4 | import feedparser 5 | import requests 6 | from content_aggregator.settings import PODCASTS_PATH 7 | from django.db import models 8 | from django.db.models.signals import post_delete 9 | from django.dispatch import receiver 10 | from django.utils.text import slugify 11 | 12 | 13 | class Feed(models.Model): 14 | title = models.CharField(max_length=200, blank=True) 15 | slug = models.SlugField(max_length=255, unique=True) 16 | description = models.TextField(blank=True) 17 | subscribe_date = models.DateTimeField(auto_now_add=True) 18 | url = models.URLField() 19 | link = models.URLField(blank=True) 20 | max_entries = models.PositiveSmallIntegerField( 21 | default=3, verbose_name="Number of episodes to keep" 22 | ) 23 | image = models.URLField(blank=True) 24 | fs_path = models.CharField(max_length=255) 25 | 26 | def __str__(self) -> str: 27 | return f"{self.title}" 28 | 29 | def save(self, *args, **kwargs): 30 | channel = feedparser.parse(self.url) 31 | self.title = channel.feed.get("title", "No Title") 32 | self.description = channel.feed.get("description", "No Description") 33 | self.link = channel.feed.get("link") 34 | self.image = channel.feed.get("image").get("href") 35 | self.slug = slugify(self.title) 36 | self.fs_path = PODCASTS_PATH + "/" + self.slug 37 | super(Feed, self).save(*args, **kwargs) 38 | 39 | 40 | @receiver(post_delete, sender=Feed) 41 | def _delete_feed_fs_path(sender, instance, **kwargs): 42 | shutil.rmtree(instance.fs_path, ignore_errors=True) 43 | 44 | 45 | class Episode(models.Model): 46 | title = models.CharField(max_length=200) 47 | pub_date = models.DateTimeField() 48 | link = models.URLField() 49 | feed = models.ForeignKey(Feed, on_delete=models.CASCADE) 50 | audio = models.URLField(blank=True) 51 | local_filename = models.CharField(max_length=255, blank=True) 52 | 53 | def download_file(self, url): 54 | Path(self.feed.fs_path).mkdir(parents=True, exist_ok=True) 55 | filetype = '.' + url.split('.')[-1] 56 | pub_date = self.pub_date.strftime("%Y-%m-%d") 57 | self.local_filename = pub_date + "-" + slugify(self.title) + filetype 58 | destination = self.feed.fs_path + "/" + self.local_filename 59 | with requests.get(url, stream=True) as r: 60 | with open(destination, "wb") as f: 61 | shutil.copyfileobj(r.raw, f) 62 | 63 | def __str__(self) -> str: 64 | return f"{self.feed.title}: {self.title}" 65 | 66 | def save(self, *args, **kwargs): 67 | if self.audio != "": 68 | self.download_file(self.audio) 69 | super(Episode, self).save(*args, **kwargs) 70 | 71 | 72 | @receiver(post_delete, sender=Episode) 73 | def _delete_episode_fs_path(sender, instance, **kwargs): 74 | audio_file = Path(instance.feed.fs_path + '/' + instance.local_filename) 75 | try: 76 | audio_file.unlink() 77 | except FileNotFoundError: 78 | pass 79 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/templates/homepage.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | {% load static %} 5 | <meta charset="UTF-8" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 | <title>Transistor - Podcasts Settings 8 | 14 | 15 | 16 | 17 | 18 |
19 | 33 |
34 | 35 |
36 |
37 |
38 |

Click here to subscribe to a new podcast

39 |
Free space left on the device: {{ free_space }}%
40 |
41 |
42 |
43 |
44 | {% for feed in feeds %} 45 |
46 |
47 |
48 | {{ feed.title }} 53 |
54 | 69 |
70 |
71 | {% endfor %} 72 |
73 |
74 |
75 | 76 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /export-image/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ "${NO_PRERUN_QCOW2}" = "0" ]; then 4 | IMG_FILE="${STAGE_WORK_DIR}/${IMG_FILENAME}${IMG_SUFFIX}.img" 5 | 6 | unmount_image "${IMG_FILE}" 7 | 8 | rm -f "${IMG_FILE}" 9 | 10 | rm -rf "${ROOTFS_DIR}" 11 | mkdir -p "${ROOTFS_DIR}" 12 | 13 | BOOT_SIZE="$((256 * 1024 * 1024))" 14 | ROOT_SIZE=$(du --apparent-size -s "${EXPORT_ROOTFS_DIR}" --exclude var/cache/apt/archives --exclude boot --block-size=1 | cut -f 1) 15 | 16 | # All partition sizes and starts will be aligned to this size 17 | ALIGN="$((4 * 1024 * 1024))" 18 | # Add this much space to the calculated file size. This allows for 19 | # some overhead (since actual space usage is usually rounded up to the 20 | # filesystem block size) and gives some free space on the resulting 21 | # image. 22 | ROOT_MARGIN="$(echo "($ROOT_SIZE * 0.2 + 200 * 1024 * 1024) / 1" | bc)" 23 | 24 | BOOT_PART_START=$((ALIGN)) 25 | BOOT_PART_SIZE=$(((BOOT_SIZE + ALIGN - 1) / ALIGN * ALIGN)) 26 | ROOT_PART_START=$((BOOT_PART_START + BOOT_PART_SIZE)) 27 | ROOT_PART_SIZE=$(((ROOT_SIZE + ROOT_MARGIN + ALIGN - 1) / ALIGN * ALIGN)) 28 | IMG_SIZE=$((BOOT_PART_START + BOOT_PART_SIZE + ROOT_PART_SIZE)) 29 | 30 | truncate -s "${IMG_SIZE}" "${IMG_FILE}" 31 | 32 | parted --script "${IMG_FILE}" mklabel msdos 33 | parted --script "${IMG_FILE}" unit B mkpart primary fat32 "${BOOT_PART_START}" "$((BOOT_PART_START + BOOT_PART_SIZE - 1))" 34 | parted --script "${IMG_FILE}" unit B mkpart primary ext4 "${ROOT_PART_START}" "$((ROOT_PART_START + ROOT_PART_SIZE - 1))" 35 | 36 | PARTED_OUT=$(parted -sm "${IMG_FILE}" unit b print) 37 | BOOT_OFFSET=$(echo "$PARTED_OUT" | grep -e '^1:' | cut -d':' -f 2 | tr -d B) 38 | BOOT_LENGTH=$(echo "$PARTED_OUT" | grep -e '^1:' | cut -d':' -f 4 | tr -d B) 39 | 40 | ROOT_OFFSET=$(echo "$PARTED_OUT" | grep -e '^2:' | cut -d':' -f 2 | tr -d B) 41 | ROOT_LENGTH=$(echo "$PARTED_OUT" | grep -e '^2:' | cut -d':' -f 4 | tr -d B) 42 | 43 | echo "Mounting BOOT_DEV..." 44 | cnt=0 45 | until BOOT_DEV=$(losetup --show -f -o "${BOOT_OFFSET}" --sizelimit "${BOOT_LENGTH}" "${IMG_FILE}"); do 46 | if [ $cnt -lt 5 ]; then 47 | cnt=$((cnt + 1)) 48 | echo "Error in losetup for BOOT_DEV. Retrying..." 49 | sleep 5 50 | else 51 | echo "ERROR: losetup for BOOT_DEV failed; exiting" 52 | exit 1 53 | fi 54 | done 55 | 56 | echo "Mounting ROOT_DEV..." 57 | cnt=0 58 | until ROOT_DEV=$(losetup --show -f -o "${ROOT_OFFSET}" --sizelimit "${ROOT_LENGTH}" "${IMG_FILE}"); do 59 | if [ $cnt -lt 5 ]; then 60 | cnt=$((cnt + 1)) 61 | echo "Error in losetup for ROOT_DEV. Retrying..." 62 | sleep 5 63 | else 64 | echo "ERROR: losetup for ROOT_DEV failed; exiting" 65 | exit 1 66 | fi 67 | done 68 | 69 | echo "/boot: offset $BOOT_OFFSET, length $BOOT_LENGTH" 70 | echo "/: offset $ROOT_OFFSET, length $ROOT_LENGTH" 71 | 72 | ROOT_FEATURES="^huge_file" 73 | for FEATURE in metadata_csum 64bit; do 74 | if grep -q "$FEATURE" /etc/mke2fs.conf; then 75 | ROOT_FEATURES="^$FEATURE,$ROOT_FEATURES" 76 | fi 77 | done 78 | mkdosfs -n boot -F 32 -v "$BOOT_DEV" > /dev/null 79 | mkfs.ext4 -L rootfs -O "$ROOT_FEATURES" "$ROOT_DEV" > /dev/null 80 | 81 | mount -v "$ROOT_DEV" "${ROOTFS_DIR}" -t ext4 82 | mkdir -p "${ROOTFS_DIR}/boot" 83 | mount -v "$BOOT_DEV" "${ROOTFS_DIR}/boot" -t vfat 84 | 85 | rsync -aHAXx --exclude /var/cache/apt/archives --exclude /boot "${EXPORT_ROOTFS_DIR}/" "${ROOTFS_DIR}/" 86 | rsync -rtx "${EXPORT_ROOTFS_DIR}/boot/" "${ROOTFS_DIR}/boot/" 87 | fi 88 | -------------------------------------------------------------------------------- /export-image/04-finalise/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | IMG_FILE="${STAGE_WORK_DIR}/${IMG_FILENAME}${IMG_SUFFIX}.img" 4 | INFO_FILE="${STAGE_WORK_DIR}/${IMG_FILENAME}${IMG_SUFFIX}.info" 5 | 6 | on_chroot << EOF 7 | if [ -x /etc/init.d/fake-hwclock ]; then 8 | /etc/init.d/fake-hwclock stop 9 | fi 10 | if hash hardlink 2>/dev/null; then 11 | hardlink -t /usr/share/doc 12 | fi 13 | EOF 14 | 15 | if [ -d "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/.config" ]; then 16 | chmod 700 "${ROOTFS_DIR}/home/${FIRST_USER_NAME}/.config" 17 | fi 18 | 19 | rm -f "${ROOTFS_DIR}/usr/bin/qemu-arm-static" 20 | 21 | if [ "${USE_QEMU}" != "1" ]; then 22 | if [ -e "${ROOTFS_DIR}/etc/ld.so.preload.disabled" ]; then 23 | mv "${ROOTFS_DIR}/etc/ld.so.preload.disabled" "${ROOTFS_DIR}/etc/ld.so.preload" 24 | fi 25 | fi 26 | 27 | rm -f "${ROOTFS_DIR}/etc/network/interfaces.dpkg-old" 28 | 29 | rm -f "${ROOTFS_DIR}/etc/apt/sources.list~" 30 | rm -f "${ROOTFS_DIR}/etc/apt/trusted.gpg~" 31 | 32 | rm -f "${ROOTFS_DIR}/etc/passwd-" 33 | rm -f "${ROOTFS_DIR}/etc/group-" 34 | rm -f "${ROOTFS_DIR}/etc/shadow-" 35 | rm -f "${ROOTFS_DIR}/etc/gshadow-" 36 | rm -f "${ROOTFS_DIR}/etc/subuid-" 37 | rm -f "${ROOTFS_DIR}/etc/subgid-" 38 | 39 | rm -f "${ROOTFS_DIR}"/var/cache/debconf/*-old 40 | rm -f "${ROOTFS_DIR}"/var/lib/dpkg/*-old 41 | 42 | rm -f "${ROOTFS_DIR}"/usr/share/icons/*/icon-theme.cache 43 | 44 | rm -f "${ROOTFS_DIR}/var/lib/dbus/machine-id" 45 | 46 | true > "${ROOTFS_DIR}/etc/machine-id" 47 | 48 | ln -nsf /proc/mounts "${ROOTFS_DIR}/etc/mtab" 49 | 50 | find "${ROOTFS_DIR}/var/log/" -type f -exec cp /dev/null {} \; 51 | 52 | rm -f "${ROOTFS_DIR}/root/.vnc/private.key" 53 | rm -f "${ROOTFS_DIR}/etc/vnc/updateid" 54 | 55 | update_issue "$(basename "${EXPORT_DIR}")" 56 | install -m 644 "${ROOTFS_DIR}/etc/rpi-issue" "${ROOTFS_DIR}/boot/issue.txt" 57 | 58 | cp "$ROOTFS_DIR/etc/rpi-issue" "$INFO_FILE" 59 | 60 | 61 | { 62 | if [ -f "$ROOTFS_DIR/usr/share/doc/raspberrypi-kernel/changelog.Debian.gz" ]; then 63 | firmware=$(zgrep "firmware as of" \ 64 | "$ROOTFS_DIR/usr/share/doc/raspberrypi-kernel/changelog.Debian.gz" | \ 65 | head -n1 | sed -n 's|.* \([^ ]*\)$|\1|p') 66 | printf "\nFirmware: https://github.com/raspberrypi/firmware/tree/%s\n" "$firmware" 67 | 68 | kernel="$(curl -s -L "https://github.com/raspberrypi/firmware/raw/$firmware/extra/git_hash")" 69 | printf "Kernel: https://github.com/raspberrypi/linux/tree/%s\n" "$kernel" 70 | 71 | uname="$(curl -s -L "https://github.com/raspberrypi/firmware/raw/$firmware/extra/uname_string7")" 72 | printf "Uname string: %s\n" "$uname" 73 | fi 74 | 75 | printf "\nPackages:\n" 76 | dpkg -l --root "$ROOTFS_DIR" 77 | } >> "$INFO_FILE" 78 | 79 | mkdir -p "${DEPLOY_DIR}" 80 | 81 | rm -f "${DEPLOY_DIR}/${ZIP_FILENAME}${IMG_SUFFIX}.zip" 82 | rm -f "${DEPLOY_DIR}/${IMG_FILENAME}${IMG_SUFFIX}.img" 83 | 84 | mv "$INFO_FILE" "$DEPLOY_DIR/" 85 | 86 | if [ "${USE_QCOW2}" = "0" ] && [ "${NO_PRERUN_QCOW2}" = "0" ]; then 87 | ROOT_DEV="$(mount | grep "${ROOTFS_DIR} " | cut -f1 -d' ')" 88 | 89 | unmount "${ROOTFS_DIR}" 90 | zerofree "${ROOT_DEV}" 91 | 92 | unmount_image "${IMG_FILE}" 93 | else 94 | unload_qimage 95 | make_bootable_image "${STAGE_WORK_DIR}/${IMG_FILENAME}${IMG_SUFFIX}.qcow2" "$IMG_FILE" 96 | fi 97 | 98 | if [ "${DEPLOY_ZIP}" == "1" ]; then 99 | pushd "${STAGE_WORK_DIR}" > /dev/null 100 | zip "${DEPLOY_DIR}/${ZIP_FILENAME}${IMG_SUFFIX}.zip" \ 101 | "$(basename "${IMG_FILE}")" 102 | popd > /dev/null 103 | else 104 | mv "$IMG_FILE" "$DEPLOY_DIR/" 105 | fi 106 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/01-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # set hostname 4 | install -v -m 644 files/etc/hostname "${ROOTFS_DIR}/etc/" 5 | install -v -m 644 files/etc/hosts "${ROOTFS_DIR}/etc/" 6 | 7 | # pulseaudio 8 | install -v -m 644 files/etc/systemd/system/pulseaudio.service "${ROOTFS_DIR}/etc/systemd/system/" 9 | install -v -m 644 files/etc/pulse/client.conf "${ROOTFS_DIR}/etc/pulse/" 10 | install -v -m 644 files/etc/pulse/default.pa "${ROOTFS_DIR}/etc/pulse/" 11 | 12 | # pulseaudio first boot volume 13 | install -v -m 644 files/transistor_first_boot "${ROOTFS_DIR}/transistor_first_boot" 14 | 15 | # mpd 16 | install -v -m 644 files/etc/mpd.conf "${ROOTFS_DIR}/etc/" 17 | 18 | # physical interface (aka buttons) 19 | install -v -m 644 files/etc/systemd/system/radio-interface.service "${ROOTFS_DIR}/etc/systemd/system/" 20 | mkdir "${ROOTFS_DIR}/usr/local/lib/radio-interface/" 21 | install -v -m 644 files/usr/local/lib/radio-interface/interface.py "${ROOTFS_DIR}/usr/local/lib/radio-interface/" 22 | install -v -m 644 files/usr/local/lib/radio-interface/audio.py "${ROOTFS_DIR}/usr/local/lib/radio-interface/" 23 | install -v -m 644 files/usr/local/lib/radio-interface/system.py "${ROOTFS_DIR}/usr/local/lib/radio-interface/" 24 | install -v -m 644 files/usr/local/lib/radio-interface/display.py "${ROOTFS_DIR}/usr/local/lib/radio-interface/" 25 | install -v -m 644 files/usr/local/lib/radio-interface/helpers.py "${ROOTFS_DIR}/usr/local/lib/radio-interface/" 26 | install -v -m 644 files/usr/local/lib/radio-interface/sleep-timer.wav "${ROOTFS_DIR}/usr/local/lib/radio-interface/" 27 | 28 | # shutdown daemon (to cut off power completely on shutdown) 29 | mkdir -p "${ROOTFS_DIR}/lib/systemd/system-shutdown/" 30 | install -v -m 755 files/lib/systemd/system-shutdown/gpio-poweroff "${ROOTFS_DIR}/lib/systemd/system-shutdown/" 31 | 32 | # web interface 33 | install -v -m 644 files/etc/systemd/system/ympd.service "${ROOTFS_DIR}/etc/systemd/system/" 34 | git clone https://github.com/pirateradiohack/ympd.git "${ROOTFS_DIR}/root/ympd" 35 | 36 | # radio browser service 37 | mkdir "${ROOTFS_DIR}/usr/local/lib/radio-browser/" 38 | install -v -m 644 files/usr/local/lib/radio-browser/radio-browser.py "${ROOTFS_DIR}/usr/local/lib/radio-browser/" 39 | install -v -m 644 files/etc/systemd/system/radio-browser.service "${ROOTFS_DIR}/etc/systemd/system/" 40 | 41 | # radio settings interface 42 | mkdir "${ROOTFS_DIR}/usr/local/lib/radio-settings/" 43 | cp -r files/usr/local/lib/radio-settings/* "${ROOTFS_DIR}/usr/local/lib/radio-settings/" 44 | install -v -m 644 files/etc/systemd/system/radio-settings.service "${ROOTFS_DIR}/etc/systemd/system/" 45 | install -v -m 644 files/etc/systemd/system/podcasts-updater.service "${ROOTFS_DIR}/etc/systemd/system/" 46 | 47 | # bluetooth 48 | install -v -m 644 files/etc/bluetooth/main.conf "${ROOTFS_DIR}/etc/bluetooth/" 49 | install -v -m 644 files/etc/systemd/system/bluetooth-agent.service "${ROOTFS_DIR}/etc/systemd/system/" 50 | mkdir "${ROOTFS_DIR}/usr/local/sbin/bluetooth-agent/" 51 | install -v -m 644 files/usr/local/sbin/bluetooth-agent/bluezutils.py "${ROOTFS_DIR}/usr/local/sbin/bluetooth-agent/" 52 | install -v -m 755 files/usr/local/sbin/bluetooth-agent/simple-agent "${ROOTFS_DIR}/usr/local/sbin/bluetooth-agent/" 53 | install -v -m 644 files/etc/systemd/system/bluetooth-discovery.service "${ROOTFS_DIR}/etc/systemd/system/" 54 | install -v -m 755 files/usr/local/sbin/bt-discovery "${ROOTFS_DIR}/usr/local/sbin/" 55 | 56 | # wifi setup 57 | install -v -m 600 files/etc/hostapd/hostapd.conf "${ROOTFS_DIR}/etc/hostapd/" 58 | install -v -m 644 files/etc/systemd/system/accesspoint@.service "${ROOTFS_DIR}/etc/systemd/system/" 59 | install -v -m 644 files/etc/wpa_supplicant/wpa_supplicant-wlan0.conf "${ROOTFS_DIR}/etc/wpa_supplicant/" 60 | mkdir "${ROOTFS_DIR}/etc/systemd/system/wpa_supplicant@wlan0.service.d/" 61 | install -v -m 644 files/etc/systemd/system/wpa_supplicant@wlan0.service.d/override.conf "${ROOTFS_DIR}/etc/systemd/system/wpa_supplicant@wlan0.service.d/" 62 | install -v -m 644 files/etc/systemd/network/08-wifi.network "${ROOTFS_DIR}/etc/systemd/network/" 63 | install -v -m 644 files/etc/systemd/network/12-ap.network "${ROOTFS_DIR}/etc/systemd/network/" 64 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-interface/audio.py: -------------------------------------------------------------------------------- 1 | """The class that implements the controller for the audio device.""" 2 | import asyncio 3 | 4 | from gpiozero import MCP3008, Button 5 | 6 | from helpers import ( 7 | connection_to_mpd, 8 | connection_to_pulseaudio, 9 | ) 10 | 11 | VOLUME_STEP = 0.05 12 | MAX_VOLUME = 0.75 13 | POTENTIOMETER_THRESHOLD_TRIGGER = 0.01 14 | 15 | 16 | class Audio: 17 | """Implements the various controls needed in an audio device.""" 18 | 19 | play_pause_button_was_held: bool = False 20 | 21 | def __init__(self) -> None: 22 | self.potentiometer_volume = MCP3008(0) 23 | 24 | def set_volume(self, amount: int) -> None: 25 | """Set audio volume amount in percent.""" 26 | with connection_to_pulseaudio(MAX_VOLUME) as pulse: 27 | pulse['client'].volume_set_all_chans(pulse['sink'], amount / 100) 28 | 29 | def volume_down(self) -> None: 30 | """Volume down button tells pulseaudio to step down the volume.""" 31 | with connection_to_pulseaudio(MAX_VOLUME) as pulse: 32 | pulse["client"].volume_change_all_chans(pulse["sink"], -VOLUME_STEP) 33 | 34 | def volume_up(self) -> None: 35 | """Volume up button tells pulseaudio to step up the volume.""" 36 | with connection_to_pulseaudio(MAX_VOLUME) as pulse: 37 | pulse["client"].volume_change_all_chans(pulse["sink"], +VOLUME_STEP) 38 | 39 | async def volume_knob(self) -> None: 40 | """ 41 | Set the volume according to the potentiometer. 42 | Gpiozero returns a float from 0 to 1 from the potentiometer. 43 | It can be fed directly into pulse audio, 1 being the maximum 44 | volume (above 1 is the soft boost). 45 | 46 | The potentiometer has an electrical charge that varies 47 | enough to constantly trigger a volume change. 48 | We make sure we only trigger the volume change when the 49 | volume knob is actually moved. 50 | That also helps in the use case when the volume is set by 51 | other means, like the web interface, it avoids the 52 | situation where the volume knob would cancel the volume 53 | change by constantly setting it back to its own level. 54 | 55 | The potentiometer value moves roughly by more or less 0.01. 56 | """ 57 | comparison_point = self.potentiometer_volume.value 58 | while True: 59 | knob_movement = ( 60 | abs(self.potentiometer_volume.value - comparison_point) 61 | > POTENTIOMETER_THRESHOLD_TRIGGER 62 | ) 63 | if knob_movement: 64 | volume = self.potentiometer_volume.value 65 | with connection_to_pulseaudio(MAX_VOLUME) as pulse: 66 | pulse["client"].volume_set_all_chans(pulse["sink"], volume) 67 | comparison_point = self.potentiometer_volume.value 68 | await asyncio.sleep(0.1) 69 | 70 | def play(self) -> None: 71 | """Start audio playback.""" 72 | with connection_to_mpd() as mpd: 73 | mpd.play() 74 | 75 | def stop(self) -> None: 76 | """Stop audio playback.""" 77 | with connection_to_mpd() as mpd: 78 | mpd.stop() 79 | 80 | def repeat(self, state: bool) -> None: 81 | """Select repeat mode for the playlist. 82 | 83 | Options for state are: 84 | - True 85 | - False 86 | """ 87 | if state: 88 | state = 1 89 | else: 90 | state = 0 91 | with connection_to_mpd() as mpd: 92 | mpd.repeat(state) 93 | 94 | def play_pause(self) -> None: 95 | """Toggle play/pause.""" 96 | if not Audio.play_pause_button_was_held: 97 | with connection_to_mpd() as mpd: 98 | mpd.pause() 99 | Audio.play_pause_button_was_held = False 100 | 101 | def next(self) -> None: 102 | """Play next track.""" 103 | with connection_to_mpd() as mpd: 104 | mpd.next() 105 | 106 | def previous(self) -> None: 107 | """Play previous track.""" 108 | with connection_to_mpd() as mpd: 109 | mpd.previous() 110 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/content_aggregator/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for content_aggregator project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | PODCASTS_PATH = '/home/transistor/podcasts' 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-bg22t%o3*7m(*3+o2i#b&0fxw-&q-@mb*2m-q_(t2=aghqw*9z' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['*'] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | # My apps 42 | "podcasts.apps.PodcastsConfig", 43 | # Third Party Apps 44 | "django_apscheduler", 45 | ] 46 | 47 | LOGGING = { 48 | "version": 1, 49 | "disable_existing_loggers": False, 50 | "handlers": { 51 | "console": { 52 | "class": "logging.StreamHandler", 53 | }, 54 | }, 55 | "root": { 56 | "handlers": ["console"], 57 | "level": "INFO", 58 | }, 59 | } 60 | 61 | MIDDLEWARE = [ 62 | 'django.middleware.security.SecurityMiddleware', 63 | 'django.contrib.sessions.middleware.SessionMiddleware', 64 | 'django.middleware.common.CommonMiddleware', 65 | 'django.middleware.csrf.CsrfViewMiddleware', 66 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 67 | 'django.contrib.messages.middleware.MessageMiddleware', 68 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 69 | ] 70 | 71 | ROOT_URLCONF = 'content_aggregator.urls' 72 | 73 | TEMPLATES = [ 74 | { 75 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 76 | 'DIRS': [ 77 | BASE_DIR / "templates", 78 | ], 79 | 'APP_DIRS': True, 80 | 'OPTIONS': { 81 | 'context_processors': [ 82 | 'django.template.context_processors.debug', 83 | 'django.template.context_processors.request', 84 | 'django.contrib.auth.context_processors.auth', 85 | 'django.contrib.messages.context_processors.messages', 86 | ], 87 | }, 88 | }, 89 | ] 90 | 91 | WSGI_APPLICATION = 'content_aggregator.wsgi.application' 92 | 93 | 94 | # Database 95 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 96 | 97 | DATABASES = { 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.sqlite3', 100 | 'NAME': BASE_DIR / 'db.sqlite3', 101 | } 102 | } 103 | 104 | 105 | # Password validation 106 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 107 | 108 | AUTH_PASSWORD_VALIDATORS = [ 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 117 | }, 118 | ] 119 | 120 | 121 | # Internationalization 122 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 123 | 124 | LANGUAGE_CODE = 'en-us' 125 | 126 | TIME_ZONE = 'UTC' 127 | 128 | USE_I18N = True 129 | 130 | USE_L10N = True 131 | 132 | USE_TZ = True 133 | 134 | 135 | # Static files (CSS, JavaScript, Images) 136 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 137 | 138 | STATIC_URL = '/static/' 139 | 140 | # Default primary key field type 141 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 142 | 143 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 144 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/podcasts/management/commands/startjobs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import feedparser 4 | from apscheduler.schedulers.blocking import BlockingScheduler 5 | from apscheduler.triggers.cron import CronTrigger 6 | from dateutil import parser 7 | from django.conf import settings 8 | from django.core.management.base import BaseCommand 9 | from django_apscheduler.jobstores import DjangoJobStore 10 | from django_apscheduler.models import DjangoJobExecution 11 | from podcasts.models import Episode, Feed 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def get_new_episodes(): 17 | """Saves new episodes in all feeds to the database. 18 | 19 | Checks the episode's URL against the episodes currently stored in the 20 | database. If not found, then a new `Episode` is added to the database. 21 | 22 | When an episode is saved in the model, its corresponding audio file is 23 | downloaded and its path is stored in the database for use in further 24 | deletion. 25 | 26 | We save only episodes with audio enclosures. 27 | """ 28 | feeds = Feed.objects.all() 29 | for feed in feeds: 30 | feed.save() # update feed metadata in model 31 | episodes = feedparser.parse(feed.url).entries 32 | episodes = [episode for episode in episodes if 33 | _is_audio_episode(episode)] 34 | episodes = episodes[: feed.max_entries] 35 | for episode in episodes: 36 | if not Episode.objects.filter(link=episode.link).exists(): 37 | audio = "" 38 | for enclosure in episode.enclosures: 39 | if "audio" in enclosure.get("type"): 40 | audio = enclosure.get("href") 41 | episode_in_db = Episode( 42 | title=episode.title, 43 | pub_date=parser.parse(episode.published), 44 | link=episode.link, 45 | feed=feed, 46 | audio=audio, 47 | ) 48 | episode_in_db.save() 49 | 50 | 51 | def _is_audio_episode(episode): 52 | """Return True when at least one of the enclosures is of type audio.""" 53 | audio_enclosures = [enclosure for enclosure in episode.enclosures if 54 | "audio" in enclosure.get("type")] 55 | return audio_enclosures != [] 56 | 57 | 58 | def trim_old_episodes(): 59 | """Remove old episodes from a feed.""" 60 | feeds = Feed.objects.all() 61 | for feed in feeds: 62 | episodes = Episode.objects.filter(feed=feed).order_by("-pub_date") 63 | if len(episodes) > feed.max_entries: 64 | keep = episodes[: feed.max_entries] 65 | Episode.objects.filter(feed=feed).exclude( 66 | pk__in=[item.id for item in keep] 67 | ).delete() 68 | 69 | 70 | def delete_old_job_executions(max_age=604_800): 71 | """Deletes all apscheduler job execution logs older than `max_age`.""" 72 | DjangoJobExecution.objects.delete_old_job_executions(max_age) 73 | 74 | 75 | class Command(BaseCommand): 76 | help = "Runs apscheduler." 77 | 78 | def handle(self, *args, **options): 79 | scheduler = BlockingScheduler(timezone=settings.TIME_ZONE) 80 | scheduler.add_jobstore(DjangoJobStore(), "default") 81 | 82 | scheduler.add_job( 83 | delete_old_job_executions, 84 | trigger=CronTrigger( 85 | day_of_week="mon", hour="00", minute="00" 86 | ), # Midnight on Monday, before start of the next work week. 87 | id="Delete Old Job Executions", 88 | max_instances=1, 89 | replace_existing=True, 90 | ) 91 | logger.info("Added weekly job: Delete Old Job Executions.") 92 | 93 | scheduler.add_job( 94 | get_new_episodes, 95 | trigger="interval", 96 | minutes=30, 97 | id="episode updater", 98 | max_instances=1, 99 | replace_existing=True, 100 | ) 101 | logger.info("Added job: Episode Updater.") 102 | 103 | scheduler.add_job( 104 | trim_old_episodes, 105 | trigger="interval", 106 | minutes=90, 107 | id="episode deleter", 108 | max_instances=1, 109 | replace_existing=True, 110 | ) 111 | logger.info("Added job: Episode Deleter.") 112 | 113 | try: 114 | logger.info("Starting scheduler...") 115 | scheduler.start() 116 | except KeyboardInterrupt: 117 | logger.info("Stopping scheduler...") 118 | scheduler.shutdown() 119 | logger.info("Scheduler shut down successfully!") 120 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/bluetooth/main.conf: -------------------------------------------------------------------------------- 1 | [General] 2 | 3 | # Defaults to 'BlueZ X.YZ', if Name is not set here and plugin 'hostname' is not loaded. 4 | # The plugin 'hostname' is loaded by default and overides the Name set here so 5 | # consider modifying /etc/machine-info with variable PRETTY_HOSTNAME= instead. 6 | #Name = BlueZ 7 | 8 | # Default device class. Only the major and minor device class bits are 9 | # considered. Defaults to '0x000000'. 10 | #Class = 0x000100 11 | Class = 0x41C 12 | 13 | # How long to stay in discoverable mode before going back to non-discoverable 14 | # The value is in seconds. Default is 180, i.e. 3 minutes. 15 | # 0 = disable timer, i.e. stay discoverable forever 16 | DiscoverableTimeout = 0 17 | 18 | # How long to stay in pairable mode before going back to non-discoverable 19 | # The value is in seconds. Default is 0. 20 | # 0 = disable timer, i.e. stay pairable forever 21 | #PairableTimeout = 0 22 | 23 | # Use vendor id source (assigner), vendor, product and version information for 24 | # DID profile support. The values are separated by ":" and assigner, VID, PID 25 | # and version. 26 | # Possible vendor id source values: bluetooth, usb (defaults to usb) 27 | #DeviceID = bluetooth:1234:5678:abcd 28 | 29 | # Do reverse service discovery for previously unknown devices that connect to 30 | # us. This option is really only needed for qualification since the BITE tester 31 | # doesn't like us doing reverse SDP for some test cases (though there could in 32 | # theory be other useful purposes for this too). Defaults to 'true'. 33 | #ReverseServiceDiscovery = true 34 | 35 | # Enable name resolving after inquiry. Set it to 'false' if you don't need 36 | # remote devices name and want shorter discovery cycle. Defaults to 'true'. 37 | #NameResolving = true 38 | 39 | # Enable runtime persistency of debug link keys. Default is false which 40 | # makes debug link keys valid only for the duration of the connection 41 | # that they were created for. 42 | #DebugKeys = false 43 | 44 | # Restricts all controllers to the specified transport. Default value 45 | # is "dual", i.e. both BR/EDR and LE enabled (when supported by the HW). 46 | # Possible values: "dual", "bredr", "le" 47 | #ControllerMode = dual 48 | 49 | # Enables Multi Profile Specification support. This allows to specify if 50 | # system supports only Multiple Profiles Single Device (MPSD) configuration 51 | # or both Multiple Profiles Single Device (MPSD) and Multiple Profiles Multiple 52 | # Devices (MPMD) configurations. 53 | # Possible values: "off", "single", "multiple" 54 | #MultiProfile = off 55 | 56 | # Permanently enables the Fast Connectable setting for adapters that 57 | # support it. When enabled other devices can connect faster to us, 58 | # however the tradeoff is increased power consumptions. This feature 59 | # will fully work only on kernel version 4.1 and newer. Defaults to 60 | # 'false'. 61 | #FastConnectable = false 62 | 63 | # Default privacy setting. 64 | # Enables use of private address. 65 | # Possible values: "off", "device", "network" 66 | # "network" option not supported currently 67 | # Defaults to "off" 68 | # Privacy = off 69 | 70 | [GATT] 71 | # GATT attribute cache. 72 | # Possible values: 73 | # always: Always cache attributes even for devices not paired, this is 74 | # recommended as it is best for interoperability, with more consistent 75 | # reconnection times and enables proper tracking of notifications for all 76 | # devices. 77 | # yes: Only cache attributes of paired devices. 78 | # no: Never cache attributes 79 | # Default: always 80 | #Cache = always 81 | 82 | # Minimum required Encryption Key Size for accessing secured characteristics. 83 | # Possible values: 0 and 7-16. 0 means don't care. 84 | # Defaults to 0 85 | # MinEncKeySize = 0 86 | 87 | [Policy] 88 | # 89 | # The ReconnectUUIDs defines the set of remote services that should try 90 | # to be reconnected to in case of a link loss (link supervision 91 | # timeout). The policy plugin should contain a sane set of values by 92 | # default, but this list can be overridden here. By setting the list to 93 | # empty the reconnection feature gets disabled. 94 | #ReconnectUUIDs=00001112-0000-1000-8000-00805f9b34fb,0000111f-0000-1000-8000-00805f9b34fb,0000110a-0000-1000-8000-00805f9b34fb 95 | 96 | # ReconnectAttempts define the number of attempts to reconnect after a link 97 | # lost. Setting the value to 0 disables reconnecting feature. 98 | #ReconnectAttempts=7 99 | 100 | # ReconnectIntervals define the set of intervals in seconds to use in between 101 | # attempts. 102 | # If the number of attempts defined in ReconnectAttempts is bigger than the 103 | # set of intervals the last interval is repeated until the last attempt. 104 | #ReconnectIntervals=1,2,4,8,16,32,64 105 | 106 | # AutoEnable defines option to enable all controllers when they are found. 107 | # This includes adapters present on start as well as adapters that are plugged 108 | # in later on. Defaults to 'false'. 109 | AutoEnable=true 110 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/pulse/default.pa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/pulseaudio -nF 2 | # 3 | # This file is part of PulseAudio. 4 | # 5 | # PulseAudio is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PulseAudio is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with PulseAudio; if not, see . 17 | 18 | # This startup script is used only if PulseAudio is started per-user 19 | # (i.e. not in system mode) 20 | 21 | .fail 22 | 23 | ### Automatically restore the volume of streams and devices 24 | load-module module-device-restore 25 | load-module module-stream-restore 26 | load-module module-card-restore 27 | 28 | ### Automatically augment property information from .desktop files 29 | ### stored in /usr/share/application 30 | load-module module-augment-properties 31 | 32 | ### Should be after module-*-restore but before module-*-detect 33 | load-module module-switch-on-port-available 34 | 35 | ### Load audio drivers statically 36 | ### (it's probably better to not load these drivers manually, but instead 37 | ### use module-udev-detect -- see below -- for doing this automatically) 38 | #load-module module-alsa-sink 39 | #load-module module-alsa-source device=hw:1,0 40 | #load-module module-oss device="/dev/dsp" sink_name=output source_name=input 41 | #load-module module-oss-mmap device="/dev/dsp" sink_name=output source_name=input 42 | #load-module module-null-sink 43 | #load-module module-pipe-sink 44 | 45 | ### Try to load pivumeter if it exists 46 | .nofail 47 | load-module module-alsa-sink device=pivumeter 48 | .fail 49 | 50 | ### Automatically load driver modules depending on the hardware available 51 | .ifexists module-udev-detect.so 52 | load-module module-udev-detect 53 | .else 54 | ### Use the static hardware detection module (for systems that lack udev support) 55 | load-module module-detect 56 | .endif 57 | 58 | ### Automatically connect sink and source if JACK server is present 59 | .ifexists module-jackdbus-detect.so 60 | .nofail 61 | load-module module-jackdbus-detect channels=2 62 | .fail 63 | .endif 64 | 65 | ### Automatically load driver modules for Bluetooth hardware 66 | .ifexists module-bluetooth-policy.so 67 | load-module module-bluetooth-policy 68 | .endif 69 | 70 | .ifexists module-bluetooth-discover.so 71 | load-module module-bluetooth-discover 72 | .endif 73 | 74 | ### Load several protocols 75 | .ifexists module-esound-protocol-unix.so 76 | load-module module-esound-protocol-unix 77 | .endif 78 | load-module module-native-protocol-unix auth-anonymous=1 socket=/tmp/pulseaudio.socket 79 | 80 | ### Network access (may be configured with paprefs, so leave this commented 81 | ### here if you plan to use paprefs) 82 | #load-module module-esound-protocol-tcp 83 | load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1 84 | #load-module module-zeroconf-publish 85 | 86 | ### Load the RTP receiver module (also configured via paprefs, see above) 87 | #load-module module-rtp-recv 88 | 89 | ### Load the RTP sender module (also configured via paprefs, see above) 90 | #load-module module-null-sink sink_name=rtp format=s16be channels=2 rate=44100 sink_properties="device.description='RTP Multicast Sink'" 91 | #load-module module-rtp-send source=rtp.monitor 92 | 93 | ### Load additional modules from GConf settings. This can be configured with the paprefs tool. 94 | ### Please keep in mind that the modules configured by paprefs might conflict with manually 95 | ### loaded modules. 96 | .ifexists module-gconf.so 97 | .nofail 98 | load-module module-gconf 99 | .fail 100 | .endif 101 | 102 | ### Automatically restore the default sink/source when changed by the user 103 | ### during runtime 104 | ### NOTE: This should be loaded as early as possible so that subsequent modules 105 | ### that look up the default sink/source get the right value 106 | load-module module-default-device-restore 107 | 108 | ### Automatically move streams to the default sink if the sink they are 109 | ### connected to dies, similar for sources 110 | load-module module-rescue-streams 111 | 112 | ### Make sure we always have a sink around, even if it is a null sink. 113 | load-module module-always-sink 114 | 115 | ### Honour intended role device property 116 | load-module module-intended-roles 117 | 118 | ### Automatically suspend sinks/sources that become idle for too long 119 | load-module module-suspend-on-idle timeout=604800 120 | 121 | ### If autoexit on idle is enabled we want to make sure we only quit 122 | ### when no local session needs us anymore. 123 | .ifexists module-console-kit.so 124 | load-module module-console-kit 125 | .endif 126 | .ifexists module-systemd-login.so 127 | load-module module-systemd-login 128 | .endif 129 | 130 | ### Enable positioned event sounds 131 | load-module module-position-event-sounds 132 | 133 | ### Cork music/video streams when a phone stream is active 134 | load-module module-role-cork 135 | 136 | ### Modules to allow autoloading of filters (such as echo cancellation) 137 | ### on demand. module-filter-heuristics tries to determine what filters 138 | ### make sense, and module-filter-apply does the heavy-lifting of 139 | ### loading modules and rerouting streams. 140 | load-module module-filter-heuristics 141 | load-module module-filter-apply 142 | 143 | ### Make some devices default 144 | #set-default-sink output 145 | #set-default-source input 146 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/sbin/bluetooth-agent/simple-agent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | from optparse import OptionParser 6 | 7 | import dbus 8 | import dbus.mainloop.glib 9 | import dbus.service 10 | 11 | try: 12 | from gi.repository import GObject 13 | except ImportError: 14 | import gobject as GObject 15 | 16 | import bluezutils 17 | 18 | BUS_NAME = "org.bluez" 19 | AGENT_INTERFACE = "org.bluez.Agent1" 20 | AGENT_PATH = "/test/agent" 21 | 22 | bus = None 23 | device_obj = None 24 | dev_path = None 25 | 26 | 27 | def ask(prompt): 28 | try: 29 | return raw_input(prompt) 30 | except: 31 | return input(prompt) 32 | 33 | 34 | def set_trusted(path): 35 | props = dbus.Interface( 36 | bus.get_object("org.bluez", path), "org.freedesktop.DBus.Properties" 37 | ) 38 | props.Set("org.bluez.Device1", "Trusted", True) 39 | 40 | 41 | def dev_connect(path): 42 | dev = dbus.Interface(bus.get_object("org.bluez", path), "org.bluez.Device1") 43 | dev.Connect() 44 | 45 | 46 | class Rejected(dbus.DBusException): 47 | _dbus_error_name = "org.bluez.Error.Rejected" 48 | 49 | 50 | class Agent(dbus.service.Object): 51 | exit_on_release = True 52 | 53 | def set_exit_on_release(self, exit_on_release): 54 | self.exit_on_release = exit_on_release 55 | 56 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="") 57 | def Release(self): 58 | print("Release") 59 | if self.exit_on_release: 60 | mainloop.quit() 61 | 62 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="") 63 | def AuthorizeService(self, device, uuid): 64 | print("AuthorizeService (%s, %s)" % (device, uuid)) 65 | return 66 | 67 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="s") 68 | def RequestPinCode(self, device): 69 | print("RequestPinCode (%s)" % (device)) 70 | set_trusted(device) 71 | return ask("Enter PIN Code: ") 72 | 73 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="u") 74 | def RequestPasskey(self, device): 75 | print("RequestPasskey (%s)" % (device)) 76 | set_trusted(device) 77 | passkey = ask("Enter passkey: ") 78 | return dbus.UInt32(passkey) 79 | 80 | @dbus.service.method(AGENT_INTERFACE, in_signature="ouq", out_signature="") 81 | def DisplayPasskey(self, device, passkey, entered): 82 | print("DisplayPasskey (%s, %06u entered %u)" % (device, passkey, entered)) 83 | 84 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="") 85 | def DisplayPinCode(self, device, pincode): 86 | print("DisplayPinCode (%s, %s)" % (device, pincode)) 87 | 88 | @dbus.service.method(AGENT_INTERFACE, in_signature="ou", out_signature="") 89 | def RequestConfirmation(self, device, passkey): 90 | print("RequestConfirmation (%s, %06d)" % (device, passkey)) 91 | confirm = ask("Confirm passkey (yes/no): ") 92 | if confirm == "yes": 93 | set_trusted(device) 94 | return 95 | raise Rejected("Passkey doesn't match") 96 | 97 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="") 98 | def RequestAuthorization(self, device): 99 | print("RequestAuthorization (%s)" % (device)) 100 | return 101 | 102 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="") 103 | def Cancel(self): 104 | print("Cancel") 105 | 106 | 107 | def pair_reply(): 108 | print("Device paired") 109 | set_trusted(dev_path) 110 | dev_connect(dev_path) 111 | mainloop.quit() 112 | 113 | 114 | def pair_error(error): 115 | err_name = error.get_dbus_name() 116 | if err_name == "org.freedesktop.DBus.Error.NoReply" and device_obj: 117 | print("Timed out. Cancelling pairing") 118 | device_obj.CancelPairing() 119 | else: 120 | print("Creating device failed: %s" % (error)) 121 | 122 | mainloop.quit() 123 | 124 | 125 | if __name__ == "__main__": 126 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 127 | 128 | bus = dbus.SystemBus() 129 | 130 | capability = "KeyboardDisplay" 131 | 132 | parser = OptionParser() 133 | parser.add_option( 134 | "-i", 135 | "--adapter", 136 | action="store", 137 | type="string", 138 | dest="adapter_pattern", 139 | default=None, 140 | ) 141 | parser.add_option( 142 | "-c", "--capability", action="store", type="string", dest="capability" 143 | ) 144 | parser.add_option( 145 | "-t", "--timeout", action="store", type="int", dest="timeout", default=60000 146 | ) 147 | (options, args) = parser.parse_args() 148 | if options.capability: 149 | capability = options.capability 150 | 151 | path = "/test/agent" 152 | agent = Agent(bus, path) 153 | 154 | mainloop = GObject.MainLoop() 155 | 156 | obj = bus.get_object(BUS_NAME, "/org/bluez") 157 | manager = dbus.Interface(obj, "org.bluez.AgentManager1") 158 | manager.RegisterAgent(path, capability) 159 | 160 | print("Agent registered") 161 | 162 | # Fix-up old style invocation (BlueZ 4) 163 | if len(args) > 0 and args[0].startswith("hci"): 164 | options.adapter_pattern = args[0] 165 | del args[:1] 166 | 167 | if len(args) > 0: 168 | device = bluezutils.find_device(args[0], options.adapter_pattern) 169 | dev_path = device.object_path 170 | agent.set_exit_on_release(False) 171 | device.Pair(reply_handler=pair_reply, error_handler=pair_error, timeout=60000) 172 | device_obj = device 173 | else: 174 | manager.RequestDefaultAgent(path) 175 | 176 | mainloop.run() 177 | 178 | # adapter.UnregisterAgent(path) 179 | # print("Agent unregistered") 180 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/usr/local/lib/radio-settings/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "35a2dc7a34d4aa9ff11781ac718a47215f4df750cbf9797f4148b91dac30af3f" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "apscheduler": { 20 | "hashes": [ 21 | "sha256:5cf344ebcfbdaa48ae178c029c055cec7bc7a4a47c21e315e4d1f08bd35f2355", 22 | "sha256:c22cb14b411a31435eb2c530dfbbec948ac63015b517087c7978adb61b574865" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 25 | "version": "==3.8.1" 26 | }, 27 | "asgiref": { 28 | "hashes": [ 29 | "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", 30 | "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214" 31 | ], 32 | "markers": "python_version >= '3.6'", 33 | "version": "==3.4.1" 34 | }, 35 | "certifi": { 36 | "hashes": [ 37 | "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", 38 | "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" 39 | ], 40 | "version": "==2021.10.8" 41 | }, 42 | "charset-normalizer": { 43 | "hashes": [ 44 | "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", 45 | "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" 46 | ], 47 | "markers": "python_version >= '3'", 48 | "version": "==2.0.9" 49 | }, 50 | "django": { 51 | "hashes": [ 52 | "sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13", 53 | "sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022" 54 | ], 55 | "index": "pypi", 56 | "version": "==3.2.6" 57 | }, 58 | "django-apscheduler": { 59 | "hashes": [ 60 | "sha256:55cba8d087d358102751df71efdd209c3c8230eb69419af76858eda2f6e94fb1", 61 | "sha256:9399ddcdb049eeb8f8ac8cca79e176d0b25a3cc51e034de1381375d6046c6113" 62 | ], 63 | "index": "pypi", 64 | "version": "==0.6.0" 65 | }, 66 | "feedparser": { 67 | "hashes": [ 68 | "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a", 69 | "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661" 70 | ], 71 | "index": "pypi", 72 | "version": "==6.0.8" 73 | }, 74 | "idna": { 75 | "hashes": [ 76 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 77 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 78 | ], 79 | "markers": "python_version >= '3'", 80 | "version": "==3.3" 81 | }, 82 | "python-dateutil": { 83 | "hashes": [ 84 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 85 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 86 | ], 87 | "index": "pypi", 88 | "version": "==2.8.2" 89 | }, 90 | "pytz": { 91 | "hashes": [ 92 | "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", 93 | "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" 94 | ], 95 | "version": "==2021.3" 96 | }, 97 | "pytz-deprecation-shim": { 98 | "hashes": [ 99 | "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6", 100 | "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d" 101 | ], 102 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 103 | "version": "==0.1.0.post0" 104 | }, 105 | "requests": { 106 | "hashes": [ 107 | "sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5", 108 | "sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df" 109 | ], 110 | "index": "pypi", 111 | "version": "==2.27.0" 112 | }, 113 | "setuptools": { 114 | "hashes": [ 115 | "sha256:5c89b1a14a67ac5f0956f1cb0aeb7d1d3f4c8ba4e4e1ab7bf1af4933f9a2f0fe", 116 | "sha256:675fcebecb43c32eb930481abf907619137547f4336206e4d673180242e1a278" 117 | ], 118 | "markers": "python_version >= '3.7'", 119 | "version": "==60.2.0" 120 | }, 121 | "sgmllib3k": { 122 | "hashes": [ 123 | "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9" 124 | ], 125 | "version": "==1.0.0" 126 | }, 127 | "six": { 128 | "hashes": [ 129 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 130 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 131 | ], 132 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 133 | "version": "==1.16.0" 134 | }, 135 | "sqlparse": { 136 | "hashes": [ 137 | "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", 138 | "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" 139 | ], 140 | "markers": "python_version >= '3.5'", 141 | "version": "==0.4.2" 142 | }, 143 | "tzdata": { 144 | "hashes": [ 145 | "sha256:3eee491e22ebfe1e5cfcc97a4137cd70f092ce59144d81f8924a844de05ba8f5", 146 | "sha256:68dbe41afd01b867894bbdfd54fa03f468cfa4f0086bfb4adcd8de8f24f3ee21" 147 | ], 148 | "markers": "python_version >= '3.6'", 149 | "version": "==2021.5" 150 | }, 151 | "tzlocal": { 152 | "hashes": [ 153 | "sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09", 154 | "sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f" 155 | ], 156 | "markers": "python_version >= '3.6'", 157 | "version": "==4.1" 158 | }, 159 | "urllib3": { 160 | "hashes": [ 161 | "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", 162 | "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" 163 | ], 164 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 165 | "version": "==1.26.7" 166 | } 167 | }, 168 | "develop": {} 169 | } 170 | -------------------------------------------------------------------------------- /stage0/01-locale/00-debconf: -------------------------------------------------------------------------------- 1 | # Locales to be generated: 2 | # Choices: All locales, aa_DJ ISO-8859-1, aa_DJ.UTF-8 UTF-8, aa_ER UTF-8, aa_ER@saaho UTF-8, aa_ET UTF-8, af_ZA ISO-8859-1, af_ZA.UTF-8 UTF-8, ak_GH UTF-8, am_ET UTF-8, an_ES ISO-8859-15, an_ES.UTF-8 UTF-8, anp_IN UTF-8, ar_AE ISO-8859-6, ar_AE.UTF-8 UTF-8, ar_BH ISO-8859-6, ar_BH.UTF-8 UTF-8, ar_DZ ISO-8859-6, ar_DZ.UTF-8 UTF-8, ar_EG ISO-8859-6, ar_EG.UTF-8 UTF-8, ar_IN UTF-8, ar_IQ ISO-8859-6, ar_IQ.UTF-8 UTF-8, ar_JO ISO-8859-6, ar_JO.UTF-8 UTF-8, ar_KW ISO-8859-6, ar_KW.UTF-8 UTF-8, ar_LB ISO-8859-6, ar_LB.UTF-8 UTF-8, ar_LY ISO-8859-6, ar_LY.UTF-8 UTF-8, ar_MA ISO-8859-6, ar_MA.UTF-8 UTF-8, ar_OM ISO-8859-6, ar_OM.UTF-8 UTF-8, ar_QA ISO-8859-6, ar_QA.UTF-8 UTF-8, ar_SA ISO-8859-6, ar_SA.UTF-8 UTF-8, ar_SD ISO-8859-6, ar_SD.UTF-8 UTF-8, ar_SS UTF-8, ar_SY ISO-8859-6, ar_SY.UTF-8 UTF-8, ar_TN ISO-8859-6, ar_TN.UTF-8 UTF-8, ar_YE ISO-8859-6, ar_YE.UTF-8 UTF-8, as_IN UTF-8, ast_ES ISO-8859-15, ast_ES.UTF-8 UTF-8, ayc_PE UTF-8, az_AZ UTF-8, be_BY CP1251, be_BY.UTF-8 UTF-8, be_BY@latin UTF-8, bem_ZM UTF-8, ber_DZ UTF-8, ber_MA UTF-8, bg_BG CP1251, bg_BG.UTF-8 UTF-8, bho_IN UTF-8, bn_BD UTF-8, bn_IN UTF-8, bo_CN UTF-8, bo_IN UTF-8, br_FR ISO-8859-1, br_FR.UTF-8 UTF-8, br_FR@euro ISO-8859-15, brx_IN UTF-8, bs_BA ISO-8859-2, bs_BA.UTF-8 UTF-8, byn_ER UTF-8, ca_AD ISO-8859-15, ca_AD.UTF-8 UTF-8, ca_ES ISO-8859-1, ca_ES.UTF-8 UTF-8, ca_ES.UTF-8@valencia UTF-8, ca_ES@euro ISO-8859-15, ca_ES@valencia ISO-8859-15, ca_FR ISO-8859-15, ca_FR.UTF-8 UTF-8, ca_IT ISO-8859-15, ca_IT.UTF-8 UTF-8, cmn_TW UTF-8, crh_UA UTF-8, cs_CZ ISO-8859-2, cs_CZ.UTF-8 UTF-8, csb_PL UTF-8, cv_RU UTF-8, cy_GB ISO-8859-14, cy_GB.UTF-8 UTF-8, da_DK ISO-8859-1, da_DK.UTF-8 UTF-8, de_AT ISO-8859-1, de_AT.UTF-8 UTF-8, de_AT@euro ISO-8859-15, de_BE ISO-8859-1, de_BE.UTF-8 UTF-8, de_BE@euro ISO-8859-15, de_CH ISO-8859-1, de_CH.UTF-8 UTF-8, de_DE ISO-8859-1, de_DE.UTF-8 UTF-8, de_DE@euro ISO-8859-15, de_LI.UTF-8 UTF-8, de_LU ISO-8859-1, de_LU.UTF-8 UTF-8, de_LU@euro ISO-8859-15, doi_IN UTF-8, dv_MV UTF-8, dz_BT UTF-8, el_CY ISO-8859-7, el_CY.UTF-8 UTF-8, el_GR ISO-8859-7, el_GR.UTF-8 UTF-8, en_AG UTF-8, en_AU ISO-8859-1, en_AU.UTF-8 UTF-8, en_BW ISO-8859-1, en_BW.UTF-8 UTF-8, en_CA ISO-8859-1, en_CA.UTF-8 UTF-8, en_DK ISO-8859-1, en_DK.ISO-8859-15 ISO-8859-15, en_DK.UTF-8 UTF-8, en_GB ISO-8859-1, en_GB.ISO-8859-15 ISO-8859-15, en_GB.UTF-8 UTF-8, en_HK ISO-8859-1, en_HK.UTF-8 UTF-8, en_IE ISO-8859-1, en_IE.UTF-8 UTF-8, en_IE@euro ISO-8859-15, en_IN UTF-8, en_NG UTF-8, en_NZ ISO-8859-1, en_NZ.UTF-8 UTF-8, en_PH ISO-8859-1, en_PH.UTF-8 UTF-8, en_SG ISO-8859-1, en_SG.UTF-8 UTF-8, en_US ISO-8859-1, en_US.ISO-8859-15 ISO-8859-15, en_US.UTF-8 UTF-8, en_ZA ISO-8859-1, en_ZA.UTF-8 UTF-8, en_ZM UTF-8, en_ZW ISO-8859-1, en_ZW.UTF-8 UTF-8, eo ISO-8859-3, eo.UTF-8 UTF-8, es_AR ISO-8859-1, es_AR.UTF-8 UTF-8, es_BO ISO-8859-1, es_BO.UTF-8 UTF-8, es_CL ISO-8859-1, es_CL.UTF-8 UTF-8, es_CO ISO-8859-1, es_CO.UTF-8 UTF-8, es_CR ISO-8859-1, es_CR.UTF-8 UTF-8, es_CU UTF-8, es_DO ISO-8859-1, es_DO.UTF-8 UTF-8, es_EC ISO-8859-1, es_EC.UTF-8 UTF-8, es_ES ISO-8859-1, es_ES.UTF-8 UTF-8, es_ES@euro ISO-8859-15, es_GT ISO-8859-1, es_GT.UTF-8 UTF-8, es_HN ISO-8859-1, es_HN.UTF-8 UTF-8, es_MX ISO-8859-1, es_MX.UTF-8 UTF-8, es_NI ISO-8859-1, es_NI.UTF-8 UTF-8, es_PA ISO-8859-1, es_PA.UTF-8 UTF-8, es_PE ISO-8859-1, es_PE.UTF-8 UTF-8, es_PR ISO-8859-1, es_PR.UTF-8 UTF-8, es_PY ISO-8859-1, es_PY.UTF-8 UTF-8, es_SV ISO-8859-1, es_SV.UTF-8 UTF-8, es_US ISO-8859-1, es_US.UTF-8 UTF-8, es_UY ISO-8859-1, es_UY.UTF-8 UTF-8, es_VE ISO-8859-1, es_VE.UTF-8 UTF-8, et_EE ISO-8859-1, et_EE.ISO-8859-15 ISO-8859-15, et_EE.UTF-8 UTF-8, eu_ES ISO-8859-1, eu_ES.UTF-8 UTF-8, eu_ES@euro ISO-8859-15, eu_FR ISO-8859-1, eu_FR.UTF-8 UTF-8, eu_FR@euro ISO-8859-15, fa_IR UTF-8, ff_SN UTF-8, fi_FI ISO-8859-1, fi_FI.UTF-8 UTF-8, fi_FI@euro ISO-8859-15, fil_PH UTF-8, fo_FO ISO-8859-1, fo_FO.UTF-8 UTF-8, fr_BE ISO-8859-1, fr_BE.UTF-8 UTF-8, fr_BE@euro ISO-8859-15, fr_CA ISO-8859-1, fr_CA.UTF-8 UTF-8, fr_CH ISO-8859-1, fr_CH.UTF-8 UTF-8, fr_FR ISO-8859-1, fr_FR.UTF-8 UTF-8, fr_FR@euro ISO-8859-15, fr_LU ISO-8859-1, fr_LU.UTF-8 UTF-8, fr_LU@euro ISO-8859-15, fur_IT UTF-8, fy_DE UTF-8, fy_NL UTF-8, ga_IE ISO-8859-1, ga_IE.UTF-8 UTF-8, ga_IE@euro ISO-8859-15, gd_GB ISO-8859-15, gd_GB.UTF-8 UTF-8, gez_ER UTF-8, gez_ER@abegede UTF-8, gez_ET UTF-8, gez_ET@abegede UTF-8, gl_ES ISO-8859-1, gl_ES.UTF-8 UTF-8, gl_ES@euro ISO-8859-15, gu_IN UTF-8, gv_GB ISO-8859-1, gv_GB.UTF-8 UTF-8, ha_NG UTF-8, hak_TW UTF-8, he_IL ISO-8859-8, he_IL.UTF-8 UTF-8, hi_IN UTF-8, hne_IN UTF-8, hr_HR ISO-8859-2, hr_HR.UTF-8 UTF-8, hsb_DE ISO-8859-2, hsb_DE.UTF-8 UTF-8, ht_HT UTF-8, hu_HU ISO-8859-2, hu_HU.UTF-8 UTF-8, hy_AM UTF-8, hy_AM.ARMSCII-8 ARMSCII-8, ia_FR UTF-8, id_ID ISO-8859-1, id_ID.UTF-8 UTF-8, ig_NG UTF-8, ik_CA UTF-8, is_IS ISO-8859-1, is_IS.UTF-8 UTF-8, it_CH ISO-8859-1, it_CH.UTF-8 UTF-8, it_IT ISO-8859-1, it_IT.UTF-8 UTF-8, it_IT@euro ISO-8859-15, iu_CA UTF-8, iw_IL ISO-8859-8, iw_IL.UTF-8 UTF-8, ja_JP.EUC-JP EUC-JP, ja_JP.UTF-8 UTF-8, ka_GE GEORGIAN-PS, ka_GE.UTF-8 UTF-8, kk_KZ PT154, kk_KZ RK1048, kk_KZ.UTF-8 UTF-8, kl_GL ISO-8859-1, kl_GL.UTF-8 UTF-8, km_KH UTF-8, kn_IN UTF-8, ko_KR.EUC-KR EUC-KR, ko_KR.UTF-8 UTF-8, kok_IN UTF-8, ks_IN UTF-8, ks_IN@devanagari UTF-8, ku_TR ISO-8859-9, ku_TR.UTF-8 UTF-8, kw_GB ISO-8859-1, kw_GB.UTF-8 UTF-8, ky_KG UTF-8, lb_LU UTF-8, lg_UG ISO-8859-10, lg_UG.UTF-8 UTF-8, li_BE UTF-8, li_NL UTF-8, lij_IT UTF-8, lo_LA UTF-8, lt_LT ISO-8859-13, lt_LT.UTF-8 UTF-8, lv_LV ISO-8859-13, lv_LV.UTF-8 UTF-8, lzh_TW UTF-8, mag_IN UTF-8, mai_IN UTF-8, mg_MG ISO-8859-15, mg_MG.UTF-8 UTF-8, mhr_RU UTF-8, mi_NZ ISO-8859-13, mi_NZ.UTF-8 UTF-8, mk_MK ISO-8859-5, mk_MK.UTF-8 UTF-8, ml_IN UTF-8, mn_MN UTF-8, mni_IN UTF-8, mr_IN UTF-8, ms_MY ISO-8859-1, ms_MY.UTF-8 UTF-8, mt_MT ISO-8859-3, mt_MT.UTF-8 UTF-8, my_MM UTF-8, nan_TW UTF-8, nan_TW@latin UTF-8, nb_NO ISO-8859-1, nb_NO.UTF-8 UTF-8, nds_DE UTF-8, nds_NL UTF-8, ne_NP UTF-8, nhn_MX UTF-8, niu_NU UTF-8, niu_NZ UTF-8, nl_AW UTF-8, nl_BE ISO-8859-1, nl_BE.UTF-8 UTF-8, nl_BE@euro ISO-8859-15, nl_NL ISO-8859-1, nl_NL.UTF-8 UTF-8, nl_NL@euro ISO-8859-15, nn_NO ISO-8859-1, nn_NO.UTF-8 UTF-8, nr_ZA UTF-8, nso_ZA UTF-8, oc_FR ISO-8859-1, oc_FR.UTF-8 UTF-8, om_ET UTF-8, om_KE ISO-8859-1, om_KE.UTF-8 UTF-8, or_IN UTF-8, os_RU UTF-8, pa_IN UTF-8, pa_PK UTF-8, pap_AN UTF-8, pap_AW UTF-8, pap_CW UTF-8, pl_PL ISO-8859-2, pl_PL.UTF-8 UTF-8, ps_AF UTF-8, pt_BR ISO-8859-1, pt_BR.UTF-8 UTF-8, pt_PT ISO-8859-1, pt_PT.UTF-8 UTF-8, pt_PT@euro ISO-8859-15, quz_PE UTF-8, ro_RO ISO-8859-2, ro_RO.UTF-8 UTF-8, ru_RU ISO-8859-5, ru_RU.CP1251 CP1251, ru_RU.KOI8-R KOI8-R, ru_RU.UTF-8 UTF-8, ru_UA KOI8-U, ru_UA.UTF-8 UTF-8, rw_RW UTF-8, sa_IN UTF-8, sat_IN UTF-8, sc_IT UTF-8, sd_IN UTF-8, sd_IN@devanagari UTF-8, se_NO UTF-8, shs_CA UTF-8, si_LK UTF-8, sid_ET UTF-8, sk_SK ISO-8859-2, sk_SK.UTF-8 UTF-8, sl_SI ISO-8859-2, sl_SI.UTF-8 UTF-8, so_DJ ISO-8859-1, so_DJ.UTF-8 UTF-8, so_ET UTF-8, so_KE ISO-8859-1, so_KE.UTF-8 UTF-8, so_SO ISO-8859-1, so_SO.UTF-8 UTF-8, sq_AL ISO-8859-1, sq_AL.UTF-8 UTF-8, sq_MK UTF-8, sr_ME UTF-8, sr_RS UTF-8, sr_RS@latin UTF-8, ss_ZA UTF-8, st_ZA ISO-8859-1, st_ZA.UTF-8 UTF-8, sv_FI ISO-8859-1, sv_FI.UTF-8 UTF-8, sv_FI@euro ISO-8859-15, sv_SE ISO-8859-1, sv_SE.ISO-8859-15 ISO-8859-15, sv_SE.UTF-8 UTF-8, sw_KE UTF-8, sw_TZ UTF-8, szl_PL UTF-8, ta_IN UTF-8, ta_LK UTF-8, te_IN UTF-8, tg_TJ KOI8-T, tg_TJ.UTF-8 UTF-8, th_TH TIS-620, th_TH.UTF-8 UTF-8, the_NP UTF-8, ti_ER UTF-8, ti_ET UTF-8, tig_ER UTF-8, tk_TM UTF-8, tl_PH ISO-8859-1, tl_PH.UTF-8 UTF-8, tn_ZA UTF-8, tr_CY ISO-8859-9, tr_CY.UTF-8 UTF-8, tr_TR ISO-8859-9, tr_TR.UTF-8 UTF-8, ts_ZA UTF-8, tt_RU UTF-8, tt_RU@iqtelif UTF-8, ug_CN UTF-8, uk_UA KOI8-U, uk_UA.UTF-8 UTF-8, unm_US UTF-8, ur_IN UTF-8, ur_PK UTF-8, uz_UZ ISO-8859-1, uz_UZ.UTF-8 UTF-8, uz_UZ@cyrillic UTF-8, ve_ZA UTF-8, vi_VN UTF-8, wa_BE ISO-8859-1, wa_BE.UTF-8 UTF-8, wa_BE@euro ISO-8859-15, wae_CH UTF-8, wal_ET UTF-8, wo_SN UTF-8, xh_ZA ISO-8859-1, xh_ZA.UTF-8 UTF-8, yi_US CP1255, yi_US.UTF-8 UTF-8, yo_NG UTF-8, yue_HK UTF-8, zh_CN GB2312, zh_CN.GB18030 GB18030, zh_CN.GBK GBK, zh_CN.UTF-8 UTF-8, zh_HK BIG5-HKSCS, zh_HK.UTF-8 UTF-8, zh_SG GB2312, zh_SG.GBK GBK, zh_SG.UTF-8 UTF-8, zh_TW BIG5, zh_TW.EUC-TW EUC-TW, zh_TW.UTF-8 UTF-8, zu_ZA ISO-8859-1, zu_ZA.UTF-8 UTF-8 3 | locales locales/locales_to_be_generated multiselect ${LOCALE_DEFAULT} UTF-8 4 | # Default locale for the system environment: 5 | # Choices: None, C.UTF-8, en_GB.UTF-8 6 | locales locales/default_environment_locale select ${LOCALE_DEFAULT} 7 | -------------------------------------------------------------------------------- /scripts/qcow2_handling: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # QCOW2 Routines 4 | 5 | export CURRENT_IMAGE 6 | export CURRENT_MOUNTPOINT 7 | 8 | export NBD_DEV 9 | export MAP_BOOT_DEV 10 | export MAP_ROOT_DEV 11 | 12 | # set in build.sh 13 | # should be fairly enough for the beginning 14 | # overwrite here by uncommenting following lines 15 | # BASE_QCOW2_SIZE=12G 16 | 17 | # find and initialize free block device nodes 18 | init_nbd() { 19 | modprobe nbd max_part=16 20 | if [ -z "${NBD_DEV}" ]; then 21 | for x in /sys/class/block/nbd* ; do 22 | S=`cat $x/size` 23 | if [ "$S" == "0" ] ; then 24 | NBD_DEV=/dev/$(basename $x) 25 | MAP_BOOT_DEV=/dev/mapper/$(basename $x)p1 26 | MAP_ROOT_DEV=/dev/mapper/$(basename $x)p2 27 | break 28 | fi 29 | done 30 | fi 31 | } 32 | export -f init_nbd 33 | 34 | # connect image to block device 35 | connect_blkdev() { 36 | init_nbd 37 | qemu-nbd --discard=unmap -c $NBD_DEV "$1" 38 | sync 39 | kpartx -a $NBD_DEV 40 | sync 41 | CURRENT_IMAGE="$1" 42 | } 43 | export -f connect_blkdev 44 | 45 | # disconnect image from block device 46 | disconnect_blkdev() { 47 | kpartx -d $NBD_DEV 48 | qemu-nbd -d $NBD_DEV 49 | NBD_DEV= 50 | MAP_BOOT_DEV= 51 | MAP_ROOT_DEV= 52 | CURRENT_IMAGE= 53 | } 54 | export -f disconnect_blkdev 55 | 56 | # mount qcow2 image: mount_image 57 | mount_qimage() { 58 | connect_blkdev "$1" 59 | mount -v -t ext4 $MAP_ROOT_DEV "$2" 60 | mkdir -p "${ROOTFS_DIR}/boot" 61 | mount -v -t vfat $MAP_BOOT_DEV "$2/boot" 62 | CURRENT_MOUNTPOINT="$2" 63 | } 64 | export -f mount_qimage 65 | 66 | # umount qcow2 image: umount_image 67 | umount_qimage() { 68 | sync 69 | #umount "$1/boot" 70 | while mount | grep -q "$1"; do 71 | local LOCS 72 | LOCS=$(mount | grep "$1" | cut -f 3 -d ' ' | sort -r) 73 | for loc in $LOCS; do 74 | echo "$loc" 75 | while mountpoint -q "$loc" && ! umount "$loc"; do 76 | sleep 0.1 77 | done 78 | done 79 | done 80 | CURRENT_MOUNTPOINT= 81 | disconnect_blkdev 82 | } 83 | export -f umount_qimage 84 | 85 | # create base image / backing image / mount image 86 | load_qimage() { 87 | if [ -z "${CURRENT_MOUNTPOINT}" ]; then 88 | if [ ! -d "${ROOTFS_DIR}" ]; then 89 | mkdir -p "${ROOTFS_DIR}"; 90 | fi 91 | 92 | if [ "${CLEAN}" = "1" ] && [ -f "${WORK_DIR}/image-${STAGE}.qcow2" ]; then 93 | rm -f "${WORK_DIR}/image-${STAGE}.qcow2"; 94 | fi 95 | 96 | if [ ! -f "${WORK_DIR}/image-${STAGE}.qcow2" ]; then 97 | pushd ${WORK_DIR} > /dev/null 98 | init_nbd 99 | if [ -z "${PREV_STAGE}" ]; then 100 | echo "Creating base image: image-${STAGE}.qcow2" 101 | # -o preallocation=falloc 102 | qemu-img create -f qcow2 image-${STAGE}.qcow2 $BASE_QCOW2_SIZE 103 | sync 104 | qemu-nbd --discard=unmap -c $NBD_DEV image-${STAGE}.qcow2 105 | sync 106 | sfdisk $NBD_DEV << EOF 107 | 4MiB,250MiB,c,* 108 | 254MiB,,83; 109 | EOF 110 | sync 111 | kpartx -a $NBD_DEV 112 | mkdosfs -n boot -F 32 -v $MAP_BOOT_DEV 113 | mkfs.ext4 -L rootfs -O "^huge_file,^metadata_csum,^64bit" $MAP_ROOT_DEV 114 | sync 115 | else 116 | if [ ! -f "${WORK_DIR}/image-${PREV_STAGE}.qcow2" ]; then 117 | exit 1; 118 | fi 119 | echo "Creating backing image: image-${STAGE}.qcow2 <- ${WORK_DIR}/image-${PREV_STAGE}.qcow2" 120 | qemu-img create -f qcow2 \ 121 | -o backing_file=${WORK_DIR}/image-${PREV_STAGE}.qcow2 \ 122 | ${WORK_DIR}/image-${STAGE}.qcow2 123 | sync 124 | qemu-nbd --discard=unmap -c $NBD_DEV image-${STAGE}.qcow2 125 | sync 126 | kpartx -a $NBD_DEV 127 | fi 128 | 129 | mount -v -t ext4 $MAP_ROOT_DEV "${ROOTFS_DIR}" 130 | mkdir -p "${ROOTFS_DIR}/boot" 131 | mount -v -t vfat $MAP_BOOT_DEV "${ROOTFS_DIR}/boot" 132 | CURRENT_IMAGE=${WORK_DIR}/image-${STAGE}.qcow2 133 | CURRENT_MOUNTPOINT=${ROOTFS_DIR} 134 | popd > /dev/null 135 | else 136 | mount_qimage "${WORK_DIR}/image-${STAGE}.qcow2" "${ROOTFS_DIR}" 137 | fi 138 | echo "Current image in use: ${CURRENT_IMAGE} (MP: ${CURRENT_MOUNTPOINT})" 139 | fi 140 | } 141 | export -f load_qimage 142 | 143 | # umount current image and refresh mount point env var 144 | unload_qimage() { 145 | if [ ! -z "${CURRENT_MOUNTPOINT}" ]; then 146 | fstrim -v "${CURRENT_MOUNTPOINT}" || true 147 | umount_qimage "${CURRENT_MOUNTPOINT}" 148 | fi 149 | } 150 | export -f unload_qimage 151 | 152 | # based on: https://github.com/SirLagz/RaspberryPi-ImgAutoSizer 153 | # helper function for make_bootable_image, do not call directly 154 | function resize_qcow2() { 155 | if [ -z "$CALL_FROM_MBI" ]; then 156 | echo "resize_qcow2: cannot be called directly, use make_bootable_image instead" 157 | return 1 158 | fi 159 | 160 | # ROOT_MARGIN=$((800*1024*1024)) 161 | ROOT_MARGIN=$((1*1024*1024)) 162 | PARTED_OUT=`parted -s -m "$NBD_DEV" unit B print` 163 | PART_NO=`echo "$PARTED_OUT" | grep ext4 | awk -F: ' { print $1 } '` 164 | PART_START=`echo "$PARTED_OUT" | grep ext4 | awk -F: ' { print substr($2,1,length($2)-1) } '` 165 | 166 | e2fsck -y -f $MAP_ROOT_DEV || true 167 | 168 | DATA_SIZE=`resize2fs -P $MAP_ROOT_DEV | awk -F': ' ' { print $2 } '` 169 | BLOCK_SIZE=$(dumpe2fs -h $MAP_ROOT_DEV | grep 'Block size' | awk -F': ' ' { print $2 }') 170 | BLOCK_SIZE=${BLOCK_SIZE// /} 171 | 172 | let DATA_SIZE=$DATA_SIZE+$ROOT_MARGIN/$BLOCK_SIZE 173 | resize2fs -p $MAP_ROOT_DEV $DATA_SIZE 174 | sleep 1 175 | 176 | let PART_NEW_SIZE=$DATA_SIZE*$BLOCK_SIZE 177 | let PART_NEW_END=$PART_START+$PART_NEW_SIZE 178 | ACT1=`parted -s "$NBD_DEV" rm 2` 179 | ACT2=`parted -s "$NBD_DEV" unit B mkpart primary $PART_START $PART_NEW_END` 180 | NEW_IMG_SIZE=`parted -s -m "$NBD_DEV" unit B print free | tail -1 | awk -F: ' { print substr($2,1,length($2)-1) } '` 181 | } 182 | export -f resize_qcow2 183 | 184 | # create raw img from qcow2: make_bootable_image 185 | function make_bootable_image() { 186 | 187 | EXPORT_QCOW2="$1" 188 | EXPORT_IMAGE="$2" 189 | 190 | echo "Connect block device to source qcow2" 191 | connect_blkdev "${EXPORT_QCOW2}" 192 | 193 | echo "Resize fs and partition" 194 | CALL_FROM_MBI=1 195 | resize_qcow2 196 | sync 197 | CALL_FROM_MBI= 198 | 199 | echo "Disconnect block device" 200 | disconnect_blkdev 201 | 202 | if [ -z "$NEW_IMG_SIZE" ]; then 203 | echo "NEW_IMG_SIZE could not be calculated, cannot process image. Exit." 204 | exit 1 205 | fi 206 | 207 | echo "Shrinking qcow2 image" 208 | qemu-img resize --shrink "${EXPORT_QCOW2}" $NEW_IMG_SIZE 209 | sync 210 | 211 | echo "Convert qcow2 to raw image" 212 | qemu-img convert -f qcow2 -O raw "${EXPORT_QCOW2}" "${EXPORT_IMAGE}" 213 | sync 214 | 215 | echo "Get PARTUUIDs from image" 216 | IMGID="$(blkid -o value -s PTUUID "${EXPORT_IMAGE}")" 217 | 218 | BOOT_PARTUUID="${IMGID}-01" 219 | echo "Boot: $BOOT_PARTUUID" 220 | ROOT_PARTUUID="${IMGID}-02" 221 | echo "Root: $ROOT_PARTUUID" 222 | 223 | echo "Mount image" 224 | MOUNTROOT=${WORK_DIR}/tmpimage 225 | mkdir -p $MOUNTROOT 226 | 227 | MOUNTPT=$MOUNTROOT 228 | PARTITION=2 229 | mount "${EXPORT_IMAGE}" "$MOUNTPT" -o loop,offset=$[ `/sbin/sfdisk -d "${EXPORT_IMAGE}" | grep "start=" | head -n $PARTITION | tail -n1 | sed 's/.*start=[ ]*//' | sed 's/,.*//'` * 512 ],sizelimit=$[ `/sbin/sfdisk -d "${EXPORT_IMAGE}" | grep "start=" | head -n $PARTITION | tail -n1 | sed 's/.*size=[ ]*//' | sed 's/,.*//'` * 512 ] || exit 1 230 | 231 | MOUNTPT=$MOUNTROOT/boot 232 | PARTITION=1 233 | mount "${EXPORT_IMAGE}" "$MOUNTPT" -o loop,offset=$[ `/sbin/sfdisk -d "${EXPORT_IMAGE}" | grep "start=" | head -n $PARTITION | tail -n1 | sed 's/.*start=[ ]*//' | sed 's/,.*//'` * 512 ],sizelimit=$[ `/sbin/sfdisk -d "${EXPORT_IMAGE}" | grep "start=" | head -n $PARTITION | tail -n1 | sed 's/.*size=[ ]*//' | sed 's/,.*//'` * 512 ] || exit 1 234 | 235 | if [ ! -d "${MOUNTROOT}/root" ]; then 236 | echo "Image damaged or not mounted. Exit." 237 | exit 1 238 | fi 239 | 240 | echo "Setup PARTUUIDs" 241 | if [ ! -z "$BOOT_PARTUUID" ] && [ ! -z "$ROOT_PARTUUID" ]; then 242 | echo "Set UUIDs to make it bootable" 243 | sed -i "s/BOOTDEV/PARTUUID=${BOOT_PARTUUID}/" "${MOUNTROOT}/etc/fstab" 244 | sed -i "s/ROOTDEV/PARTUUID=${ROOT_PARTUUID}/" "${MOUNTROOT}/etc/fstab" 245 | sed -i "s/ROOTDEV/PARTUUID=${ROOT_PARTUUID}/" "${MOUNTROOT}/boot/cmdline.txt" 246 | fi 247 | 248 | echo "Umount image" 249 | sync 250 | umount "${MOUNTROOT}/boot" || exit 1 251 | umount "${MOUNTROOT}" || exit 1 252 | 253 | echo "Remove qcow2 export image" 254 | rm -f "${EXPORT_QCOW2}" 255 | } 256 | export -f make_bootable_image 257 | -------------------------------------------------------------------------------- /stage2/01-sys-tweaks/00-debconf: -------------------------------------------------------------------------------- 1 | # Encoding to use on the console: 2 | # Choices: ARMSCII-8, CP1251, CP1255, CP1256, GEORGIAN-ACADEMY, GEORGIAN-PS, IBM1133, ISIRI-3342, ISO-8859-1, ISO-8859-10, ISO-8859-11, ISO-8859-13, ISO-8859-14, ISO-8859-15, ISO-8859-16, ISO-8859-2, ISO-8859-3, ISO-8859-4, ISO-8859-5, ISO-8859-6, ISO-8859-7, ISO-8859-8, ISO-8859-9, KOI8-R, KOI8-U, TIS-620, UTF-8, VISCII 3 | console-setup console-setup/charmap47 select UTF-8 4 | # Character set to support: 5 | # Choices: . Arabic, # Armenian, # Cyrillic - KOI8-R and KOI8-U, # Cyrillic - non-Slavic languages, # Cyrillic - Slavic languages (also Bosnian and Serbian Latin), . Ethiopic, # Georgian, # Greek, # Hebrew, # Lao, # Latin1 and Latin5 - western Europe and Turkic languages, # Latin2 - central Europe and Romanian, # Latin3 and Latin8 - Chichewa; Esperanto; Irish; Maltese and Welsh, # Latin7 - Lithuanian; Latvian; Maori and Marshallese, . Latin - Vietnamese, # Thai, . Combined - Latin; Slavic Cyrillic; Hebrew; basic Arabic, . Combined - Latin; Slavic Cyrillic; Greek, . Combined - Latin; Slavic and non-Slavic Cyrillic, Guess optimal character set 6 | console-setup console-setup/codeset47 select Guess optimal character set 7 | # Font for the console: 8 | # Choices: Fixed, Goha, GohaClassic, Terminus, TerminusBold, TerminusBoldVGA, VGA, Do not change the boot/kernel font, Let the system select a suitable font 9 | console-setup console-setup/fontface47 select Do not change the boot/kernel font 10 | # Key to function as AltGr: 11 | # Choices: The default for the keyboard layout, No AltGr key, Right Alt (AltGr), Right Control, Right Logo key, Menu key, Left Alt, Left Logo key, Keypad Enter key, Both Logo keys, Both Alt keys 12 | keyboard-configuration keyboard-configuration/altgr select The default for the keyboard layout 13 | # Keyboard model: 14 | # Choices: A4Tech KB-21, A4Tech KBS-8, A4Tech Wireless Desktop RFKB-23, Acer AirKey V, Acer C300, Acer Ferrari 4000, Acer Laptop, Advance Scorpius KI, Amiga, Apple, Apple Aluminium Keyboard (ANSI), Apple Aluminium Keyboard (ISO), Apple Aluminium Keyboard (JIS), Apple Laptop, Asus Laptop, Atari TT, Azona RF2300 wireless Internet Keyboard, BTC 5090, BTC 5113RF Multimedia, BTC 5126T, BTC 6301URF, BTC 9000, BTC 9000A, BTC 9001AH, BTC 9019U, BTC 9116U Mini Wireless Internet and Gaming, BenQ X-Touch, BenQ X-Touch 730, BenQ X-Touch 800, Brother Internet Keyboard, Cherry B.UNLIMITED, Cherry Blue Line CyBo@rd, Cherry Blue Line CyBo@rd (alternate option), Cherry CyBo@rd USB-Hub, Cherry CyMotion Expert, Cherry CyMotion Master Linux, Cherry CyMotion Master XPress, Chicony Internet Keyboard, Chicony KB-9885, Chicony KU-0108, Chicony KU-0420, Classmate PC, Compaq Easy Access Keyboard, Compaq Internet Keyboard (13 keys), Compaq Internet Keyboard (18 keys), Compaq Internet Keyboard (7 keys), Compaq iPaq Keyboard, Creative Desktop Wireless 7000, DTK2000, Dell, Dell 101-key PC, Dell Laptop/notebook Inspiron 6xxx/8xxx, Dell Laptop/notebook Precision M series, Dell Latitude series laptop, Dell Precision M65, Dell SK-8125, Dell SK-8135, Dell USB Multimedia Keyboard, Dexxa Wireless Desktop Keyboard, Diamond 9801 / 9802 series, Ennyah DKB-1008, Everex STEPnote, FL90, Fujitsu-Siemens Computers AMILO laptop, Generic 101-key PC, Generic 102-key (Intl) PC, Generic 104-key PC, Generic 105-key (Intl) PC, Genius Comfy KB-12e, Genius Comfy KB-16M / Genius MM Keyboard KWD-910, Genius Comfy KB-21e-Scroll, Genius KB-19e NB, Genius KKB-2050HS, Gyration, HTC Dream, Happy Hacking Keyboard, Happy Hacking Keyboard for Mac, Hewlett-Packard Internet Keyboard, Hewlett-Packard Mini 110 Notebook, Hewlett-Packard Omnibook 500 FA, Hewlett-Packard Omnibook 5xx, Hewlett-Packard Omnibook 6000/6100, Hewlett-Packard Omnibook XE3 GC, Hewlett-Packard Omnibook XE3 GF, Hewlett-Packard Omnibook XT1000, Hewlett-Packard Pavilion ZT11xx, Hewlett-Packard Pavilion dv5, Hewlett-Packard SK-250x Multimedia Keyboard, Hewlett-Packard nx9020, Honeywell Euroboard, Htc Dream phone, IBM Rapid Access, IBM Rapid Access II, IBM Space Saver, IBM ThinkPad 560Z/600/600E/A22E, IBM ThinkPad R60/T60/R61/T61, IBM ThinkPad Z60m/Z60t/Z61m/Z61t, Keytronic FlexPro, Kinesis, Laptop/notebook Compaq (eg. Armada) Laptop Keyboard, Laptop/notebook Compaq (eg. Presario) Internet Keyboard, Laptop/notebook eMachines m68xx, Logitech Access Keyboard, Logitech Cordless Desktop, Logitech Cordless Desktop (alternate option), Logitech Cordless Desktop EX110, Logitech Cordless Desktop LX-300, Logitech Cordless Desktop Navigator, Logitech Cordless Desktop Optical, Logitech Cordless Desktop Pro (alternate option 2), Logitech Cordless Desktop iTouch, Logitech Cordless Freedom/Desktop Navigator, Logitech G15 extra keys via G15daemon, Logitech Generic Keyboard, Logitech Internet 350 Keyboard, Logitech Internet Keyboard, Logitech Internet Navigator Keyboard, Logitech Media Elite Keyboard, Logitech Ultra-X Cordless Media Desktop Keyboard, Logitech Ultra-X Keyboard, Logitech diNovo Edge Keyboard, Logitech diNovo Keyboard, Logitech iTouch, Logitech iTouch Cordless Keyboard (model Y-RB6), Logitech iTouch Internet Navigator Keyboard SE, Logitech iTouch Internet Navigator Keyboard SE (USB), MacBook/MacBook Pro, MacBook/MacBook Pro (Intl), Macintosh, Macintosh Old, Memorex MX1998, Memorex MX2500 EZ-Access Keyboard, Memorex MX2750, Microsoft Comfort Curve Keyboard 2000, Microsoft Internet Keyboard, Microsoft Internet Keyboard Pro\, Swedish, Microsoft Natural, Microsoft Natural Keyboard Elite, Microsoft Natural Keyboard Pro / Microsoft Internet Keyboard Pro, Microsoft Natural Keyboard Pro OEM, Microsoft Natural Keyboard Pro USB / Microsoft Internet Keyboard Pro, Microsoft Natural Wireless Ergonomic Keyboard 4000, Microsoft Natural Wireless Ergonomic Keyboard 7000, Microsoft Office Keyboard, Microsoft Wireless Multimedia Keyboard 1.0A, Northgate OmniKey 101, OLPC, Ortek MCK-800 MM/Internet keyboard, PC-98xx Series, Propeller Voyager (KTEZ-1000), QTronix Scorpius 98N+, SILVERCREST Multimedia Wireless Keyboard, SK-1300, SK-2500, SK-6200, SK-7100, SVEN Ergonomic 2500, SVEN Slim 303, Samsung SDM 4500P, Samsung SDM 4510P, Sanwa Supply SKB-KG3, Sun Type 4, Sun Type 5, Sun Type 6 (Japanese layout), Sun Type 6 USB (Japanese layout), Sun Type 6 USB (Unix layout), Sun Type 6/7 USB, Sun Type 6/7 USB (European layout), Sun Type 7 USB, Sun Type 7 USB (European layout), Sun Type 7 USB (Japanese layout) / Japanese 106-key, Sun Type 7 USB (Unix layout), Super Power Multimedia Keyboard, Symplon PaceBook (tablet PC), Targa Visionary 811, Toshiba Satellite S3000, Trust Direct Access Keyboard, Trust Slimline, Trust Wireless Keyboard Classic, TypeMatrix EZ-Reach 2020, TypeMatrix EZ-Reach 2030 PS2, TypeMatrix EZ-Reach 2030 USB, TypeMatrix EZ-Reach 2030 USB (102/105:EU mode), TypeMatrix EZ-Reach 2030 USB (106:JP mode), Unitek KB-1925, ViewSonic KU-306 Internet Keyboard, Winbook Model XP5, Yahoo! Internet Keyboard 15 | keyboard-configuration keyboard-configuration/model select Generic 105-key (Intl) PC 16 | # Keymap to use: 17 | # Choices: American English, Albanian, Arabic, Asturian, Bangladesh, Belarusian, Bengali, Belgian, Bosnian, Brazilian, British English, Bulgarian, Bulgarian (phonetic layout), Burmese, Canadian French, Canadian Multilingual, Catalan, Chinese, Croatian, Czech, Danish, Dutch, Dvorak, Dzongkha, Esperanto, Estonian, Ethiopian, Finnish, French, Georgian, German, Greek, Gujarati, Gurmukhi, Hebrew, Hindi, Hungarian, Icelandic, Irish, Italian, Japanese, Kannada, Kazakh, Khmer, Kirghiz, Korean, Kurdish (F layout), Kurdish (Q layout), Lao, Latin American, Latvian, Lithuanian, Macedonian, Malayalam, Nepali, Northern Sami, Norwegian, Persian, Philippines, Polish, Portuguese, Punjabi, Romanian, Russian, Serbian (Cyrillic), Sindhi, Sinhala, Slovak, Slovenian, Spanish, Swedish, Swiss French, Swiss German, Tajik, Tamil, Telugu, Thai, Tibetan, Turkish (F layout), Turkish (Q layout), Ukrainian, Uyghur, Vietnamese 18 | keyboard-configuration keyboard-configuration/xkb-keymap select ${KEYBOARD_KEYMAP} 19 | # Compose key: 20 | # Choices: No compose key, Right Alt (AltGr), Right Control, Right Logo key, Menu key, Left Logo key, Caps Lock 21 | keyboard-configuration keyboard-configuration/compose select No compose key 22 | # Use Control+Alt+Backspace to terminate the X server? 23 | keyboard-configuration keyboard-configuration/ctrl_alt_bksp boolean true 24 | # Keyboard layout: 25 | # Choices: English (UK), English (UK) - English (UK\, Colemak), English (UK) - English (UK\, Dvorak with UK punctuation), English (UK) - English (UK\, Dvorak), English (UK) - English (UK\, Macintosh international), English (UK) - English (UK\, Macintosh), English (UK) - English (UK\, extended WinKeys), English (UK) - English (UK\, international with dead keys), Other 26 | keyboard-configuration keyboard-configuration/variant select ${KEYBOARD_LAYOUT} 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transistor [![Build Status](https://api.travis-ci.com/pirateradiohack/Transistor.svg?branch=master)](https://travis-ci.com/github/pirateradiohack/Transistor) 2 | 3 | ---------------------------------------------------------- 4 | ## Deprecation status 5 | 6 | This version is for the Raspberry Pi Zero W **version 1 only**. 7 | It was the first version of the Transistor software. 8 | It is not maintained any more but **should work** as described here. 9 | You can download it here: https://github.com/pirateradiohack/Transistor/releases/download/2022-04-10-Fix_Podcasts_Update-SSH/image_2022-04-10-Transistor-lite.zip 10 | 11 | Development efforts are being made in the new repository (https://github.com/pirateradiohack/Transistor) to support the Raspberry Pi Zero W version 2, and if possible also the version 1. 12 | 13 | This repository will be kept in its current state. 14 | 15 | ---------------------------------------------------------- 16 | 17 | Build your own radio set and listen to **web radios**, **podcasts** and **bluetooth**. 18 | 19 |  Transistor 3D printed case. 20 | 21 | 🇬🇧 [Quick Setup + User Manual](https://raw.githubusercontent.com/pirateradiohack/Transistor/master/Quick_Setup_Guide%2BUser_Manual.pdf) 22 | 🇫🇷 [Manuel de mise en route et d'utilisation](https://raw.githubusercontent.com/pirateradiohack/Transistor/master/Mise-en-route-rapide%2Bmanuel-utilisation.pdf) 23 | 24 | All you need to do is copy [the image](https://github.com/pirateradiohack/Transistor/releases/latest) to a micro SD card and insert it in the following hardware: 25 | 26 | - [Raspberry Pi Zero W](https://shop.pimoroni.com/products/raspberry-pi-zero-w?variant=39458414297171) 27 | - Micro SD card (4GB should be enough if you don't want to store anything on it. If you want to store music or podcasts on your device then the bigger the better.) 28 | - [Pirate Audio 3W Stereo Amp](https://shop.pimoroni.com/products/pirate-audio-3w-stereo-amp) 29 | - A pair of [5W 4 Ohm 65mm Full Range Speaker](https://shop.pimoroni.com/products/5w-4-ohm-65mm-full-range-speaker) 30 | - [On / Off switch](https://shop.pimoroni.com/products/onoff-shim) 31 | - [Colourful Arcade Buttons](https://shop.pimoroni.com/products/colourful-arcade-buttons?variant=3030992879626) (For the Big Next Button(TM) ) 32 | - [Tactile Switch Buttons](https://shop.pimoroni.com/products/tactile-switch-buttons-6mm-slim-x-20-pack?variant=31479866785875) (For the power button) 33 | - [USB A to microB cable - Red – 10cm](https://shop.pimoroni.com/products/usb-a-to-microb-cable-red?variant=32065140554) (To connect the On / Off switch power micro USB to the battery charger.) 34 | - [Adafruit PowerBoost 1000 Charger - Rechargeable 5V Lipo USB Boost @ 1A - 1000C](https://shop.pimoroni.com/products/powerboost-1000-charger-rechargeable-5v-lipo-usb-boost-1a-1000c) 35 | - [LiPo Battery Pack – 2000mAh](https://shop.pimoroni.com/products/lipo-battery-pack?variant=20429082247) (more mAh means more autonomy, bigger is better but also more expensive) 36 | - [Right-angle Panel Mount Extension Cables (25cm) – USB micro-B](https://shop.pimoroni.com/products/right-angle-panel-mount-extension-cables-25cm?variant=32013609631827) (To connect to the battery charger and provide a power plug that goes outside the radio.) 37 | - [optional] [RasPiO Analog Zero](https://shop.pimoroni.com/products/raspio-analog-zero) (Optional. If you plug it, you will be able to use an old radio volume knob out of the box!) 38 | 39 | (if you find better hardware parts please [tell me](https://github.com/pirateradiohack/Transistor/issues/new/choose)!) 40 | 41 | Get the components, assemble them together and build the case with a 3D printer: 42 | 43 | - [Case](https://github.com/pirateradiohack/Transistor/blob/master/Transistor5.stl) 44 | - [Back Cover](https://github.com/pirateradiohack/Transistor/blob/master/Transistor5-cover.stl) 45 | 46 | (You will need to adapt the battery compartment to fit you own battery.) 47 | 48 | Alternatively you could use your own case, or an old radio, or a cardboard box, or a wooden box, or a toaster... 49 | 50 | ## Quick Setup 51 | 52 | - Download the [latest image](https://github.com/pirateradiohack/Transistor/releases/latest), that's the ZIP file named image. Unzip it. 53 | - Flash the image with [etcher](https://www.balena.io/etcher/). 54 | - Install the SD card in your radio and turn it on. (note that the first boot takes some time, be patient) 55 | - You will see a new wifi access point called `Transistor`: connect to it (the password is `Transistor`). In your browser, open http://192.168.179.1:8000/wifi_settings (it can take some time to load, be patient) and enter your home wifi name and password there. Press OK, your Transistor will reboot and connect to your wifi. 56 | - Connect to the web interface: hold the play / pause button and you will see the IP address of the Transistor appear on its screen. Copy that to your browser. (If that does not work on your device then read [Controlling your Transistor](#controlling-your-transistor)) 57 | - From the interface, add a radio to listen to by going to `+Radio` in the menu. 58 | - Enjoy. 59 | 60 | ## Features Description: 61 | 62 | #### * Play **web radios**: 63 | From the web interface, click the **+Radio** button and enter the name of the radio. Wait a bit, it will be added automatically to your list! (thanks to radio-browser.info). (Optionally you can add an http radio stream directly with the +Stream button.) 64 | 65 | #### * Play your **podcasts**: 66 | From the web interface, click the **+Podcast** button to go to the podcasts settings where you can subscribe and manage your podcasts. Podcasts are then updated every 30 minutes. Only the latest number of episodes you specify will be kept on your device and appear in the Podcasts section automatically. (Do note that it takes time to download the episodes. Try to subscribe gently and allow some time before subscribing to other podcasts.) 67 | 68 | #### * **Bluetooth speaker**: 69 | Simply connect to the bluetooth speaker called **Transistor** from your phone or computer. 70 | #### * **Sleep timer**: 71 | 72 | When you hold the on / off button for 1 second your Transistor will turn off 20 minutes later. If you feel the need, you can hold it again to add another 20 minutes and so on, until you fall asleep. (You can still simply press the on/off button to immediately turn off your Transistor.) 73 | 74 | #### * **The BiG NexT ButtoN (TM)**: 75 | A big, very satisfying to hit, arcade style button that skips to the next radio. Feels Good. 76 | 77 | ## Images: 78 | 79 | Inside: 80 |  Transistor 3D printed case inside. 81 | 82 | The web interface: 83 | Transistor interface 84 | 85 | ## Detailed setup 86 | 87 | ### Getting the image 88 | ### Ready-to-write image 89 | For your convenience you will find the latest image pre-built. Access it from the [releases](https://github.com/pirateradiohack/Transistor/releases) page. Download and unzip the latest image, it's the zip file called "image" with a date. (It is built automatically from the source code by Travis-ci.) 90 | 91 | 92 | In case you would like to set your wifi manually, the file to edit is: 93 | `/etc/wpa_supplicant/wpa_supplicant-wlan0.conf` (edit this file as root / sudo) 94 | 95 | Your operating system needs to be able to mount `ext4` (linux) partitions. 96 | You can edit the files *before* or *after* writing the image to the sd card: 97 | - before: you can mount the `.img`. 98 | With a modern operating system you probably just have to click the .img. 99 | With Linux you can use `kpartx` (from the `multipath-tools` package) to be able to mount the partition directly: `sudo kpartx -a path/to/2019-05-23-Transistor-lite.img` followed by `sudo mount /dev/mapper/loop0p2 tmp/ -o loop,rw` 100 | (you will need to create the mount directory first and check what loop device you are using with `sudo kpartx -l path/to/2019-05-23-Transistor-lite.img`). Then you can edit the files mentioned above. And `sudo umount tmp`. 101 | You are safe to flash the image. 102 | - after: your operating system probably automounts the partitions. 103 | 104 | ### building from source 105 | - First clone this repository with `git clone https://github.com/pirateradiohack/Transistor.git`. 106 | - Then build the image. (You can see the whole guide on the official RaspberryPi repo: https://github.com/RPi-Distro/pi-gen). I find it easier to use docker (obviously you need to have docker installed on your system) as there is nothing else to install, just run one command from this directory: `./build-docker.sh`. That's it. On my computer it takes between 15 and 30 minutes. And at the end you should see something like: `Done! Your image(s) should be in deploy/` 107 | If you don't see that, it's probably that the build failed. It happens to me sometimes for no reason and I find that just re-launching the build with `CONTINUE=1 ./build-docker.sh` finishes the build correctly. 108 | - On some systems even Docker fails to build the image. In this case you can fallback to Vagrant. It will 109 | create a virtual machine with a supported OS to build the image. 110 | You will need to install both Vagrant and VirtualBox. The vagrantfile at the root of this repository will 111 | install Debian Buster as recommend by [pi-gen](https://github.com/RPi-Distro/pi-gen). It will also share 112 | the repository inside the VM at /pigen. It means you just need to issue those commands: 113 | `vagrant up` then `vagrant ssh --command 'sudo /pigen/build.sh'` and finally `vagrant halt`. 114 | If everything goes well you will find your image in the `deploy` folder. 115 | - You should find the newly created image in the `deploy` directory. 116 | 117 | ## Write the image to a SD card 118 | Now that you have your image file, you need to write it to a SD card in order to put it in the Raspberry Pi. Choose you favorite method below and once you have your SD card ready you can boot your Raspberry Pi, the first boot can take a minute or two, and then your radio is ready to go. 119 | 120 | ### graphically 121 | For a user friendly experience you can try [etcher](https://www.balena.io/etcher/) to flash the image to the SD card. 122 | 123 | ### manually 124 | On linux (and it probably works on Mac too) an example to get it on the SD card would be: 125 | `sudo dd bs=4M if=deploy/2019-05-23-Piradio-lite.img of=/dev/mmcblk0 conv=fsync` 126 | (of course you need to replace `/dev/mmcblk0` with the path to your own SD card. You can find it with the command `lsblk -f`) 127 | Those settings are recommended by the RaspberryPi instructions. 128 | 129 | ## Controlling your Transistor 130 | 131 | Transistor is equipped with the necessary buttons to turn it on / off, control the volume up and down, and select the next and previous radio stations. Besides that, you can control it remotely, on the network. 132 | 133 | Transistor connects to the wifi network that was set in the config file. It should receive an IP address from the Internet router. You need to find this IP address in order to control the Transistor. There are several ways you can do that: 134 | 135 | - For convinience, Transistor sets its own local domain. Some devices are compatible with this (Android supposedly works, but iOS does not. Linux should be fine if avahi is installed). So you can try to reach your Transistor at the address `transistor.local`. 136 | - You can see the IP address of your Transistor directly on its screen when you hold the play / pause button. 137 | - The command `nmap 192.168.1.0/24` can list the devices on your network (adapt the network address based on your computer's IP address, most probably the `1` could be another number). You will see a device named `transistor.local` along with its IP address. 138 | - Or you could also check your Internet router to get the list of connected devices and their corresponding IP addresses. 139 | 140 | ### via web interface 141 | You can control your radio via web interface: try to open `http://transistor.local` (or the IP address) in a web browser. 142 | 143 | ### via ssh with a terminal interface 144 | If you prefer the command line, you can ssh into Transistor and then use `ncmpcpp` to get a nice terminal interface (see some screenshots here: https://rybczak.net/ncmpcpp/screenshots/). 145 | 146 | ### via an application 147 | You can use any `mpd` client you like (a non exhaustive list of applications for various platforms can be found here: https://www.musicpd.org/clients/). If you are asked for the port number, it's the default one, 6600. And for the IP address, it's the same thing as above. 148 | 149 | ## How it is built 150 | The image is built with the official RaspberryPi.org tool (https://github.com/RPi-Distro/pi-gen) to build a Raspbian lite system with all the software needed 151 | to have a working internet radio stream client. It uses `mpd`. 152 | 153 | ## Developers 154 | The most interesting bits happen in the `stage2/04-pirate-radio` directory. Where files can be added and then instructions can be set for the building tool to create the final OS image. 155 | 156 | If you want to test the image locally, without the need to burn it to an SD card, you can use QEMU to emulate the hardware on your system and then create a virtual machine. 157 | - Make sure you have QEMU installed for the arm architecture, you can test with `qemu-system-arm --version`. 158 | - Set `USE_QEMU` to `"1"` or `true` in the config file and then build your image as usual. 159 | - Download the [Buster kernel](https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.19.50-buster) and the [Device Tree File](https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/versatile-pb.dtb) in the `deploy` directory. 160 | - Execute `qemu-system-arm -kernel kernel-qemu-4.19.50-buster -cpu arm1176 -m 256 -M versatilepb -dtb versatile-pb.dtb -no-reboot -serial stdio -net nic -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::2223-:80 -append "root=/dev/sda2 panic=1 rootfstype=ext4 rw" -hda name_of_your_image` 161 | 162 | That should boot the image. You can then `ssh` into it on `localhost` on port 2222, and use port 80 on `localhost` port 2223. 163 | 164 | ## Hardware 165 | This is the detailed usage of the pins (thanks to https://pinout.xyz). This is mostly of interest when you want to connect the buttons. 166 | 167 | OOS: On Off Shim 168 | AZ: Raspio Analog Zero 169 | PA: Pirate Audio Amp + LCD 170 | 171 | Powers and grounds don't appear in the picture, here is their pinout: 172 | - Power 1: OOS / AZ 173 | - Power 2: OOS / PA 174 | - Ground 6: AZ 175 | - Ground 9: AZ 176 | - Ground 14: AZ 177 | - Ground 20: AZ 178 | - Ground 25: AZ / PA 179 | - Ground 30: AZ 180 | - Ground 34: AZ 181 | - Ground 39: AZ / PA 182 | 183 | Transistor pins usage 184 | Transistor pins usage legend 185 | 186 | The first channel of the Raspio Analog Zero (channel 0) is expecting to receive a potentiometer to set the sound volume: 187 | plug the ends of the potentiometer to 3.3v and ground, then plug the wiper (usually found in the middle of the three tabs) into the first channel of the Analog Zero. 188 | 189 | These are the instructions to generate the image of the pins layout: 190 | - cd to pinout directory 191 | - edit `src/en/overlay/transistor.md` 192 | - `docker build -t pinout.xyz .` 193 | - `docker run -p 5000:5000 -e PINOUT_LANG=en pinout.xyz` 194 | - in browser open `http://localhost:5000/pinout/transistor` 195 | - `docker stop container_name` 196 | - `docker rm container_name` 197 | - `docker rmi pinout.xyz` 198 | 199 | ## Contribution 200 | Issues and pull requests are welcome. 201 | -------------------------------------------------------------------------------- /stage2/04-pirate-radio/files/etc/mpd.conf: -------------------------------------------------------------------------------- 1 | # An example configuration file for MPD. 2 | # Read the user manual for documentation: http://www.musicpd.org/doc/user/ 3 | # or /usr/share/doc/mpd/user-manual.html 4 | 5 | 6 | # Files and directories ####################################################### 7 | # 8 | # This setting controls the top directory which MPD will search to discover the 9 | # available audio files and add them to the daemon's online database. This 10 | # setting defaults to the XDG directory, otherwise the music directory will be 11 | # be disabled and audio files will only be accepted over ipc socket (using 12 | # file:// protocol) or streaming files over an accepted protocol. 13 | # 14 | music_directory "/home/transistor/podcasts" 15 | # 16 | # This setting sets the MPD internal playlist directory. The purpose of this 17 | # directory is storage for playlists created by MPD. The server will use 18 | # playlist files not created by the server but only if they are in the MPD 19 | # format. This setting defaults to playlist saving being disabled. 20 | # 21 | playlist_directory "/var/lib/mpd/playlists" 22 | # 23 | # This setting sets the location of the MPD database. This file is used to 24 | # load the database at server start up and store the database while the 25 | # server is not up. This setting defaults to disabled which will allow 26 | # MPD to accept files over ipc socket (using file:// protocol) or streaming 27 | # files over an accepted protocol. 28 | # 29 | db_file "/var/lib/mpd/tag_cache" 30 | # 31 | # These settings are the locations for the daemon log files for the daemon. 32 | # These logs are great for troubleshooting, depending on your log_level 33 | # settings. 34 | # 35 | # The special value "syslog" makes MPD use the local syslog daemon. This 36 | # setting defaults to logging to syslog, otherwise logging is disabled. 37 | # 38 | log_file "/var/log/mpd/mpd.log" 39 | # 40 | # This setting sets the location of the file which stores the process ID 41 | # for use of mpd --kill and some init scripts. This setting is disabled by 42 | # default and the pid file will not be stored. 43 | # 44 | pid_file "/run/mpd/pid" 45 | # 46 | # This setting sets the location of the file which contains information about 47 | # most variables to get MPD back into the same general shape it was in before 48 | # it was brought down. This setting is disabled by default and the server 49 | # state will be reset on server start up. 50 | # 51 | state_file "/var/lib/mpd/state" 52 | # 53 | # The location of the sticker database. This is a database which 54 | # manages dynamic information attached to songs. 55 | # 56 | sticker_file "/var/lib/mpd/sticker.sql" 57 | # 58 | ############################################################################### 59 | 60 | 61 | # General music daemon options ################################################ 62 | # 63 | # This setting specifies the user that MPD will run as. MPD should never run as 64 | # root and you may use this setting to make MPD change its user ID after 65 | # initialization. This setting is disabled by default and MPD is run as the 66 | # current user. 67 | # 68 | user "mpd" 69 | # 70 | # This setting specifies the group that MPD will run as. If not specified 71 | # primary group of user specified with "user" setting will be used (if set). 72 | # This is useful if MPD needs to be a member of group such as "audio" to 73 | # have permission to use sound card. 74 | # 75 | #group "nogroup" 76 | # 77 | # This setting sets the address for the daemon to listen on. Careful attention 78 | # should be paid if this is assigned to anything other then the default, any. 79 | # This setting can deny access to control of the daemon. Choose any if you want 80 | # to have mpd listen on every address. Not effective if systemd socket 81 | # activation is in use. 82 | # 83 | # For network 84 | bind_to_address "localhost" 85 | # 86 | # And for Unix Socket 87 | #bind_to_address "/run/mpd/socket" 88 | # 89 | # This setting is the TCP port that is desired for the daemon to get assigned 90 | # to. 91 | # 92 | #port "6600" 93 | # 94 | # This setting controls the type of information which is logged. Available 95 | # setting arguments are "default", "secure" or "verbose". The "verbose" setting 96 | # argument is recommended for troubleshooting, though can quickly stretch 97 | # available resources on limited hardware storage. 98 | # 99 | #log_level "default" 100 | # 101 | # If you have a problem with your MP3s ending abruptly it is recommended that 102 | # you set this argument to "no" to attempt to fix the problem. If this solves 103 | # the problem, it is highly recommended to fix the MP3 files with vbrfix 104 | # (available as vbrfix in the debian archive), at which 105 | # point gapless MP3 playback can be enabled. 106 | # 107 | #gapless_mp3_playback "yes" 108 | # 109 | # Setting "restore_paused" to "yes" puts MPD into pause mode instead 110 | # of starting playback after startup. 111 | # 112 | #restore_paused "no" 113 | # 114 | # This setting enables MPD to create playlists in a format usable by other 115 | # music players. 116 | # 117 | #save_absolute_paths_in_playlists "no" 118 | # 119 | # This setting defines a list of tag types that will be extracted during the 120 | # audio file discovery process. The complete list of possible values can be 121 | # found in the mpd.conf man page. 122 | #metadata_to_use "artist,album,title,track,name,genre,date,composer,performer,disc" 123 | # 124 | # This setting enables automatic update of MPD's database when files in 125 | # music_directory are changed. 126 | # 127 | auto_update "yes" 128 | # 129 | # Limit the depth of the directories being watched, 0 means only watch 130 | # the music directory itself. There is no limit by default. 131 | # 132 | #auto_update_depth "3" 133 | # 134 | ############################################################################### 135 | 136 | 137 | # Symbolic link behavior ###################################################### 138 | # 139 | # If this setting is set to "yes", MPD will discover audio files by following 140 | # symbolic links outside of the configured music_directory. 141 | # 142 | #follow_outside_symlinks "yes" 143 | # 144 | # If this setting is set to "yes", MPD will discover audio files by following 145 | # symbolic links inside of the configured music_directory. 146 | # 147 | #follow_inside_symlinks "yes" 148 | # 149 | ############################################################################### 150 | 151 | 152 | # Zeroconf / Avahi Service Discovery ########################################## 153 | # 154 | # If this setting is set to "yes", service information will be published with 155 | # Zeroconf / Avahi. 156 | # 157 | zeroconf_enabled "yes" 158 | # 159 | # The argument to this setting will be the Zeroconf / Avahi unique name for 160 | # this MPD server on the network. 161 | # 162 | zeroconf_name "Transistor" 163 | # 164 | ############################################################################### 165 | 166 | 167 | # Permissions ################################################################# 168 | # 169 | # If this setting is set, MPD will require password authorization. The password 170 | # can setting can be specified multiple times for different password profiles. 171 | # 172 | #password "password@read,add,control,admin" 173 | # 174 | # This setting specifies the permissions a user has who has not yet logged in. 175 | # 176 | #default_permissions "read,add,control,admin" 177 | # 178 | ############################################################################### 179 | 180 | 181 | # Database ####################################################################### 182 | # 183 | 184 | #database { 185 | # plugin "proxy" 186 | # host "other.mpd.host" 187 | # port "6600" 188 | #} 189 | 190 | # Input ####################################################################### 191 | # 192 | 193 | input { 194 | plugin "curl" 195 | # proxy "proxy.isp.com:8080" 196 | # proxy_user "user" 197 | # proxy_password "password" 198 | } 199 | 200 | # 201 | ############################################################################### 202 | 203 | # Audio Output ################################################################ 204 | # 205 | # MPD supports various audio output types, as well as playing through multiple 206 | # audio outputs at the same time, through multiple audio_output settings 207 | # blocks. Setting this block is optional, though the server will only attempt 208 | # autodetection for one sound card. 209 | # 210 | #audio_output { 211 | # type "pulse" 212 | # name "MPD" 213 | # server "127.0.0.1" 214 | #} 215 | audio_output { 216 | type "alsa" 217 | name "MPD" 218 | device "pulse" 219 | mixer_control "Master" 220 | } 221 | # 222 | # An example of an ALSA output: 223 | # 224 | #audio_output { 225 | # type "alsa" 226 | # name "My ALSA Device" 227 | # device "hw:0,0" # optional 228 | # mixer_type "hardware" # optional 229 | # mixer_device "default" # optional 230 | # mixer_control "PCM" # optional 231 | # mixer_index "0" # optional 232 | #} 233 | # 234 | # An example of an OSS output: 235 | # 236 | #audio_output { 237 | # type "oss" 238 | # name "My OSS Device" 239 | # device "/dev/dsp" # optional 240 | # mixer_type "hardware" # optional 241 | # mixer_device "/dev/mixer" # optional 242 | # mixer_control "PCM" # optional 243 | #} 244 | # 245 | # An example of a shout output (for streaming to Icecast): 246 | # 247 | #audio_output { 248 | # type "shout" 249 | # encoder "vorbis" # optional 250 | # name "My Shout Stream" 251 | # host "localhost" 252 | # port "8000" 253 | # mount "/mpd.ogg" 254 | # password "hackme" 255 | # quality "5.0" 256 | # bitrate "128" 257 | # format "44100:16:1" 258 | # protocol "icecast2" # optional 259 | # user "source" # optional 260 | # description "My Stream Description" # optional 261 | # url "http://example.com" # optional 262 | # genre "jazz" # optional 263 | # public "no" # optional 264 | # timeout "2" # optional 265 | # mixer_type "software" # optional 266 | #} 267 | # 268 | # An example of a recorder output: 269 | # 270 | #audio_output { 271 | # type "recorder" 272 | # name "My recorder" 273 | # encoder "vorbis" # optional, vorbis or lame 274 | # path "/var/lib/mpd/recorder/mpd.ogg" 275 | ## quality "5.0" # do not define if bitrate is defined 276 | # bitrate "128" # do not define if quality is defined 277 | # format "44100:16:1" 278 | #} 279 | # 280 | # An example of a httpd output (built-in HTTP streaming server): 281 | # 282 | #audio_output { 283 | # type "httpd" 284 | # name "My HTTP Stream" 285 | # encoder "vorbis" # optional, vorbis or lame 286 | # port "8000" 287 | # bind_to_address "0.0.0.0" # optional, IPv4 or IPv6 288 | # quality "5.0" # do not define if bitrate is defined 289 | # bitrate "128" # do not define if quality is defined 290 | # format "44100:16:1" 291 | # max_clients "0" # optional 0=no limit 292 | #} 293 | # 294 | # An example of a pulseaudio output (streaming to a remote pulseaudio server) 295 | # Please see README.Debian if you want mpd to play through the pulseaudio 296 | # daemon started as part of your graphical desktop session! 297 | # 298 | #audio_output { 299 | # type "pulse" 300 | # name "My Pulse Output" 301 | # server "remote_server" # optional 302 | # sink "remote_server_sink" # optional 303 | #} 304 | # 305 | # An example of a winmm output (Windows multimedia API). 306 | # 307 | #audio_output { 308 | # type "winmm" 309 | # name "My WinMM output" 310 | # device "Digital Audio (S/PDIF) (High Definition Audio Device)" # optional 311 | # or 312 | # device "0" # optional 313 | # mixer_type "hardware" # optional 314 | #} 315 | # 316 | # An example of an openal output. 317 | # 318 | #audio_output { 319 | # type "openal" 320 | # name "My OpenAL output" 321 | # device "Digital Audio (S/PDIF) (High Definition Audio Device)" # optional 322 | #} 323 | # 324 | ## Example "pipe" output: 325 | # 326 | #audio_output { 327 | # type "pipe" 328 | # name "my pipe" 329 | # command "aplay -f cd 2>/dev/null" 330 | ## Or if you're want to use AudioCompress 331 | # command "AudioCompress -m | aplay -f cd 2>/dev/null" 332 | ## Or to send raw PCM stream through PCM: 333 | # command "nc example.org 8765" 334 | # format "44100:16:2" 335 | #} 336 | # 337 | ## An example of a null output (for no audio output): 338 | # 339 | #audio_output { 340 | # type "null" 341 | # name "My Null Output" 342 | # mixer_type "none" # optional 343 | #} 344 | # 345 | # If MPD has been compiled with libsamplerate support, this setting specifies 346 | # the sample rate converter to use. Possible values can be found in the 347 | # mpd.conf man page or the libsamplerate documentation. By default, this is 348 | # setting is disabled. 349 | # 350 | #samplerate_converter "Fastest Sinc Interpolator" 351 | # 352 | ############################################################################### 353 | 354 | 355 | # Normalization automatic volume adjustments ################################## 356 | # 357 | # This setting specifies the type of ReplayGain to use. This setting can have 358 | # the argument "off", "album", "track" or "auto". "auto" is a special mode that 359 | # chooses between "track" and "album" depending on the current state of 360 | # random playback. If random playback is enabled then "track" mode is used. 361 | # See for more details about ReplayGain. 362 | # This setting is off by default. 363 | # 364 | #replaygain "album" 365 | # 366 | # This setting sets the pre-amp used for files that have ReplayGain tags. By 367 | # default this setting is disabled. 368 | # 369 | #replaygain_preamp "0" 370 | # 371 | # This setting sets the pre-amp used for files that do NOT have ReplayGain tags. 372 | # By default this setting is disabled. 373 | # 374 | #replaygain_missing_preamp "0" 375 | # 376 | # This setting enables or disables ReplayGain limiting. 377 | # MPD calculates actual amplification based on the ReplayGain tags 378 | # and replaygain_preamp / replaygain_missing_preamp setting. 379 | # If replaygain_limit is enabled MPD will never amplify audio signal 380 | # above its original level. If replaygain_limit is disabled such amplification 381 | # might occur. By default this setting is enabled. 382 | # 383 | #replaygain_limit "yes" 384 | # 385 | # This setting enables on-the-fly normalization volume adjustment. This will 386 | # result in the volume of all playing audio to be adjusted so the output has 387 | # equal "loudness". This setting is disabled by default. 388 | # 389 | #volume_normalization "no" 390 | # 391 | ############################################################################### 392 | 393 | 394 | # Character Encoding ########################################################## 395 | # 396 | # If file or directory names do not display correctly for your locale then you 397 | # may need to modify this setting. 398 | # 399 | filesystem_charset "UTF-8" 400 | # 401 | # This setting controls the encoding that ID3v1 tags should be converted from. 402 | # 403 | id3v1_encoding "UTF-8" 404 | # 405 | ############################################################################### 406 | 407 | 408 | # SIDPlay decoder ############################################################# 409 | # 410 | # songlength_database: 411 | # Location of your songlengths file, as distributed with the HVSC. 412 | # The sidplay plugin checks this for matching MD5 fingerprints. 413 | # See http://www.c64.org/HVSC/DOCUMENTS/Songlengths.faq 414 | # 415 | # default_songlength: 416 | # This is the default playing time in seconds for songs not in the 417 | # songlength database, or in case you're not using a database. 418 | # A value of 0 means play indefinitely. 419 | # 420 | # filter: 421 | # Turns the SID filter emulation on or off. 422 | # 423 | #decoder { 424 | # plugin "sidplay" 425 | # songlength_database "/media/C64Music/DOCUMENTS/Songlengths.txt" 426 | # default_songlength "120" 427 | # filter "true" 428 | #} 429 | # 430 | ############################################################################### 431 | 432 | --------------------------------------------------------------------------------