├── .gitignore ├── Changelog.md ├── LICENSE ├── README.md ├── aml-imgpack.py ├── build_image.sh ├── create_dist.sh ├── files ├── Readme.md ├── data │ ├── etc │ │ ├── X11 │ │ │ ├── xorg.conf.landscape │ │ │ └── xorg.conf.portrait │ │ ├── fstab │ │ └── inittab │ ├── lib │ │ └── systemd │ │ │ └── system │ │ │ ├── backlight.service │ │ │ ├── buttons.service │ │ │ ├── chromium.service │ │ │ ├── usbgadget.service │ │ │ └── vnc.service │ └── scripts │ │ ├── buttons_app.py │ │ ├── buttons_settings.py │ │ ├── chromium_settings.sh │ │ ├── clear_display.sh │ │ ├── requirements.txt │ │ ├── setup_backlight.sh │ │ ├── setup_display.sh │ │ ├── setup_usbgadget.sh │ │ ├── setup_vnc.sh │ │ ├── start_buttons.sh │ │ ├── start_chromium.sh │ │ └── vnc_passwd ├── env │ ├── env_abb.txt │ ├── env_stock.txt │ └── env_switchable.txt ├── logo │ ├── Readme.md │ ├── bad_charger.bmp │ ├── bootup.bmp │ ├── bootup_spotify.bmp │ ├── upgrade_bar.bmp │ ├── upgrade_error.bmp │ ├── upgrade_fail.bmp │ ├── upgrade_logo.bmp │ ├── upgrade_success.bmp │ ├── upgrade_unfocus.bmp │ └── upgrade_upgrading.bmp └── system_a │ └── etc │ ├── fstab │ ├── init.d │ └── S49usbgadget │ └── inittab ├── flash_test_image.sh ├── image_config.sh ├── pictures ├── superbird_ha_landscape.jpg ├── superbird_ha_portrait.jpg ├── superbird_landscape_back.jpg ├── superbird_poe.jpg └── superbird_wall_mount.jpg ├── setup_host.sh └── update_local.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/.DS_Store 3 | ._DS_Store 4 | **/.DS_Store 5 | 6 | temp/ 7 | tmp/ 8 | old/ 9 | output/ 10 | dumps 11 | superbird_tool* 12 | dumps/ 13 | research/ 14 | rootfs 15 | rootfs.tar.gz 16 | modules/ 17 | dist/ 18 | headers/ 19 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | v1.8 4 | * customized the boot logos a little bit, using [`aml-imgpack`](https://github.com/bishopdynamics/aml-imgpack) 5 | 6 | v1.7 7 | * switch from `x11vnc` to `tigervnc-scraping-server` for better vnc performance 8 | * default password `superbird`, to change: `sudo vncpasswd /scripts/vnc_passwd`, and `sudo systemctl restart vnc.service` 9 | * vnc password now survives upgrade via `update_local.sh` 10 | 11 | v1.6 12 | * `build_image.sh` can now create full image from a stock dump without extra steps 13 | * `image_config.sh` now contains all user-configurable values for building and updating image 14 | * the 256MB `settings` partition is now mounted at `/config/`, used for chromium user profile 15 | * reorganize files to make it clearer where they go 16 | 17 | v1.5.1 18 | * fixed oops: missing `/scripts/chromium_settings.sh` in prebuilt image 19 | 20 | v1.5 21 | * moved chromium settings into separate file `/scripts/chromium_settings.sh` 22 | * added buttons service, configure in `/scripts/buttons_settings.py` 23 | * need to provide your Home Assistant url, and long-lived token 24 | * control a light entity brightness by turning knob, pressing toggles on/off 25 | * recall scene/automation/script using buttons along edge, and button next to knob 26 | * ditched the heredoc'd files in `install_debian.sh`, reorganized files needed for it 27 | * added `update_local.sh` which can update an already-running local device running a previous release 28 | 29 | Starting with this release, your settings are stored in `/scripts/chromium_settings.sh` and `/scripts/buttons_settings.py`, and those two files will NOT be touched during subsequent upgrades using `update_local.sh`, so your settings will survive upgrades. 30 | However, your existing settings will NOT be migrated, so if you use `update_local.sh` to upgrade an existing device you will then need to edit those two files. 31 | 32 | You should setup ssh key with superbird first, so that you don't have to type the password a bunch of times during upgrade. 33 | 34 | If you are coming from v1.2 you should flash the image from Releases instead, you will end up with much more free space. 35 | 36 | v1.4 37 | * added back some python packages for fun 38 | * added `--local_proxy` flag to `install_debian.sh`, to use a local instance of apt-cacher-ng 39 | * added some helper scripts for creating an image for release 40 | * switch to main debian mirror, `http://deb.debian.org/debian/` 41 | * use x11vnc `-loop` flag instead of our own loop 42 | * remove ~10px black border around chromium (more pixels!) 43 | * hide scrollbars in chromium 44 | * chromium service (including X11) now logs to `/var/log/chromium.log` 45 | 46 | v1.3 47 | * hide cursor in chromium 48 | * add xorg.conf entries for buttons and knob 49 | * fix issue with landscape touch input doubling 50 | * remove a couple unnecessary packages to free up space 51 | * fix incorrect kernel modules in /lib/modules 52 | * remove unnecessary hardcoded chromium width and height 53 | * remove unnecessary hardcoded vnc server width and height 54 | * make vnc survive restart of X11 55 | * clear display when chromium.service stops 56 | 57 | v1.2 58 | * initial release 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bishop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wall Thing: Debian Chromium Kiosk on Spotify Car Thing (superbird) 2 | 3 | This is a prebuilt image of Debian 13 (Trixie) for the Spotify Car Thing, aka superbird. 4 | It combines the stock kernel with a debian rootfs, and launches a fullscreen Chromium kiosk. I like to use it with Home Assistant. 5 | 6 | Home Assistant on Car Thing 7 | Home Assistant on Car Thing 8 | 9 | 10 | This image will remove the default Spotify functionality. You should definitely [make a full backup](https://github.com/bishopdynamics/superbird-tool) before proceeding! 11 | 12 | Default user and password are both `superbird` 13 | 14 | ![Latest Release](https://img.shields.io/github/v/release/bishopdynamics/superbird-debian-kiosk?logo=github) 15 | 16 | ## Features 17 | 18 | Working: 19 | * Debian 13 (Trixie) aarch64 20 | * Framebuffer display working with X11, in portrait or landscape, with touch input 21 | * Networking via USB RNDIS (requires a host device) 22 | * Automatic blacklight on/off with display wake/sleep 23 | * VNC and SSH (forwarded through host device) 24 | * Chromium browser, fullscreen kiosk mode 25 | * Buttons and dial used to control a light and recall scenes/automations/scripts on Home Assistant 26 | * 256MB `settings` partition used for Chromium user profile 27 | 28 | Available, but not used in this image: 29 | 30 | * Bluetooth 31 | * Backlight brightness control (currently fixed at 100) 32 | * Audio (mic array, DSP) 33 | 34 | Not working: 35 | * Wifi 36 | * GPU acceleration 37 | 38 | WiFi is technically possible on this hardware, but the stock bootloaders and kernel disable it. 39 | It might be possible to cherry-pick the wifi information from the Radxa Zero device tree (practically the same SoC), but I think you would need to rebuild one or more of the bootloader stages to make it work. 40 | 41 | GPU: the hardware has a Mali GPU, but the stock OS uses it via DirectFB QT library, and does not include necessary libraries to make it work with X11. It may be possible to grab the needed files from Radxa Zero. 42 | 43 | 44 | ## Boot Modes 45 | 46 | After installation, you will have 3 different boot options, depending on what buttons are held: 47 | 48 | * Debian Mode - default, no buttons held 49 | * bootlogo says Debian Trixie 50 | * kernel is `boot_a` root is `data` 51 | 52 | * Utility Mode - hold button 1 53 | * bootlogo says Utility Mode 54 | * kernel is `boot_a` root is `system_a` 55 | * adb and already configured 56 | * scripts to install debian 57 | 58 | * USB Burn Mode - hold button 4 59 | * bootlogo says USB Burn Mode 60 | 61 | 62 | ## Installation 63 | 64 | ### Requirements: 65 | * Spotify Car Thing 66 | * another device to act as host, such as Radxa Zero, Rockpi S, Raspberry Pi 4, etc 67 | * a USB cable to connect the two 68 | * power supply for the host device 69 | * a desktop/laptop for flashing the image to the Car Thing 70 | 71 | 72 | ### Setup: 73 | 1. Download and extract the latest image from [Releases](https://github.com/bishopdynamics/superbird-debian-kiosk/releases) 74 | 2. Put your device in burn mode by holding buttons 1 & 4 while plugging into usb port 75 | 1. avoid using a USB hub, you will have issues flashing the image 76 | 3. Use the latest version of [superbird-tool](https://github.com/bishopdynamics/superbird-tool) to flash the extracted image folder: 77 | 78 | ```bash 79 | # root may be needed, check superbird-tool readme for platform-specific usage 80 | # make sure your device is found 81 | python3 superbird_tool.py --find_device 82 | # restore the entire folder to your device 83 | python3 superbird_tool.py --restore_device ~/Downloads/debian_v1.2_2023-12-19 84 | ``` 85 | 86 | 4. Configure a host system 87 | 1. Select a host device. I have tested: 88 | 1. [Radxa Zero](pictures/superbird_wall_mount.jpg) with [Armbian](https://www.armbian.com/radxa-zero/) Jammy Minimal CLI 89 | 1. The Armbian Bookworm release did not work with USB burn mode, but works fine as a host just for networking 90 | 2. [Radxa Rockpi S](pictures/superbird_landscape_back.jpg) ([with a PoE hat!](pictures/superbird_poe.jpg)), also with Armbian Jammy 91 | 3. Raspberry Pi 4B, with Raspi OS Bookworm Lite 92 | 2. Copy and run `setup_host.sh` on the host device (as root), and reboot 93 | 3. Connect the Car Thing into the host device and power it up 94 | 5. ssh to the host device, and then you should be able to ssh to the Car Thing (user and password are both `superbird`) : 95 | ```bash 96 | # script added entry in /etc/hosts, use hostname "superbird" from host device 97 | ssh superbird@superbird 98 | # or by ip (host device is 192.168.7.1, superbird is 192.168.7.2) 99 | ssh superbird@192.168.7.2 100 | ``` 101 | 1. From another device on the same network, you should be able to ssh directly to the Car Thing using port 2022: 102 | ```bash 103 | # where "host-device" is the hostname or ip of your host device 104 | ssh -p 2022 superbird@host-device 105 | ``` 106 | 1. Once you have ssh access to the Car Thing, edit some things: 107 | 1. Probably change password 108 | 2. Edit `/scripts/chromium_settings.sh` to change what URL to launch in the kiosk 109 | 1. Restart X11 and Chromium with: `sudo systemctl restart chromium.service` 110 | 3. Edit `/scripts/buttons_settings.py` to change Home Assistant URL and add long-lived token for access 111 | 1. assign scenes/automations/scripts to buttons, assign a light entity to the knob 112 | 2. Restart buttons script with: `sudo systemctl restart buttons.service` 113 | 4. Edit `/etc/X11/xorg.conf` to adjust screen timeout (default 10 mins), orientation (default portrait) 114 | 1. for landscape, un-comment lines `38` and `71` 115 | 5. Edit `/scripts/setup_display.sh` and `/scripts/setup_backlight.sh` to adjust backlight brightness (default 100) 116 | 1. Restart backlight script with: `sudo systemctl restart backlight.service` 117 | 6. Change vnc password: `sudo vncpasswd /scripts/vnc_passwd` 118 | 1. Restart vnc server with: `sudo systemctl restart vnc.service` 119 | 2. Using your favorite VNC client, connect by VNC to the host device's address, port 5900, if you need to interact with a page (sign in) 120 | 3. ? 121 | 4. Profit 122 | 123 | 124 | ## How to build the image 125 | 126 | 1. using [superbird-tool](https://github.com/bishopdynamics/superbird-tool), use `--dump_device` to dump a stock device into `./dumps/debian_current/` 127 | 2. run `./build_image.sh`, which will: 128 | 1. replace `env.txt` with switchable version (see [`files/env/env_switchable.txt`](files/env/env_switchable.txt)) 129 | 2. modify `system_a` partition for Utility Mode: 130 | 1. install usb gadget for ADB (see [`files/system_a/etc/init.d/S49usbgadget`](files/system_a/etc/init.d/S49usbgadget)) 131 | 2. modify `/etc/fstab` and `/etc/inittab` to not mount `data` or `settings` partitions (see [`files/system_a/etc/`](files/system_a/etc)) 132 | 3. format `settings` partition 133 | 4. format `data` partition, and: 134 | 1. use debootstrap to create a minimal debian root filesystem, plus a few extra packages 135 | 1. `systemd systemd-sysv dbus kmod usbutils htop nano tree file less locales sudo dialog apt wget curl iputils-ping iputils-tracepath iputils-arping iproute2 net-tools openssh-server ntp xserver-xorg-core xserver-xorg-video-fbdev xterm xinit x11-xserver-utils shared-mime-info xserver-xorg-input-evdev libinput-bin xserver-xorg-input-libinput xinput fbset x11vnc chromium python3-minimal python3-pip` 136 | 2. python packages from [`requirements.txt`](files/data/scripts/requirements.txt) 137 | 2. copy `/lib/modules/4.9.113` from `system_a` 138 | 3. configure X11 via [`/etc/X11/xorg.conf`](files/data/etc/X11/xorg.conf.portrait) 139 | 4. set hostname to `superbird` (configure in [`image_config.sh`](image_config.sh)) 140 | 5. add entry to `/etc/hosts` to resolve `host` as `192.168.7.1` (host device) 141 | 6. create regular user `superbird`, password: `superbird`, with passwordless sudo (configure in [`image_config.sh`](image_config.sh)) 142 | 7. install scripts to `/scripts/` (see [`files/data/scripts/`](files/data/scripts)) 143 | 8. install services to `/lib/systemd/system/` (see [`files/data/lib/systemd/system/`](files/data/lib/systemd/system)) 144 | 9. set locale to `en_US.UTF-8` 145 | 10. set timezone to `America/Los_Angeles` 146 | 11. add entry to `/etc/fstab` to mount `settings` partition at `/config` (for chromium profile) (see [`files/data/etc/fstab`](files/data/etc/fstab)) 147 | 12. add entry to `/etc/inittab` to enable serial console at 115200 baud (see [`files/data/etc/inittab`](files/data/etc/inittab)) 148 | 13. generate new image for `logo` partition using [`files/logo/*.bmp`](files/logo) 149 | 3. You now have an image at `./dumps/debian_current/` ready to flash to device using [superbird-tool](https://github.com/bishopdynamics/superbird-tool) 150 | 151 | 152 | Hint: Install `apt-cacher-ng` and then run `./build_image.sh --local_proxy` to use locally cached packages (avoid re-downloading packages every time, much faster) 153 | 154 | 155 | ## Warranty and Liability 156 | 157 | None. You definitely can mess up your device in ways that are difficult to recover. I cannot promise a bug in this script will not brick your device. 158 | By using this tool, you accept responsibility for the outcome. 159 | 160 | I highly recommend connecting to the UART console, [frederic's repo](https://github.com/frederic/superbird-bulkcmd) has some good pictures showing where the pads are. 161 | 162 | Make backups. 163 | -------------------------------------------------------------------------------- /aml-imgpack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Resource packer/unpacker for Amlogic Logo image files 4 | License: GPL-2.0 5 | https://github.com/bishopdynamics/aml-imgpack 6 | """ 7 | # pylint: disable=line-too-long,missing-class-docstring,missing-function-docstring,consider-using-f-string,invalid-name,broad-exception-raised,protected-access 8 | 9 | from __future__ import annotations 10 | 11 | import struct 12 | import argparse 13 | import binascii 14 | 15 | from pathlib import Path 16 | 17 | AML_RES_IMG_VERSION_V1 = 0x01 18 | AML_RES_IMG_VERSION_V2 = 0x02 19 | AML_RES_IMG_ITEM_ALIGN_SZ = 16 20 | AML_RES_IMG_VERSION = 0x01 21 | AML_RES_IMG_V1_MAGIC_LEN = 8 22 | AML_RES_IMG_V1_MAGIC = b'AML_RES!' # 8 chars 23 | AML_RES_IMG_HEAD_SZ = AML_RES_IMG_ITEM_ALIGN_SZ * 4 # 64 24 | AML_RES_ITEM_HEAD_SZ = AML_RES_IMG_ITEM_ALIGN_SZ * 4 # 64 25 | IH_MAGIC = 0x27051956 # Image Magic Number 26 | IH_NMLEN = 32 # Image Name Length 27 | ARCH_ARM = 8 28 | 29 | 30 | # typedef struct { 31 | # __u32 crc; //crc32 value for the resouces image 32 | # __s32 version;//current version is 0x01 33 | # __u8 magic[AML_RES_IMG_V1_MAGIC_LEN]; //resources images magic 34 | # __u32 imgSz; //total image size in byte 35 | # __u32 imgItemNum;//total item packed in the image 36 | # __u32 alignSz;//AML_RES_IMG_ITEM_ALIGN_SZ 37 | # __u8 reserv[AML_RES_IMG_HEAD_SZ - 8 * 3 - 4]; 38 | # }AmlResImgHead_t; 39 | 40 | # typedef struct pack_header{ 41 | # unsigned int magic; /* Image Header Magic Number */ 42 | # unsigned int hcrc; /* Image Header CRC Checksum */ 43 | # unsigned int size; /* Image Data Size */ 44 | # unsigned int start; /* item data offset in the image*/ 45 | # unsigned int end; /* Entry Point Address */ 46 | # unsigned int next; /* Next item head offset in the image*/ 47 | # unsigned int dcrc; /* Image Data CRC Checksum */ 48 | # unsigned char index; /* Operating System */ 49 | # unsigned char nums; /* CPU architecture */ 50 | # unsigned char type; /* Image Type */ 51 | # unsigned char comp; /* Compression Type */ 52 | # char name[IH_NMLEN]; /* Image Name */ 53 | # }AmlResItemHead_t; 54 | 55 | 56 | class AmlResourcesImage(object): 57 | def __init__(self): 58 | self.header = AmlResImgHead() 59 | self.items = [] 60 | 61 | @classmethod 62 | def unpack_from(cls, fp) -> AmlResourcesImage: 63 | img = cls() 64 | fp.seek(0) 65 | img.header = AmlResImgHead.unpack_from(fp) 66 | while True: 67 | item = AmlResItem.unpack_from(fp) 68 | img.items.append(item) 69 | if item.next == 0: 70 | break 71 | fp.seek(item.next) 72 | return img 73 | 74 | def pack(self) -> bytes: 75 | packed = bytes() 76 | 77 | data_pack = bytes() 78 | for item in self.items: 79 | item.start = len(data_pack) + AmlResImgHead._size + (AmlResItem._size * len(self.items)) 80 | item.size = len(item.data) 81 | data_pack += item.data 82 | data_pack += struct.pack("%ds" % (len(data_pack) % self.header.alignSz), b"\0" * self.header.alignSz) 83 | 84 | for i, item in enumerate(self.items): 85 | item.index = i 86 | if i < (len(self.items) - 1): 87 | item.next = AmlResImgHead._size + (AmlResItem._size * (i + 1)) 88 | packed += item.pack() 89 | self.header.imgItemNum = len(self.items) 90 | self.header.imgSz = len(packed) + AmlResImgHead._size 91 | return self.header.pack() + packed + data_pack 92 | 93 | 94 | class AmlResItem: 95 | _format = "IIIIIIIBBBB%ds" % IH_NMLEN 96 | _size = struct.calcsize(_format) 97 | magic = IH_MAGIC 98 | hcrc = 0 99 | size = 0 100 | start = 0 101 | end = 0 102 | next = 0 103 | dcrc = 0 104 | index = 0 105 | nums = ARCH_ARM 106 | type = 0 107 | comp = 0 108 | name = "" 109 | data = "" 110 | 111 | @classmethod 112 | def from_file(cls, file:Path) -> AmlResItem: 113 | item = cls() 114 | with open(file, mode='br') as fp: 115 | item.data = fp.read() 116 | item.dcrc = binascii.crc32(item.data) & 0xFFFFFFFF 117 | item.size = len(item.data) 118 | item.name = file.stem 119 | return item 120 | 121 | @classmethod 122 | def unpack_from(cls, fp) -> AmlResItem: 123 | h = cls() 124 | h.magic, h.hcrc, h.size, h.start, h.end, h.next, h.dcrc, h.index, \ 125 | h.nums, h.type, h.comp, h.name = struct.unpack(h._format, fp.read(h._size)) 126 | h.name = h.name.rstrip(b'\0') 127 | if h.magic != IH_MAGIC: 128 | raise Exception("Invalid item header magic, should 0x%x, is 0x%x" % (IH_MAGIC, h.magic)) 129 | fp.seek(h.start) 130 | h.data = fp.read(h.size) 131 | return h 132 | 133 | def pack(self) -> bytes: 134 | packed = struct.pack(self._format, self.magic, self.hcrc, self.size, self.start, self.end, self.next, self.dcrc, self.index, self.nums,self.type, self.comp, self.name.encode('utf-8')) 135 | return packed 136 | 137 | def __repr__(self) -> str: 138 | return "AmlResItem(name=%s start=0x%x size=%d)" % (self.name, self.start, self.size) 139 | 140 | 141 | class AmlResImgHead(object): 142 | _format = "Ii%dsIII%ds" % (AML_RES_IMG_V1_MAGIC_LEN, AML_RES_IMG_HEAD_SZ - 8 * 3 - 4) 143 | _size = struct.calcsize(_format) 144 | crc = 0 145 | version = AML_RES_IMG_VERSION_V2 146 | magic = AML_RES_IMG_V1_MAGIC 147 | imgSz = 0 148 | imgItemNum = 0 149 | alignSz = AML_RES_IMG_ITEM_ALIGN_SZ 150 | reserv = "" 151 | 152 | @classmethod 153 | def unpack_from(cls, fp) -> AmlResImgHead: 154 | h = cls() 155 | h.crc, h.version, h.magic, h.imgSz, h.imgItemNum, h.alignSz, h.reserv = struct.unpack(h._format, fp.read(h._size)) 156 | if h.magic != AML_RES_IMG_V1_MAGIC: 157 | raise Exception("Magic is not right, should %s, is %s" % (AML_RES_IMG_V1_MAGIC, h.magic)) 158 | if h.version > AML_RES_IMG_VERSION_V2: 159 | raise Exception("res-img version %d not supported" % h.version) 160 | return h 161 | 162 | def pack(self) -> bytes: 163 | packed = struct.pack(self._format, self.crc, self.version, self.magic, self.imgSz, self.imgItemNum, self.alignSz, self.reserv.encode('utf-8')) 164 | return packed 165 | 166 | def __repr__(self) -> str: 167 | return "AmlResImgHead(crc=0x%x version=%d imgSz=%d imgItemNum=%d alignSz=%d)" % \ 168 | (self.crc, self.version, self.imgSz, self.imgItemNum, self.alignSz) 169 | 170 | 171 | 172 | def list_items(logo_img_file): 173 | print("Listing assets in %s" % logo_img_file) 174 | with open(logo_img_file, mode='rb') as fp: 175 | img = AmlResourcesImage.unpack_from(fp) 176 | print(img.header) 177 | for item in img.items: 178 | print(" %s" % item) 179 | 180 | 181 | def unpack_image_file(logo_img_file): 182 | print("Unpacking assets in %s" % logo_img_file) 183 | with open(logo_img_file, mode='rb') as fp: 184 | img = AmlResourcesImage.unpack_from(fp) 185 | for item in img.items: 186 | print(" Unpacking %s" % item.name.decode('utf-8')) 187 | with open("%s.bmp" % item.name.decode('utf-8'), "wb") as item_fp: 188 | item_fp.write(item.data) 189 | 190 | 191 | def pack_image_file(outfile, assets): 192 | print("Packing files in %s:" % outfile) 193 | img = AmlResourcesImage() 194 | img.items = [] 195 | for asset in assets: 196 | img.items.append(AmlResItem.from_file(Path(asset))) 197 | for item in img.items: 198 | print(" %s (%d bytes)" % (item.name, item.size)) 199 | with open(outfile, "wb") as fp: 200 | fp.write(img.pack()) 201 | 202 | 203 | def main(): 204 | parser = argparse.ArgumentParser(description='Pack and unpack amlogic uboot images') 205 | parser.add_argument("--unpack", help="Unpack image file", action="store_true") 206 | parser.add_argument("--pack", help="Pack image file") 207 | parser.add_argument('assets', metavar='file', type=str, nargs='+', help='an integer for the accumulator') 208 | 209 | args = parser.parse_args() 210 | if args.unpack: 211 | unpack_image_file(args.assets[0]) 212 | elif args.pack: 213 | pack_image_file(args.pack, args.assets) 214 | else: 215 | list_items(args.assets[0]) 216 | 217 | main() 218 | -------------------------------------------------------------------------------- /build_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2129 3 | 4 | # build debian image, intended to run on a debian 11 arm64 host 5 | # expects an existing dump at ./dumps/debian_current/ 6 | # ./dumps is ignored by git 7 | # add --local_proxy flag argument to try to use local instance of apt-cacher-ng at localhost:3142 8 | 9 | set -e 10 | 11 | # all config lives in image_config.sh 12 | source ./image_config.sh 13 | 14 | 15 | ################################################ Additional Packages ################################################ 16 | 17 | # init system, either systemd or sysvinit 18 | # without systemd-sysv, no reboot/shutdown commands 19 | PACKAGES="systemd systemd-sysv dbus kmod" 20 | # base packages 21 | PACKAGES="$PACKAGES usbutils htop nano tree file less locales sudo dialog apt" 22 | # stuff for networking 23 | PACKAGES="$PACKAGES wget curl iputils-ping iputils-tracepath iputils-arping iproute2 net-tools openssh-server ntp" 24 | # minimal xorg 25 | PACKAGES="$PACKAGES xserver-xorg-core xserver-xorg-video-fbdev xterm xinit x11-xserver-utils shared-mime-info" 26 | # xorg input 27 | PACKAGES="$PACKAGES xserver-xorg-input-evdev libinput-bin xserver-xorg-input-libinput xinput" 28 | # additional required tools 29 | PACKAGES="$PACKAGES fbset tigervnc-scraping-server" 30 | 31 | # NOTE: we cannot install chromium at at the debootstrap stage 32 | # so we install chromium and other packages in a separate stage using chroot 33 | 34 | STAGE2_PACKAGES="chromium python3-minimal python3-pip $EXTRA_PACKAGES" 35 | 36 | 37 | ################################################ Running Variables ################################################ 38 | 39 | KERNEL_VERSION="4.9.113" # this is the kernel that comes with superbird, we dont have any other kernel 40 | 41 | ENV_FILE="./files/env/env_switchable.txt" # env file to replace existing 42 | FILES_SYS="./files/system_a" 43 | FILES_DATA="./files/data" 44 | TEMP_DIR="./temp" 45 | 46 | SYS_PATH="${TEMP_DIR}/system_a" # this is where we will mount system_a partition to modify, and get modules 47 | INSTALL_PATH="${TEMP_DIR}/data" # this is where we will mount data partition to perform install 48 | 49 | CSV_PACKAGES=$(echo "$PACKAGES"| tr ' ' ',') # need comma-separated list of packages for debootstrap 50 | 51 | 52 | ################################################ Functions ################################################ 53 | 54 | in_target() { 55 | # run command(s) within the chroot 56 | chroot "${INSTALL_PATH}" "$@" 57 | } 58 | 59 | install_script() { 60 | # copy the named script into target /scripts/ and make it executable, owned by superbird 61 | SCR_NAME="$1" 62 | echo "Installing script: $SCR_NAME" 63 | cp "${FILES_DATA}/scripts/$SCR_NAME" "${INSTALL_PATH}/scripts/$SCR_NAME" 64 | chmod +x "${INSTALL_PATH}/scripts/$SCR_NAME" 65 | in_target chown "$USER_NAME" "/scripts/$SCR_NAME" 66 | } 67 | 68 | install_service() { 69 | # copy named service file into taret /lib/systemd/system/, symlink it into multi-user.target.wants, and make it owned by superbird 70 | SVC_NAME="$1" 71 | echo "Installing service: $SVC_NAME" 72 | cp "${FILES_DATA}/lib/systemd/system/$SVC_NAME" "${INSTALL_PATH}/lib/systemd/system/$SVC_NAME" 73 | in_target chown "$USER_NAME" "/lib/systemd/system/$SVC_NAME" 74 | in_target ln -s "/lib/systemd/system/$SVC_NAME" "/etc/systemd/system/multi-user.target.wants/$SVC_NAME" 75 | } 76 | 77 | 78 | ################################################ Entrypoint ################################################ 79 | 80 | echo "Going to install Debian $DISTRO_BRANCH $DISTRO_VARIANT $ARCHITECTURE into image at $EXISTING_DUMP" 81 | 82 | # need to be root 83 | if [ "$(id -u)" != "0" ]; then 84 | echo "Must be run as root" 85 | exit 1 86 | fi 87 | 88 | if [ "$(uname -s)" != "Linux" ]; then 89 | echo "Only works on Linux!" 90 | exit 1 91 | fi 92 | 93 | if [ -z "$EXISTING_DUMP" ] || [ ! -d "$EXISTING_DUMP" ]; then 94 | echo "Need to provide an existing dump for us to modify" 95 | echo "ex: $0 ./dumps/debian_current" 96 | exit 1 97 | fi 98 | if [ ! -f "${EXISTING_DUMP}/data.ext4" ]; then 99 | echo "Missing expected ${EXISTING_DUMP}/data.ext4" 100 | exit 1 101 | fi 102 | if [ ! -f "${EXISTING_DUMP}/system_a.ext2" ]; then 103 | echo "Missing expected ${EXISTING_DUMP}/system_a.ext2" 104 | exit 1 105 | fi 106 | if [ ! -f "${EXISTING_DUMP}/settings.ext4" ]; then 107 | echo "Missing expected ${EXISTING_DUMP}/settings.ext4" 108 | exit 1 109 | fi 110 | 111 | mkdir -p ${TEMP_DIR} 112 | 113 | 114 | ################################################ Modify env ################################################ 115 | 116 | cp "$ENV_FILE" "${EXISTING_DUMP}/env.txt" 117 | # we dont need to keep env.dump, superbird_tool prefers env.txt, safer way to deal with env 118 | if [ -f "${EXISTING_DUMP}/env.dump" ]; then 119 | rm "${EXISTING_DUMP}/env.dump" 120 | fi 121 | 122 | ################################################ Format Partitions ################################################ 123 | 124 | echo "formatting ${EXISTING_DUMP}/data.ext4" 125 | mountpoint "$INSTALL_PATH" && umount "$INSTALL_PATH" 126 | mkfs.ext4 -F "${EXISTING_DUMP}/data.ext4" || { 127 | echo "failed to format data (or user cancelled format), quitting" 128 | exit 1 129 | } 130 | 131 | mkfs.ext4 -F "${EXISTING_DUMP}/settings.ext4" || { 132 | echo "failed to format settings (or user cancelled format), quitting" 133 | exit 1 134 | } 135 | 136 | ################################################ Mount Partitions ################################################ 137 | 138 | mkdir -p "$INSTALL_PATH" 139 | mount -o loop "${EXISTING_DUMP}/data.ext4" "$INSTALL_PATH" 140 | mkdir -p "$SYS_PATH" 141 | mount -o loop "${EXISTING_DUMP}/system_a.ext2" "$SYS_PATH" 142 | 143 | 144 | ################################################ Install Packages ################################################ 145 | echo "Installing packages: $CSV_PACKAGES" 146 | echo "" 147 | 148 | # use local apt-cacher-ng instance 149 | if [ "$1" = "--local_proxy" ]; then 150 | export http_proxy=http://127.0.0.1:3142 151 | echo "Using local apt-cacher-ng proxy at: ${http_proxy}" 152 | echo "" 153 | fi 154 | 155 | echo "Debootstrap: debootstrap --variant=$DISTRO_VARIANT --no-check-gpg --arch=$ARCHITECTURE $DISTRO_BRANCH $INSTALL_PATH $DISTRO_REPO_URL" 156 | echo "" 157 | 158 | debootstrap --verbose --variant="$DISTRO_VARIANT" --no-check-gpg --include="$CSV_PACKAGES" --arch="$ARCHITECTURE" "$DISTRO_BRANCH" "$INSTALL_PATH" "$DISTRO_REPO_URL" 159 | 160 | in_target apt update 161 | in_target apt install -y --no-install-recommends --no-install-suggests $STAGE2_PACKAGES 162 | 163 | mkdir -p "${INSTALL_PATH}/scripts" 164 | cp "${FILES_DATA}/scripts/requirements.txt" "${INSTALL_PATH}/scripts/requirements.txt" 165 | in_target python3 -m pip install -r /scripts/requirements.txt --break-system-packages 166 | 167 | 168 | ################################################ Configure partition mountpoints and serial console ############################## 169 | 170 | cp ${FILES_DATA}/etc/fstab "${INSTALL_PATH}/etc/fstab" 171 | cp ${FILES_DATA}/etc/inittab "${INSTALL_PATH}/etc/inittab" 172 | 173 | ################################################ Copy Kernel Modules from system_a ################################################ 174 | 175 | mkdir -p "${INSTALL_PATH}/lib/modules" 176 | cp -r "${SYS_PATH}/lib/modules/${KERNEL_VERSION}" "${INSTALL_PATH}/lib/modules/" 177 | 178 | 179 | ################################################ Modify system_a for Utility Mode ################################################ 180 | 181 | cp ${FILES_SYS}/etc/fstab ${SYS_PATH}/etc/ 182 | cp ${FILES_SYS}/etc/inittab ${SYS_PATH}/etc/ 183 | cp ${FILES_SYS}/etc/init.d/S49usbgadget ${SYS_PATH}/etc/init.d/ 184 | chmod +x ${SYS_PATH}/etc/init.d/S49usbgadget 185 | 186 | 187 | ################################################ Done with system_a, unmount it ################################################ 188 | 189 | umount "$SYS_PATH" 190 | rmdir "$SYS_PATH" 191 | 192 | 193 | ################################################ Setup Xorg ################################################ 194 | 195 | echo "creating xorg.conf" 196 | mkdir -p "${INSTALL_PATH}/etc/X11" 197 | cp ${FILES_DATA}/etc/X11/xorg.conf.portrait "${INSTALL_PATH}/etc/X11/xorg.conf" 198 | 199 | # need to disable the scripts that try to autodetect input devices, they cause double input 200 | # this is particularly evident when in landscape mode, as only one of the two inputs is correctly transformed for the rotation 201 | # these files were installed by xserver-xorg-input-libinput 202 | in_target mv /usr/share/X11/xorg.conf.d /usr/share/X11/xorg.conf.d.bak 203 | 204 | 205 | ################################################ Setup Hostname and Hosts ################################################ 206 | 207 | echo "Setting hostname" 208 | echo "$HOST_NAME" > "${INSTALL_PATH}/etc/hostname" 209 | 210 | echo "Generating /etc/hosts" 211 | 212 | HOSTS_CONTENT=$( 213 | cat <<- EOHF 214 | # generated by $0 215 | 127.0.0.1 localhost 216 | 127.0.0.1 $HOST_NAME 217 | ::1 localhost $HOST_NAME ip6-localhost ip6-loopback 218 | ff02::1 ip6-allnodes 219 | ff02::2 ip6-allrouters 220 | ${USBNET_PREFIX}.1 host 221 | EOHF 222 | ) 223 | echo "$HOSTS_CONTENT" > "${INSTALL_PATH}/etc/hosts" 224 | 225 | 226 | ################################################ Setup user accounts ################################################ 227 | 228 | # NOTE: you could set the root password here, but you need to do it interactively 229 | # in_target passwd 230 | 231 | echo "Creating regular user (with sudo rights): $USER_NAME" 232 | 233 | in_target useradd -p "$USER_PASS_HASH" --shell /bin/bash "$USER_NAME" 234 | in_target mkdir -p "/home/${USER_NAME}" 235 | in_target chown "${USER_NAME}":"${USER_NAME}" "/home/${USER_NAME}" 236 | in_target chmod 700 "/home/${USER_NAME}" 237 | 238 | # let user use sudo without password 239 | echo "$USER_NAME ALL=(ALL) NOPASSWD: ALL" >> "${INSTALL_PATH}/etc/sudoers" 240 | 241 | set +e # ok if some of these fail 242 | in_target usermod -aG cdrom "$USER_NAME" 243 | in_target usermod -aG floppy "$USER_NAME" 244 | in_target usermod -aG sudo "$USER_NAME" 245 | in_target usermod -aG audio "$USER_NAME" 246 | in_target usermod -aG dip "$USER_NAME" 247 | in_target usermod -aG video "$USER_NAME" 248 | in_target usermod -aG plugdev "$USER_NAME" 249 | # in_target usermod -aG netdev "$USER_NAME" 250 | # in_target usermod -aG ssh "$USER_NAME" 251 | set -e 252 | 253 | 254 | ################################################ Setup scripts and services ################################################ 255 | 256 | install_script setup_usbgadget.sh 257 | install_service usbgadget.service 258 | 259 | install_script setup_display.sh 260 | install_script clear_display.sh 261 | 262 | install_script vnc_passwd 263 | install_script setup_vnc.sh 264 | install_service vnc.service 265 | 266 | install_script start_buttons.sh 267 | install_script buttons_app.py 268 | install_script buttons_settings.py 269 | install_service buttons.service 270 | 271 | install_script setup_backlight.sh 272 | install_service backlight.service 273 | 274 | install_script start_chromium.sh 275 | install_script chromium_settings.sh 276 | install_service chromium.service 277 | 278 | in_target chown -R "$USER_NAME" /scripts 279 | 280 | 281 | ################################################ Cleanup systemd and timezone stuff ################################################ 282 | 283 | echo "making sure symlinks exist for systemd" 284 | in_target ln -sf "/usr/bin/systemd" "/usr/sbin/init" # package systemd-sysv does this too 285 | in_target ln -sf "/lib/systemd/system/getty@.service" "/etc/systemd/system/getty.target.wants/getty@ttyS0.service" 286 | 287 | echo "Generating locales for $LOCALE" 288 | sed -i -e 's/# '"$LOCALE"' UTF-8/'"$LOCALE"' UTF-8/' "${INSTALL_PATH}/etc/locale.gen" 289 | echo "LANG=\"${LOCALE}\"" > "${INSTALL_PATH}/etc/default/locale" 290 | in_target dpkg-reconfigure --frontend=noninteractive locales 291 | 292 | echo "Setting timezone to $TIMEZONE" 293 | in_target ln -sf "/usr/share/zoneinfo/$TIMEZONE" "/etc/localtime" 294 | in_target dpkg-reconfigure --frontend=noninteractive tzdata 295 | 296 | 297 | ################################################ Done! ################################################ 298 | 299 | echo "synching disk changes" 300 | sync 301 | 302 | echo "Filesystem Size Used Avail Use% Mounted on" 303 | df -h |grep "$INSTALL_PATH" 304 | 305 | echo "Un-mounting $INSTALL_PATH" 306 | umount "$INSTALL_PATH" 307 | 308 | set +e # ok if cleanup fails 309 | # cleanup temp 310 | rm -r ${TEMP_DIR} 311 | 312 | echo "Done installing debian to: ${EXISTING_DUMP}" 313 | -------------------------------------------------------------------------------- /create_dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # create a distributable tar.gz from ./dumps/debian_current with given version number (without v) and today's date 4 | 5 | # all config lives in image_config.sh 6 | source ./image_config.sh 7 | 8 | DATESTAMP=$(date -I) 9 | VERSION="$1" 10 | 11 | if [ -z "$VERSION" ]; then 12 | echo "Need to provide version: ./create_release.sh 1.1" 13 | exit 1 14 | fi 15 | 16 | RELEASE_NAME="debian_v${VERSION}_${DATESTAMP}" 17 | ARCHIVE_NAME="${RELEASE_NAME}.tar.gz" 18 | 19 | if [ -e "./dist/$ARCHIVE_NAME" ]; then 20 | echo "dist package already exists! ./dist/$ARCHIVE_NAME" 21 | exit 1 22 | fi 23 | 24 | 25 | mkdir -p ./dumps 26 | mv "$EXISTING_DUMP" "./dumps/$RELEASE_NAME" 27 | 28 | pushd ./dumps || exit 1 29 | 30 | tar czvf "../dist/$ARCHIVE_NAME" "./$RELEASE_NAME" 31 | 32 | popd || exit 1 33 | 34 | mv "./dumps/$RELEASE_NAME" "$EXISTING_DUMP" 35 | 36 | echo "Created ./dist/$ARCHIVE_NAME" 37 | -------------------------------------------------------------------------------- /files/Readme.md: -------------------------------------------------------------------------------- 1 | # Files to be added to image 2 | 3 | system_a/ (files to be added to system_a partition for Utility Mode) 4 | debian_data/ (files to be added to data partition while installing debian) 5 | env/ (env config, only switchable is used) 6 | -------------------------------------------------------------------------------- /files/data/etc/X11/xorg.conf.landscape: -------------------------------------------------------------------------------- 1 | # Xorg.conf for superbird 2 | # Landscape orientation (buttons on top) 3 | 4 | Section "ServerFlags" 5 | Option "BlankTime" "10" 6 | Option "StandbyTime" "10" 7 | Option "SuspendTime" "10" 8 | Option "OffTime" "10" 9 | Option "dpms" "on" 10 | EndSection 11 | 12 | Section "ServerLayout" 13 | Identifier "Simple Layout" 14 | Screen "Panel" 15 | InputDevice "TouchScreen" "Pointer" 16 | InputDevice "GPIOKeys" "Keyboard" 17 | InputDevice "Rotary" "Keyboard" 18 | EndSection 19 | 20 | Section "Screen" 21 | Identifier "Panel" 22 | Monitor "DefaultMonitor" 23 | Device "FramebufferDevice" 24 | DefaultDepth 24 25 | DefaultFbBpp 32 26 | SubSection "Display" 27 | Depth 32 28 | Virtual 480 800 29 | ViewPort 0 0 30 | Modes "480x800" 31 | EndSubSection 32 | EndSection 33 | 34 | Section "Device" 35 | Identifier "FramebufferDevice" 36 | Driver "fbdev" 37 | Option "fbdev" "/dev/fb0" 38 | Option "Rotate" "CW" 39 | EndSection 40 | 41 | Section "Monitor" 42 | Identifier "DefaultMonitor" 43 | Option "DPMS" "on" 44 | EndSection 45 | 46 | # All the device buttons are part of event0, which appears as a keyboard 47 | # buttons along the edge are: 1, 2, 3, 4, m 48 | # next to the knob: ESC 49 | # knob click: Enter 50 | Section "InputDevice" 51 | Identifier "GPIOKeys" 52 | Driver "libinput" 53 | Option "Device" "/dev/input/event0" 54 | EndSection 55 | 56 | # Turning the knob is a separate device, event1, which also appears as a keyboard 57 | # turning the knob corresponds to the left and right arrow keys 58 | Section "InputDevice" 59 | Identifier "Rotary" 60 | Driver "libinput" 61 | Option "Device" "/dev/input/event1" 62 | EndSection 63 | 64 | # The touchscreen is event3 65 | Section "InputDevice" 66 | Identifier "TouchScreen" 67 | Driver "libinput" 68 | Option "Device" "/dev/input/event3" 69 | Option "Mode" "Absolute" 70 | Option "GrabDevice" "1" 71 | Option "TransformationMatrix" "0 1 0 -1 0 1 0 0 1" 72 | EndSection 73 | -------------------------------------------------------------------------------- /files/data/etc/X11/xorg.conf.portrait: -------------------------------------------------------------------------------- 1 | # Xorg.conf for superbird 2 | # Portrait orientation (buttons on the right side) 3 | 4 | Section "ServerFlags" 5 | Option "BlankTime" "10" 6 | Option "StandbyTime" "10" 7 | Option "SuspendTime" "10" 8 | Option "OffTime" "10" 9 | Option "dpms" "on" 10 | EndSection 11 | 12 | Section "ServerLayout" 13 | Identifier "Simple Layout" 14 | Screen "Panel" 15 | InputDevice "TouchScreen" "Pointer" 16 | InputDevice "GPIOKeys" "Keyboard" 17 | InputDevice "Rotary" "Keyboard" 18 | EndSection 19 | 20 | Section "Screen" 21 | Identifier "Panel" 22 | Monitor "DefaultMonitor" 23 | Device "FramebufferDevice" 24 | DefaultDepth 24 25 | DefaultFbBpp 32 26 | SubSection "Display" 27 | Depth 32 28 | Virtual 480 800 29 | ViewPort 0 0 30 | Modes "480x800" 31 | EndSubSection 32 | EndSection 33 | 34 | Section "Device" 35 | Identifier "FramebufferDevice" 36 | Driver "fbdev" 37 | Option "fbdev" "/dev/fb0" 38 | # Option "Rotate" "CW" 39 | EndSection 40 | 41 | Section "Monitor" 42 | Identifier "DefaultMonitor" 43 | Option "DPMS" "on" 44 | EndSection 45 | 46 | # All the device buttons are part of event0, which appears as a keyboard 47 | # buttons along the edge are: 1, 2, 3, 4, m 48 | # next to the knob: ESC 49 | # knob click: Enter 50 | Section "InputDevice" 51 | Identifier "GPIOKeys" 52 | Driver "libinput" 53 | Option "Device" "/dev/input/event0" 54 | EndSection 55 | 56 | # Turning the dial is a separate device, event1, which also appears as a keyboard 57 | # turning the knob corresponds to the left and right arrow keys 58 | Section "InputDevice" 59 | Identifier "Rotary" 60 | Driver "libinput" 61 | Option "Device" "/dev/input/event1" 62 | EndSection 63 | 64 | # The touchscreen is event3 65 | Section "InputDevice" 66 | Identifier "TouchScreen" 67 | Driver "libinput" 68 | Option "Device" "/dev/input/event3" 69 | Option "Mode" "Absolute" 70 | Option "GrabDevice" "1" 71 | # Option "TransformationMatrix" "0 1 0 -1 0 1 0 0 1" 72 | EndSection 73 | -------------------------------------------------------------------------------- /files/data/etc/fstab: -------------------------------------------------------------------------------- 1 | # /etc/fstab: static file system information 2 | # 3 | 4 | # for chromium user profile 5 | /dev/settings /config ext4 rw 0 0 6 | -------------------------------------------------------------------------------- /files/data/etc/inittab: -------------------------------------------------------------------------------- 1 | # /etc/inittab 2 | # 3 | # Copyright (C) 2001 Erik Andersen 4 | 5 | # Format for each entry: ::: 6 | # 7 | # id == tty to run on, or empty for /dev/console 8 | # runlevels == ignored 9 | # action == one of sysinit, respawn, askfirst, wait, and once 10 | # process == program to run 11 | 12 | # console on serial port 13 | T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100 14 | -------------------------------------------------------------------------------- /files/data/lib/systemd/system/backlight.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Backlight sync to display state 3 | Wants=network-online.target 4 | 5 | [Service] 6 | ExecStart=/scripts/setup_backlight.sh 7 | RestartSec=5 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | EOBLSF -------------------------------------------------------------------------------- /files/data/lib/systemd/system/buttons.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Button service to integrate with Home Assistant 3 | Wants=network-online.target 4 | 5 | [Service] 6 | ExecStart=/scripts/start_buttons.sh 7 | RestartSec=5 8 | 9 | [Install] 10 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /files/data/lib/systemd/system/chromium.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Chromium Fullscreen 3 | Wants=network-online.target 4 | 5 | [Service] 6 | ExecStart=/scripts/start_chromium.sh 7 | # clear display when stopping, so it doesn't just freeze on the last image 8 | ExecStopPost=/scripts/clear_display.sh 9 | RestartSec=5 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /files/data/lib/systemd/system/usbgadget.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=USB Gadget for RNDIS and ADB 3 | Before=network-pre.target 4 | Wants=network-pre.target 5 | 6 | [Service] 7 | ExecStart=/scripts/setup_usbgadget.sh > /var/log/setup_usbgadget.log 2>&1 8 | 9 | [Install] 10 | WantedBy=network.target 11 | -------------------------------------------------------------------------------- /files/data/lib/systemd/system/vnc.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=VNC server for remote access 3 | Wants=network-online.target 4 | 5 | [Service] 6 | ExecStart=/scripts/setup_vnc.sh 7 | RestartSec=5 8 | 9 | [Install] 10 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /files/data/scripts/buttons_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Buttons monitoring, to integrate with Home Assistant 3 | """ 4 | # pylint: disable=global-statement,line-too-long,broad-exception-caught,logging-fstring-interpolation 5 | 6 | # https://stackoverflow.com/questions/5060710/format-of-dev-input-event 7 | # https://homeassistantapi.readthedocs.io/en/latest/usage.html 8 | # https://homeassistantapi.readthedocs.io/en/latest/api.html#homeassistant_api.Client 9 | # https://github.com/maximehk/ha_lights/blob/main/ha_lights/ha_lights.py 10 | 11 | 12 | import time 13 | import struct 14 | import contextlib 15 | import warnings 16 | import logging 17 | 18 | from threading import Thread 19 | from threading import Event as ThreadEvent 20 | 21 | import requests 22 | import urllib3 23 | from urllib3.exceptions import InsecureRequestWarning 24 | 25 | from homeassistant_api import Client 26 | 27 | # user-configurable settings are all in button_settings.py 28 | from buttons_settings import ROOM_LIGHT, ROOM_SCENES, ESC_SCENE, LEVEL_INCREMENT 29 | from buttons_settings import HA_SERVER, HA_TOKEN 30 | 31 | # All the device buttons are part of event0, which appears as a keyboard 32 | # buttons along the edge are: 1, 2, 3, 4, m 33 | # next to the knob: ESC 34 | # knob click: Enter 35 | # Turning the knob is a separate device, event1, which also appears as a keyboard 36 | # turning the knob corresponds to the left and right arrow keys 37 | 38 | DEV_BUTTONS = '/dev/input/event0' 39 | DEV_KNOB = '/dev/input/event1' 40 | 41 | # for event0, these are the keycodes for buttons 42 | BUTTONS_CODE_MAP = { 43 | 2: '1', 44 | 3: '2', 45 | 4: '3', 46 | 5: '4', 47 | 50: 'm', 48 | 28: 'ENTER', 49 | 1: 'ESC', 50 | } 51 | 52 | # for event1, when the knob is turned it is always keycode 6, but value changes on direction 53 | KNOB_LEFT = 4294967295 # actually -1 but unsigned int so wraps around 54 | KNOB_RIGHT = 1 55 | 56 | # https://github.com/torvalds/linux/blob/v5.5-rc5/include/uapi/linux/input.h#L28 57 | # long int, long int, unsigned short, unsigned short, unsigned int 58 | EVENT_FORMAT = 'llHHI' 59 | EVENT_SIZE = struct.calcsize(EVENT_FORMAT) 60 | 61 | # global for HA Client 62 | HA_CLIENT:Client = None 63 | 64 | # suppress warnings about invalid certs 65 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 66 | old_merge_environment_settings = requests.Session.merge_environment_settings 67 | 68 | logformat = logging.Formatter('%(created)f %(levelname)s [%(filename)s:%(lineno)d]: %(message)s') 69 | logger = logging.getLogger('buttons') 70 | logger.setLevel(logging.DEBUG) 71 | 72 | fh = logging.FileHandler('/var/log/buttons.log') 73 | fh.setLevel(logging.DEBUG) 74 | fh.setFormatter(logformat) 75 | logger.addHandler(fh) 76 | 77 | ch = logging.StreamHandler() 78 | ch.setLevel(logging.DEBUG) 79 | ch.setFormatter(logformat) 80 | logger.addHandler(ch) 81 | 82 | 83 | @contextlib.contextmanager 84 | def no_ssl_verification(): 85 | """ 86 | context manager that monkey patches requests and changes it so that verify=False is the default and suppresses the warning 87 | https://stackoverflow.com/questions/15445981/how-do-i-disable-the-security-certificate-check-in-python-requests 88 | """ 89 | opened_adapters = set() 90 | 91 | def merge_environment_settings(self, url, proxies, stream, verify, cert): 92 | # Verification happens only once per connection so we need to close 93 | # all the opened adapters once we're done. Otherwise, the effects of 94 | # verify=False persist beyond the end of this context manager. 95 | opened_adapters.add(self.get_adapter(url)) 96 | 97 | settings = old_merge_environment_settings(self, url, proxies, stream, verify, cert) 98 | settings['verify'] = False 99 | 100 | return settings 101 | 102 | requests.Session.merge_environment_settings = merge_environment_settings 103 | 104 | try: 105 | with warnings.catch_warnings(): 106 | warnings.simplefilter('ignore', InsecureRequestWarning) 107 | yield 108 | finally: 109 | requests.Session.merge_environment_settings = old_merge_environment_settings 110 | 111 | for adapter in opened_adapters: 112 | try: 113 | adapter.close() 114 | except Exception: 115 | pass 116 | 117 | 118 | def translate_event(etype: int, code: int, value: int) -> str: 119 | """ 120 | Translate combination of type, code, value into string representing button pressed 121 | """ 122 | if etype == 1 and value == 1: 123 | # button press 124 | if code in BUTTONS_CODE_MAP: 125 | return BUTTONS_CODE_MAP[code] 126 | if etype == 2: 127 | if code == 6: 128 | # knob turn 129 | if value == KNOB_RIGHT: 130 | return 'RIGHT' 131 | if value == KNOB_LEFT: 132 | return 'LEFT' 133 | return 'UNKNOWN' 134 | 135 | 136 | def handle_button(pressed_key: str): 137 | """ 138 | Decide what to do in response to a button press 139 | """ 140 | logger.info(f'Pressed button: {pressed_key}') 141 | # check for presets 142 | if pressed_key in ['1', '2', '3', '4', 'm']: 143 | if pressed_key == 'm': 144 | pressed_key = '5' 145 | if len(ROOM_SCENES) >= int(pressed_key): 146 | preset = ROOM_SCENES[int(pressed_key) - 1] 147 | cmd_scene(preset) 148 | elif pressed_key in ['ESC', 'ENTER', 'LEFT', 'RIGHT']: 149 | if pressed_key == 'ENTER': 150 | cmd_toggle() 151 | elif pressed_key == 'LEFT': 152 | cmd_lower() 153 | elif pressed_key == 'RIGHT': 154 | cmd_raise() 155 | if pressed_key == 'ESC': 156 | cmd_scene(ESC_SCENE) 157 | 158 | 159 | def get_light_level(entity_id: str) -> int: 160 | """ 161 | Get current brightness of a light 162 | """ 163 | light = HA_CLIENT.get_entity(entity_id=entity_id) 164 | level = light.get_state().attributes['brightness'] 165 | if level is None: 166 | level = 0 167 | return level 168 | 169 | 170 | def set_light_level(entity_id: str, level: int): 171 | """ 172 | Set light brightness 173 | """ 174 | light_domain = HA_CLIENT.get_domain('light') 175 | light_domain.turn_on(entity_id=entity_id, brightness=level) 176 | 177 | 178 | def cmd_scene(entity_id: str): 179 | """ 180 | Recall a scene / automation / script by entity id 181 | you can use any entity where turn_on is valid 182 | """ 183 | if entity_id == '': 184 | return 185 | domain = entity_id.split('.')[0] 186 | logger.info(f'Recalling {domain}: {entity_id}') 187 | scene_domain = HA_CLIENT.get_domain(domain) 188 | scene_domain.turn_on(entity_id=entity_id) 189 | 190 | 191 | def cmd_toggle(): 192 | """ 193 | Toggle the light for this room on/off 194 | """ 195 | logger.info(f'Toggling state of light: {ROOM_LIGHT}') 196 | light_domain = HA_CLIENT.get_domain('light') 197 | light_domain.toggle(entity_id=ROOM_LIGHT) 198 | 199 | 200 | def cmd_lower(): 201 | """ 202 | Lower the level of the light for this room 203 | """ 204 | logger.info(f'Lowering brightness of {ROOM_LIGHT}') 205 | current_level = get_light_level(ROOM_LIGHT) 206 | new_level = current_level - LEVEL_INCREMENT 207 | new_level = max(new_level, 0) 208 | logger.info(f'New level: {new_level}') 209 | if new_level < current_level: 210 | set_light_level(ROOM_LIGHT, new_level) 211 | 212 | 213 | def cmd_raise(): 214 | """ 215 | Raise the level of the light for this room 216 | """ 217 | logger.info(f'Raising brightness of {ROOM_LIGHT}') 218 | current_level = get_light_level(ROOM_LIGHT) 219 | new_level = current_level + LEVEL_INCREMENT 220 | new_level = min(new_level, 255) 221 | logger.info(f'New level: {new_level}') 222 | if new_level > current_level: 223 | set_light_level(ROOM_LIGHT, new_level) 224 | 225 | 226 | class EventListener(): 227 | """ 228 | Listen to a specific /dev/eventX and call handle_button 229 | """ 230 | def __init__(self, device: str) -> None: 231 | self.device = device 232 | self.stopper = ThreadEvent() 233 | self.thread:Thread = None 234 | self.start() 235 | 236 | def start(self): 237 | """ 238 | Start listening thread 239 | """ 240 | logger.info(f'Starting listener for {self.device}') 241 | self.thread = Thread(target=self.listen, daemon=True) 242 | self.thread.start() 243 | 244 | def stop(self): 245 | """ 246 | Stop listening thread 247 | """ 248 | logger.info(f'Stopping listener for {self.device}') 249 | self.stopper.set() 250 | self.thread.join() 251 | 252 | def listen(self): 253 | """ 254 | To run in thread, listen for events and call handle_buttons if applicable 255 | """ 256 | with open(self.device, "rb") as in_file: 257 | event = in_file.read(EVENT_SIZE) 258 | while event and not self.stopper.is_set(): 259 | if self.stopper.is_set(): 260 | break 261 | (_sec, _usec, etype, code, value) = struct.unpack(EVENT_FORMAT, event) 262 | # logger.info(f'Event: type: {etype}, code: {code}, value:{value}') 263 | event_str = translate_event(etype, code, value) 264 | if event_str in ['1', '2', '3', '4', 'm', 'ENTER', 'ESC', 'LEFT', 'RIGHT']: 265 | handle_button(event_str) 266 | event = in_file.read(EVENT_SIZE) 267 | 268 | 269 | if __name__ == '__main__': 270 | # NOTE: we use no_ssl_verification context handler to nuke the obnoxiously difficult-to-disable SSL verification of requests 271 | logger.info('Starting buttons listeners') 272 | with no_ssl_verification(): 273 | HA_CLIENT = Client(f'{HA_SERVER}/api', HA_TOKEN, global_request_kwargs={'verify': False}, cache_session=False) 274 | EventListener(DEV_BUTTONS) 275 | EventListener(DEV_KNOB) 276 | while True: 277 | time.sleep(1) 278 | -------------------------------------------------------------------------------- /files/data/scripts/buttons_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings for /scripts/buttons_app.py 3 | """ 4 | # pylint: disable=line-too-long 5 | 6 | # Home Assistant address, including port 7 | HA_SERVER = 'https://192.168.1.144:8123' 8 | 9 | # long-lived token, https://www.home-assistant.io/docs/authentication/#your-account-profile 10 | HA_TOKEN = 'insert-token-here' 11 | 12 | # Light entity to control with knob 13 | ROOM_LIGHT = 'light.office' 14 | 15 | # when you turn the knob, brightness will go up or down by this amount 16 | # brightness is 0 - 255 17 | LEVEL_INCREMENT = 32 18 | 19 | # assign scene/automation/script to buttons along the edge 20 | # anything that supports turn_on() should work 21 | # blank entries are ignored 22 | 23 | ROOM_SCENES = [ 24 | 'scene.office_bright', # 1 25 | 'scene.office_half', # 2 26 | 'scene.office_blues', # 3 27 | '', # 4 28 | '', # 5 aka m (recessed menu button) 29 | ] 30 | 31 | # assign a scene/automation/script to the button next to the knob, aka ESC 32 | ESC_SCENE = 'scene.office_bright' 33 | -------------------------------------------------------------------------------- /files/data/scripts/chromium_settings.sh: -------------------------------------------------------------------------------- 1 | # settings for /scripts/start_chromium.sh 2 | 3 | URL="https://192.168.1.144:8123/lovelace/" 4 | SCALE="1.0" 5 | EXTRA_CHROMIUM_ARGS="" 6 | EXTRA_XORG_ARGS="-nocursor" 7 | -------------------------------------------------------------------------------- /files/data/scripts/clear_display.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # clear the display 3 | 4 | FB="fb0" 5 | echo 1 > /sys/class/graphics/$FB/osd_clear 6 | -------------------------------------------------------------------------------- /files/data/scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | # python packages to be installed (as root) using pip, on superbird 2 | # Home Assistant API, for controlling lighting and recalling scenes 3 | homeassistant_api 4 | -------------------------------------------------------------------------------- /files/data/scripts/setup_backlight.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # set up the backlight by following if display is on or off 3 | 4 | # 0 - 100, display brightness when On 5 | BRIGHTNESS=100 6 | 7 | # seconds, how often to check state of display 8 | CHECK_TIME=0.1 9 | 10 | # the backlight brightness control 11 | BACKLIGHT="/sys/devices/platform/backlight/backlight/aml-bl/brightness" 12 | 13 | while :;do 14 | DISPLAY_STATUS=$(DISPLAY=:0 xset -q|grep "Monitor is"|awk '{print $3}') 15 | if [ "$DISPLAY_STATUS" == "Off" ]; then 16 | # only turn off backlight if it actually says "Off", fallback is always on 17 | echo 0 > $BACKLIGHT 18 | else 19 | echo $BRIGHTNESS > $BACKLIGHT 20 | fi 21 | sleep $CHECK_TIME 22 | done 23 | 24 | # try to leave backlight on if the loop breaks 25 | echo $BRIGHTNESS > $BACKLIGHT 26 | -------------------------------------------------------------------------------- /files/data/scripts/setup_display.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # set up display mode 3 | 4 | # by default portrait orientation (rotate in xorg.conf): 5 | # width: 480 6 | # height: 800 7 | # depth: 32 bits 8 | 9 | FB="fb0" 10 | 11 | WIDTH="480" 12 | HEIGHT="800" 13 | DEPTH="32" # mandatory 32bit 14 | 15 | # set the framebuffer geometry and bit depth 16 | fbset -fb /dev/${FB} -g "$WIDTH" "$HEIGHT" "$WIDTH" "$HEIGHT" "$DEPTH" 17 | 18 | # clear scaling values 19 | echo 0 > /sys/class/graphics/$FB/free_scale 20 | echo 1 > /sys/class/graphics/$FB/freescale_mode 21 | 22 | # scaling values are always N - 1, where N is the value you actually want 23 | # under normal conditions these two lines should match numbers 24 | # but if you need to scale things, adjust free_scale_axis to compensate 25 | # but keep window_axis as-is 26 | echo 0 0 479 799 > /sys/class/graphics/$FB/free_scale_axis 27 | echo 0 0 479 799 > /sys/class/graphics/$FB/window_axis 28 | 29 | # this seems to "apply" the values set above 30 | echo 0x10001 > /sys/class/graphics/$FB/free_scale 31 | 32 | # make sure backlight is on 33 | echo 100 > /sys/devices/platform/backlight/backlight/aml-bl/brightness 34 | -------------------------------------------------------------------------------- /files/data/scripts/setup_usbgadget.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup Linux USB Gadget for just rndis 4 | # this version is meant to run within native debian on the device 5 | 6 | # Available options: 7 | 8 | # usb_f_rndis.ko 9 | # usb_f_fs.ko 10 | # usb_f_midi.ko 11 | # usb_f_mtp.ko 12 | # usb_f_ptp.ko 13 | # usb_f_audio_source.ko 14 | # usb_f_accessory.ko 15 | 16 | 17 | ######### Variables 18 | 19 | USBNET_PREFIX="192.168.7" 20 | SERIAL_NUMBER="12345678" 21 | # 18d1:4e40 Google Inc. Nexus 7 22 | ID_VENDOR="0x18d1" 23 | ID_PRODUCT="0x4e40" 24 | MANUFACTURER="Spotify" 25 | PRODUCT="Superbird" 26 | # ADBD_LOG_FILE="/tmp/adbd.log" 27 | 28 | 29 | # Research 30 | # starting point: https://github.com/frederic/superbird-bulkcmd/blob/main/scripts/enable-adb.sh.client 31 | # info about configfs https://elinux.org/images/e/ef/USB_Gadget_Configfs_API_0.pdf 32 | # info about usbnet and bridging https://developer.ridgerun.com/wiki/index.php/How_to_use_USB_device_networking 33 | # more info, including for windows https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget 34 | # a gist that was helpful: https://gist.github.com/geekman/5bdb5abdc9ec6ac91d5646de0c0c60c4 35 | # https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt 36 | 37 | ######### Functions 38 | 39 | create_device() { 40 | # create usb gadget device 41 | ID_VEND="$1" 42 | ID_PROD="$2" 43 | BCD_DEVICE="$3" 44 | BCD_USB="$4" 45 | echo "### Creating device $ID_VEND $ID_PROD" 46 | mkdir -p "/dev/usb-ffs" 47 | mkdir -p "/dev/usb-ffs/adb" 48 | mountpoint /sys/kernel/config/ || mount -t configfs none "/sys/kernel/config/" 49 | mkdir -p "/sys/kernel/config/usb_gadget/g1" 50 | echo "$ID_VEND" > "/sys/kernel/config/usb_gadget/g1/idVendor" 51 | echo "$ID_PROD" > "/sys/kernel/config/usb_gadget/g1/idProduct" 52 | echo "$BCD_DEVICE" > "/sys/kernel/config/usb_gadget/g1/bcdDevice" 53 | echo "$BCD_USB" > "/sys/kernel/config/usb_gadget/g1/bcdUSB" 54 | mkdir -p "/sys/kernel/config/usb_gadget/g1/strings/0x409" 55 | sleep 1 56 | } 57 | 58 | configure_device() { 59 | # configure usb gadget device 60 | MANUF="$1" 61 | PROD="$2" 62 | SERIAL="$3" 63 | CONFIG_NAME="$4" 64 | echo "### Configuring device as $MANUF $PROD" 65 | echo "$MANUF" > "/sys/kernel/config/usb_gadget/g1/strings/0x409/manufacturer" 66 | echo "$PROD" > "/sys/kernel/config/usb_gadget/g1/strings/0x409/product" 67 | echo "$SERIAL" > "/sys/kernel/config/usb_gadget/g1/strings/0x409/serialnumber" 68 | mkdir -p "/sys/kernel/config/usb_gadget/g1/configs/c.1" 69 | mkdir -p "/sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409" 70 | echo "$CONFIG_NAME" > "/sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409/configuration" 71 | echo 500 > "/sys/kernel/config/usb_gadget/g1/configs/c.1/MaxPower" 72 | ln -s "/sys/kernel/config/usb_gadget/g1/configs/c.1" "/sys/kernel/config/usb_gadget/g1/os_desc/c.1" 73 | sleep 1 74 | } 75 | 76 | add_function(){ 77 | # add a function to existing config id 78 | FUNCTION_NAME="$1" 79 | echo "### adding function $FUNCTION_NAME to config c.1" 80 | mkdir -p "/sys/kernel/config/usb_gadget/g1/functions/${FUNCTION_NAME}" 81 | ln -s "/sys/kernel/config/usb_gadget/g1/functions/${FUNCTION_NAME}" "/sys/kernel/config/usb_gadget/g1/configs/c.1" 82 | } 83 | 84 | start_adb_daemon() { 85 | # mount adb functionfs and start daemon 86 | LOG_FILE="$1" 87 | echo "### starting adb daemon" 88 | mkdir -p /dev/usbgadget/adb 89 | mount -t functionfs adb /dev/usbgadget/adb 90 | if [ ! -f "/usr/bin/adbd" ]; then 91 | echo "Unable to find adbd binary!" 92 | else 93 | /usr/bin/adbd > "$LOG_FILE" 2>&1 & 94 | echo "$!" > /tmp/adbd.pid 95 | fi 96 | sleep 5s 97 | } 98 | 99 | attach_driver(){ 100 | # attach the created gadget device to our UDC driver 101 | UDC_DEVICE=$(/bin/ls -1 /sys/class/udc/) # ff400000.dwc2_a 102 | echo "### Attaching gadget to UDC device: $UDC_DEVICE" 103 | echo "$UDC_DEVICE" > /sys/kernel/config/usb_gadget/g1/UDC 104 | sleep 1 105 | } 106 | 107 | configure_usbnet() { 108 | DEVICE="$1" 109 | NETWORK="$2" # just the first 3 octets 110 | NETMASK="$3" 111 | echo "### bringing up $DEVICE with ${NETWORK}.2" 112 | ifconfig "$DEVICE" up 113 | ifconfig "$DEVICE" "${NETWORK}.2" netmask "$NETMASK" broadcast "${NETWORK}.255" 114 | echo "adding routes for $DEVICE" 115 | ip route add default via "${NETWORK}.1" dev "$DEVICE" 116 | echo "making sure you have a dns server" 117 | echo "nameserver 1.1.1.1" > /etc/resolv.conf 118 | sleep 1 119 | } 120 | 121 | shutdown_gadget() { 122 | # shutdown and clean up usb gadget and services 123 | # ref: https://wiki.tizen.org/USB/Linux_USB_Layers/Configfs_Composite_Gadget/Usage_eq._to_g_ffs.ko 124 | echo "$UDC_DEVICE" > /sys/kernel/config/usb_gadget/g1/UDC 125 | if [ -f "/tmp/adbd.pid" ]; then 126 | kill -9 "$(cat /tmp/adbd.pid)" 127 | umount /dev/usbgadget/adb 128 | fi 129 | find "/sys/kernel/config/usb_gadget/g1/configs/c.1" -type l -exec unlink {} \; 130 | rm -r "/sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409" 131 | rm -r /sys/kernel/config/usb_gadget/g1/strings/0x409 132 | rm -r "/sys/kernel/config/usb_gadget/g1/configs/c.1" 133 | rm -r /sys/kernel/config/usb_gadget/g1/functions/* 134 | rm -r /sys/kernel/config/usb_gadget/g1/ 135 | 136 | } 137 | 138 | ######### Entrypoint 139 | 140 | echo "### Configuring USB Gadget with adb and rndis" 141 | create_device "$ID_VENDOR" "$ID_PRODUCT" "0x0223" "0x0200" 142 | configure_device "$MANUFACTURER" "$PRODUCT" "$SERIAL_NUMBER" "Multi-Function Device" 143 | 144 | add_function "rndis.usb0" # rndis usb ethernet 145 | 146 | # add_function "ffs.adb" # adb 147 | # start_adb_daemon "$ADBD_LOG_FILE" 148 | 149 | attach_driver 150 | 151 | configure_usbnet "usb0" "$USBNET_PREFIX" "255.255.255.0" 152 | 153 | echo "Done setting up USB Gadget" 154 | -------------------------------------------------------------------------------- /files/data/scripts/setup_vnc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # setup vnc server 4 | 5 | LOGFILE="/var/log/vnc.log" 6 | # To change password, run: sudo vncpasswd /scripts/vnc_passwd 7 | 8 | while :; do 9 | /usr/bin/X0tigervnc -display=:0 -rfbport=5900 -rfbauth=/scripts/vnc_passwd -SecurityTypes=VncAuth > "$LOGFILE" 2>&1 10 | done 11 | -------------------------------------------------------------------------------- /files/data/scripts/start_buttons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # start the buttons monitoring service 3 | 4 | export DISPLAY=:0 5 | /usr/bin/python3 /scripts/buttons_app.py 6 | -------------------------------------------------------------------------------- /files/data/scripts/start_chromium.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Start X with just Chromium browser 3 | # fullscreen, kiosk mode, tweaked for touchscreen, with given url 4 | 5 | # handle defaults 6 | # URL="https://192.168.1.144:8123/lovelace/" 7 | # SCALE="1.0" 8 | # EXTRA_CHROMIUM_ARGS="" 9 | # EXTRA_XORG_ARGS="-nocursor" 10 | 11 | source /scripts/chromium_settings.sh 12 | 13 | ## Hardcoded Vars you dont need to mess with 14 | CHROMIUM_BINARY="/usr/bin/chromium" 15 | USER_DATA_DIR="/config" 16 | DISK_CACHE_DIR="/dev/null" # prevent chromium from caching anything 17 | 18 | # log this script's actions to a file 19 | exec 1>/var/log/chromium.log 2>&1 20 | 21 | echo "Starting chromium kiosk" 22 | 23 | command -v "$CHROMIUM_BINARY" >/dev/null || { 24 | echo "Need to install chromium! " 25 | exit 1 26 | } 27 | 28 | /scripts/setup_display.sh 29 | 30 | # does not get cleaned up properly after previous exit 31 | rm ${USER_DATA_DIR}/SingletonLock 32 | 33 | # get current resolution so we can match it 34 | # if you don't set --window-size, chromium will go almost-fullscreen, about 10px shy on all sides 35 | # if you don't set --window-position, chromium will start at about 10,10 instead of 0,0 36 | # why does chromium do this??!?! 37 | # here we detect resolution by briefly starting X11 and then parsing output of xrandr 38 | # this is simpler and more reliable than parsing xorg.conf 39 | # by avoiding a hardcoded resolution here, we only need to make changes in xorg.conf if we want to change resolution or rotate 40 | 41 | echo "Briefly starting X11 in order to detect configured resolution" 42 | DISP_REZ=$(xinit /usr/bin/xrandr 2>/dev/null|grep "\*"|awk '{print $1}'|tr 'x' ',') 43 | echo "Detected resolution: $DISP_REZ" 44 | 45 | CHROMIUM_CMD="xinit $CHROMIUM_BINARY \ 46 | --no-gpu \ 47 | --disable-gpu \ 48 | --no-sandbox \ 49 | --autoplay-policy=no-user-gesture-required \ 50 | --use-fake-ui-for-media-stream \ 51 | --use-fake-device-for-media-stream \ 52 | --disable-sync \ 53 | --remote-debugging-port=9222 \ 54 | --window-size=$DISP_REZ \ 55 | --window-position=0,0 \ 56 | --force-device-scale-factor=$SCALE \ 57 | --pull-to-refresh=1 \ 58 | --disable-smooth-scrolling \ 59 | --disable-login-animations \ 60 | --disable-modal-animations \ 61 | --noerrdialogs \ 62 | --no-first-run \ 63 | --disable-infobars \ 64 | --fast \ 65 | --fast-start \ 66 | --disable-pinch \ 67 | --overscroll-history-navigation=0 \ 68 | --disable-translate \ 69 | --hide-scrollbars \ 70 | --disable-overlay-scrollbar \ 71 | --disable-features=OverlayScrollbar \ 72 | --disable-features=TranslateUI \ 73 | --disk-cache-dir=$DISK_CACHE_DIR \ 74 | --password-store=basic \ 75 | --touch-events=enabled \ 76 | --ignore-certificate-errors \ 77 | --user-data-dir=$USER_DATA_DIR \ 78 | --kiosk $EXTRA_CHROMIUM_ARGS \ 79 | --app=$URL -- $EXTRA_XORG_ARGS" 80 | 81 | echo "" 82 | echo "running chromium command: $CHROMIUM_CMD" 83 | echo "" 84 | $CHROMIUM_CMD 85 | 86 | # clear the display after chromium is killed, otherwise the last image will remain frozen 87 | /scripts/clear_display.sh 88 | -------------------------------------------------------------------------------- /files/data/scripts/vnc_passwd: -------------------------------------------------------------------------------- 1 | zRqAp� -------------------------------------------------------------------------------- /files/env/env_abb.txt: -------------------------------------------------------------------------------- 1 | EnableSelinux=enforcing 2 | Irq_check_en=0 3 | active_slot=_a 4 | boot_part=boot_a 5 | avb2=0 6 | baudrate=115200 7 | display_bpp=16 8 | display_color_bg=0 9 | display_color_fg=0xffff 10 | display_color_index=16 11 | display_height=800 12 | display_init=1 13 | display_width=480 14 | dtb_mem_addr=0x1000000 15 | fb_addr=0x1f800000 16 | fdt_high=0x20000000 17 | loadaddr=1080000 18 | lock=10001000 19 | firstboot=0 20 | recovery_offset=0 21 | recovery_part=recovery 22 | 23 | system_mode=1 24 | try_auto_burn=update 700 750; 25 | update=run usb_burning; 26 | upgrade_step=0 27 | usb_burning=update 1000 28 | wipe_cache=successful 29 | wipe_data=successful 30 | 31 | sdc_burning=sdc_burn ${sdcburncfg} 32 | sdcburncfg=aml_sdc_burn.ini 33 | silent=on 34 | bcb_cmd=get_avb_mode;get_valid_slot; 35 | bootcmd=run storeboot 36 | 37 | init_display_normal=osd open;osd clear;imgread pic logo bootup_spotify $loadaddr;bmp display $bootup_spotify_offset;bmp scale;vout output panel; 38 | init_display_recovery=osd open;osd clear;imgread pic logo upgrade_logo $loadaddr;bmp display $upgrade_logo_offset;bmp scale;vout output panel; 39 | init_display_burn=osd open;osd clear;imgread pic logo upgrade_error $loadaddr;bmp display $upgrade_error_offset;bmp scale;vout output panel; 40 | 41 | initargs_sysa=init=/sbin/pre-init ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000 rootfstype=ext4 console=ttyS0,115200n8 no_console_suspend earlycon=aml-uart,0xff803000 ro rootwait skip_initramfs root=/dev/mmcblk0p14 androidboot.slot_suffix=_a 42 | initargs_sysb=init=/sbin/pre-init ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000 rootfstype=ext4 console=ttyS0,115200n8 no_console_suspend earlycon=aml-uart,0xff803000 ro rootwait skip_initramfs root=/dev/mmcblk0p15 androidboot.slot_suffix=_b 43 | 44 | splash_boot=imgread pic logo bootup_spotify $loadaddr;bmp display $bootup_spotify_offset;bmp scale;run storeboot; 45 | bootargs_video=logo=osd0,loaded,0x1f800000 fb_width=480 fb_height=800 vout=panel,enable panel_type=lcd_8 frac_rate_policy=1 osd_reverse=0 video_reverse=0 46 | storeargs=setenv bootargs ${initargs} ${bootargs_video} reboot_mode_android=normal androidboot.selinux=${EnableSelinux} androidboot.firstboot=${firstboot} androidboot.hardware=amlogic irq_check_en=0 jtag=disable uboot_version=${gitver}; setenv avb2 0; if gpio input GPIOA_3; then run init_display_burn; run update; fi; 47 | storeboot=boot_cooling;run storeargs;get_valid_slot;consume_boot_try;if imgread kernel ${boot_part} ${loadaddr}; then bootm ${loadaddr}; fi;run update; 48 | 49 | boot_slot_normal=run init_display_normal; setenv initargs ${initargs_sysa}; setenv active_slot _a; setenv boot_part boot_a; 50 | boot_slot_recovery=run init_display_recovery; setenv initargs ${initargs_sysb}; setenv active_slot _b; setenv boot_part boot_b; 51 | 52 | pick_boot_slot=if gpio input GPIOA_0; then run boot_slot_recovery; else run boot_slot_normal; fi; 53 | 54 | preboot=run bcb_cmd; run pick_boot_slot; run storeargs;bcb uboot-command; run storeboot; 55 | -------------------------------------------------------------------------------- /files/env/env_stock.txt: -------------------------------------------------------------------------------- 1 | EnableSelinux=enforcing 2 | Irq_check_en=0 3 | active_slot=_a 4 | avb2=1 5 | baudrate=115200 6 | bcb_cmd=get_avb_mode;get_valid_slot; 7 | boot_part=boot 8 | bootcmd=run check_charger 9 | check_charger=mw 0xFF6346DC 0x33000000;mw.b 0x1337DEAD 0x00 1;mw.b 0x1330DEAD 0x12 1;mw.b 0x1331DEAD 0x13 1;mw.b 0x1332DEAD 0x15 1;mw.b 0x1333DEAD 0x16 1;i2c dev 2;i2c read 0x35 0x3 1 0x1337DEAD;if cmp.b 0x1337DEAD 0x1330DEAD 1; then run storeboot;elif cmp.b 0x1337DEAD 0x1331DEAD 1; then run storeboot;elif cmp.b 0x1337DEAD 0x1332DEAD 1; then run storeboot;elif cmp.b 0x1337DEAD 0x1333DEAD 1; then run storeboot;else osd open;osd clear;imgread pic logo bad_charger $loadaddr;bmp display $bad_charger_offset;bmp scale;vout output ${outputmode};while true; do sleep 1; if gpio input GPIOAO_3; then run splash_boot; fi; i2c read 0x35 0x3 1 0x1337DEAD;if cmp.b 0x1337DEAD 0x1330DEAD 1; then run splash_boot;elif cmp.b 0x1337DEAD 0x1331DEAD 1; then run splash_boot;elif cmp.b 0x1337DEAD 0x1332DEAD 1; then run splash_boot;elif cmp.b 0x1337DEAD 0x1333DEAD 1; then run splash_boot;fi;i2c mw 0x35 0x09 0x8F 1;done;fi; 10 | display_bpp=16 11 | display_color_bg=0 12 | display_color_fg=0xffff 13 | display_color_index=16 14 | display_height=800 15 | display_init=1 16 | display_layer=osd0 17 | display_stack=unknown 18 | display_width=480 19 | dtb_mem_addr=0x1000000 20 | fb_addr=0x1f800000 21 | fb_height=800 22 | fb_width=480 23 | fdt_high=0x20000000 24 | firstboot=0 25 | frac_rate_policy=1 26 | fs_type=ro rootwait skip_initramfs 27 | init_display=if test ${display_init} = 1; then osd open;osd clear;imgread pic logo bootup_spotify $loadaddr;bmp display $bootup_spotify_offset;bmp scale;vout output ${outputmode};fi; 28 | initargs=init=/sbin/pre-init quiet ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000 rootfstype=ext4 29 | jtag=disable 30 | loadaddr=1080000 31 | lock=10001000 32 | osd_reverse=0 33 | outputmode=panel 34 | panel_type=lcd_8 35 | preboot=run bcb_cmd; run init_display;run storeargs;bcb uboot-command; 36 | reboot_mode_android=normal 37 | recovery_offset=0 38 | recovery_part=recovery 39 | sdc_burning=sdc_burn ${sdcburncfg} 40 | sdcburncfg=aml_sdc_burn.ini 41 | silent=on 42 | splash_boot=imgread pic logo bootup_spotify $loadaddr;bmp display $bootup_spotify_offset;bmp scale;run storeboot; 43 | storeargs=setenv bootargs ${initargs} ${fs_type} reboot_mode_android=${reboot_mode_android} logo=${display_layer},loaded,${fb_addr} fb_width=${fb_width} fb_height=${fb_height} vout=${outputmode},enable panel_type=${panel_type} frac_rate_policy=${frac_rate_policy} osd_reverse=${osd_reverse} video_reverse=${video_reverse} irq_check_en=${Irq_check_en} androidboot.selinux=${EnableSelinux} androidboot.firstboot=${firstboot} jtag=${jtag} uboot_version=${gitver};setenv bootargs ${bootargs} androidboot.hardware=amlogic; 44 | storeboot=boot_cooling;run storeargs;get_valid_slot;setenv bootargs ${bootargs} androidboot.slot_suffix=${active_slot};consume_boot_try;if imgread kernel ${boot_part} ${loadaddr}; then bootm ${loadaddr}; fi;run update; 45 | system_mode=1 46 | try_auto_burn=update 700 750; 47 | update=run usb_burning; 48 | upgrade_step=0 49 | usb_burning=update 1000 50 | video_reverse=0 51 | wipe_cache=successful 52 | wipe_data=successful -------------------------------------------------------------------------------- /files/env/env_switchable.txt: -------------------------------------------------------------------------------- 1 | EnableSelinux=enforcing 2 | Irq_check_en=0 3 | active_slot=_a 4 | boot_part=boot_a 5 | avb2=0 6 | baudrate=115200 7 | display_bpp=16 8 | display_color_bg=0 9 | display_color_fg=0xffff 10 | display_color_index=16 11 | display_height=800 12 | display_init=1 13 | display_width=480 14 | dtb_mem_addr=0x1000000 15 | fb_addr=0x1f800000 16 | fdt_high=0x20000000 17 | loadaddr=1080000 18 | lock=10001000 19 | firstboot=0 20 | recovery_offset=0 21 | recovery_part=recovery 22 | 23 | system_mode=1 24 | try_auto_burn=update 700 750; 25 | update=run usb_burning; 26 | upgrade_step=0 27 | usb_burning=update 1000 28 | wipe_cache=successful 29 | wipe_data=successful 30 | 31 | sdc_burning=sdc_burn ${sdcburncfg} 32 | sdcburncfg=aml_sdc_burn.ini 33 | silent=on 34 | bcb_cmd=get_avb_mode;get_valid_slot; 35 | bootcmd=run storeboot 36 | 37 | init_display_normal=osd open;osd clear;imgread pic logo bootup_spotify $loadaddr;bmp display $bootup_spotify_offset;bmp scale;vout output panel; 38 | init_display_recovery=osd open;osd clear;imgread pic logo upgrade_logo $loadaddr;bmp display $upgrade_logo_offset;bmp scale;vout output panel; 39 | init_display_debian=osd open;osd clear;imgread pic logo upgrade_success $loadaddr;bmp display $upgrade_success_offset;bmp scale;vout output panel; 40 | init_display_burn=osd open;osd clear;imgread pic logo upgrade_error $loadaddr;bmp display $upgrade_error_offset;bmp scale;vout output panel; 41 | 42 | initargs_sysa=init=/sbin/pre-init ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000 rootfstype=ext4 console=ttyS0,115200n8 no_console_suspend earlycon=aml-uart,0xff803000 ro rootwait skip_initramfs root=/dev/mmcblk0p14 androidboot.slot_suffix=_a 43 | initargs_sysb=init=/sbin/pre-init ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000 rootfstype=ext4 console=ttyS0,115200n8 no_console_suspend earlycon=aml-uart,0xff803000 ro rootwait skip_initramfs root=/dev/mmcblk0p15 androidboot.slot_suffix=_b 44 | initargs_debian=init=/usr/sbin/init ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000 rootfstype=ext4 console=ttyS0,115200n8 no_console_suspend earlycon=aml-uart,0xff803000 rw rootwait skip_initramfs root=/dev/mmcblk0p18 androidboot.slot_suffix=_a 45 | 46 | splash_boot=imgread pic logo bootup_spotify $loadaddr;bmp display $bootup_spotify_offset;bmp scale;run storeboot; 47 | bootargs_video=logo=osd0,loaded,0x1f800000 fb_width=480 fb_height=800 vout=panel,enable panel_type=lcd_8 frac_rate_policy=1 osd_reverse=0 video_reverse=0 48 | storeargs=setenv bootargs ${initargs} ${bootargs_video} reboot_mode_android=normal androidboot.selinux=${EnableSelinux} androidboot.firstboot=${firstboot} androidboot.hardware=amlogic irq_check_en=0 jtag=disable uboot_version=${gitver}; setenv avb2 0; if gpio input GPIOA_3; then run init_display_burn; run update; fi; 49 | storeboot=boot_cooling;run storeargs;get_valid_slot;consume_boot_try;if imgread kernel ${boot_part} ${loadaddr}; then bootm ${loadaddr}; fi;run update; 50 | 51 | 52 | boot_slot_normal=run init_display_normal; setenv initargs ${initargs_sysa}; setenv active_slot _a; setenv boot_part boot_a; 53 | boot_slot_recovery=run init_display_recovery; setenv initargs ${initargs_sysb}; setenv active_slot _b; setenv boot_part boot_b; 54 | boot_slot_debian=run init_display_debian; setenv initargs ${initargs_debian}; setenv active_slot _a; setenv boot_part boot_a; 55 | 56 | pick_boot_slot=if gpio input GPIOA_0; then run boot_slot_normal; else run boot_slot_debian; fi; 57 | 58 | preboot=run bcb_cmd; run pick_boot_slot; run storeargs;bcb uboot-command; run storeboot; 59 | -------------------------------------------------------------------------------- /files/logo/Readme.md: -------------------------------------------------------------------------------- 1 | # Logos 2 | 3 | These were extracted using [`aml-imgpack`](../../aml-imgpack.py), and then modified using GIMP. 4 | The script `build_image.sh` will rebuild `logo.dump` using these images. 5 | 6 | Images must be 16bit BMP. 7 | 8 | Using GIMP: File -> Export, Advanced Options, 16bits R5 G6 B5 9 | -------------------------------------------------------------------------------- /files/logo/bad_charger.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/bad_charger.bmp -------------------------------------------------------------------------------- /files/logo/bootup.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/bootup.bmp -------------------------------------------------------------------------------- /files/logo/bootup_spotify.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/bootup_spotify.bmp -------------------------------------------------------------------------------- /files/logo/upgrade_bar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/upgrade_bar.bmp -------------------------------------------------------------------------------- /files/logo/upgrade_error.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/upgrade_error.bmp -------------------------------------------------------------------------------- /files/logo/upgrade_fail.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/upgrade_fail.bmp -------------------------------------------------------------------------------- /files/logo/upgrade_logo.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/upgrade_logo.bmp -------------------------------------------------------------------------------- /files/logo/upgrade_success.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/upgrade_success.bmp -------------------------------------------------------------------------------- /files/logo/upgrade_unfocus.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/upgrade_unfocus.bmp -------------------------------------------------------------------------------- /files/logo/upgrade_upgrading.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/files/logo/upgrade_upgrading.bmp -------------------------------------------------------------------------------- /files/system_a/etc/fstab: -------------------------------------------------------------------------------- 1 | # 2 | /dev/system_a / ext4 ro,noload,noauto,noatime,errors=remount-ro 0 1 3 | /dev/data /mnt/root_data ext4 rw 0 0 4 | proc /proc proc defaults 0 0 5 | devpts /dev/pts devpts defaults,gid=5,mode=620,ptmxmode=0666 0 0 6 | tmpfs /var tmpfs mode=0777 0 0 7 | tmpfs /dev/shm tmpfs mode=0777 0 0 8 | tmpfs /tmp tmpfs mode=1777 0 0 9 | tmpfs /run tmpfs mode=0755,nosuid,nodev 0 0 10 | sysfs /sys sysfs defaults 0 0 -------------------------------------------------------------------------------- /files/system_a/etc/init.d/S49usbgadget: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Setup Linux USB Gadget for adb, rndis 3 | # this is meant to be placed at /etc/init.d/S49usbgadget on the system_a partition of device 4 | 5 | # Available options: 6 | 7 | # usb_f_rndis.ko 8 | # usb_f_fs.ko 9 | # usb_f_midi.ko 10 | # usb_f_mtp.ko 11 | # usb_f_ptp.ko 12 | # usb_f_audio_source.ko 13 | # usb_f_accessory.ko 14 | 15 | 16 | ######### Variables 17 | 18 | LANG="0x409" # english 19 | SERIAL_NUMBER="12345678" 20 | # 18d1:4e40 Google Inc. Nexus 7 21 | ID_VENDOR="0x18d1" 22 | ID_PRODUCT="0x4e40" 23 | MANUFACTURER="Spotify" 24 | PRODUCT="Superbird" 25 | ADBD_LOG_FILE="/tmp/adbd.log" 26 | 27 | 28 | # Research 29 | # starting point: https://github.com/frederic/superbird-bulkcmd/blob/main/scripts/enable-adb.sh.client 30 | # info about configfs https://elinux.org/images/e/ef/USB_Gadget_Configfs_API_0.pdf 31 | # info about usbnet and bridging https://developer.ridgerun.com/wiki/index.php/How_to_use_USB_device_networking 32 | # more info, including for windows https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget 33 | # a gist that was helpful: https://gist.github.com/geekman/5bdb5abdc9ec6ac91d5646de0c0c60c4 34 | # https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt 35 | 36 | ######### Functions 37 | 38 | create_device() { 39 | # create usb gadget device 40 | ID_VEND="$1" 41 | ID_PROD="$2" 42 | BCD_DEVICE="$3" 43 | BCD_USB="$4" 44 | LANGUAGE="$5" 45 | echo "### Creating device $ID_VEND $ID_PROD" 46 | mkdir -p "/dev/usb-ffs" 47 | mkdir -p "/dev/usb-ffs/adb" 48 | mountpoint /sys/kernel/config/ || mount -t configfs none "/sys/kernel/config/" 49 | mkdir -p "/sys/kernel/config/usb_gadget/g1" 50 | echo "$ID_VEND" > "/sys/kernel/config/usb_gadget/g1/idVendor" 51 | echo "$ID_PROD" > "/sys/kernel/config/usb_gadget/g1/idProduct" 52 | echo "$BCD_DEVICE" > "/sys/kernel/config/usb_gadget/g1/bcdDevice" 53 | echo "$BCD_USB" > "/sys/kernel/config/usb_gadget/g1/bcdUSB" 54 | mkdir -p "/sys/kernel/config/usb_gadget/g1/strings/${LANGUAGE}" 55 | sleep 1 56 | } 57 | 58 | configure_device() { 59 | # configure usb gadget device 60 | MANUF="$1" 61 | PROD="$2" 62 | SERIAL="$3" 63 | LANGUAGE="$4" 64 | CONFIG_ID="$5" 65 | CONFIG_NAME="$6" 66 | echo "### Configuring device as $MANUF $PROD" 67 | echo "$MANUF" > "/sys/kernel/config/usb_gadget/g1/strings/${LANGUAGE}/manufacturer" 68 | echo "$PROD" > "/sys/kernel/config/usb_gadget/g1/strings/${LANGUAGE}/product" 69 | echo "$SERIAL" > "/sys/kernel/config/usb_gadget/g1/strings/${LANGUAGE}/serialnumber" 70 | mkdir -p "/sys/kernel/config/usb_gadget/g1/configs/${CONFIG_ID}" 71 | mkdir -p "/sys/kernel/config/usb_gadget/g1/configs/${CONFIG_ID}/strings/${LANGUAGE}" 72 | echo "$CONFIG_NAME" > "/sys/kernel/config/usb_gadget/g1/configs/${CONFIG_ID}/strings/${LANGUAGE}/configuration" 73 | echo 500 > "/sys/kernel/config/usb_gadget/g1/configs/${CONFIG_ID}/MaxPower" 74 | ln -s "/sys/kernel/config/usb_gadget/g1/configs/${CONFIG_ID}" "/sys/kernel/config/usb_gadget/g1/os_desc/${CONFIG_ID}" 75 | sleep 1 76 | } 77 | 78 | add_function(){ 79 | # add a function to existing config id 80 | FUNCTION_NAME="$1" 81 | CONFIG_ID="$2" 82 | echo "### adding function $FUNCTION_NAME to config $CONFIG_ID" 83 | mkdir -p "/sys/kernel/config/usb_gadget/g1/functions/${FUNCTION_NAME}" 84 | ln -s "/sys/kernel/config/usb_gadget/g1/functions/${FUNCTION_NAME}" "/sys/kernel/config/usb_gadget/g1/configs/${CONFIG_ID}" 85 | } 86 | 87 | start_adb_daemon() { 88 | # mount adb functionfs and start daemon 89 | LOG_FILE="$1" 90 | echo "### starting adb daemon" 91 | mkdir -p /dev/usb-ffs/adb 92 | mount -t functionfs adb /dev/usb-ffs/adb 93 | if [ ! -f "/usr/bin/adbd" ]; then 94 | echo "Unable to find adbd binary!" 95 | else 96 | /usr/bin/adbd > "$LOG_FILE" 2>&1 & 97 | fi 98 | sleep 5s 99 | } 100 | 101 | attach_driver(){ 102 | # attach the created gadget device to our UDC driver 103 | UDC_DEVICE=$(/bin/ls -1 /sys/class/udc/) # ff400000.dwc2_a 104 | echo "### Attaching gadget to UDC device: $UDC_DEVICE" 105 | echo "$UDC_DEVICE" > /sys/kernel/config/usb_gadget/g1/UDC 106 | sleep 1 107 | } 108 | 109 | configure_usbnet() { 110 | DEVICE="$1" 111 | NETWORK="$2" # just the first 3 octets 112 | NETMASK="$3" 113 | echo "### bringing up $DEVICE with ${NETWORK}.2" 114 | ifconfig "$DEVICE" up 115 | ifconfig "$DEVICE" "${NETWORK}.2" netmask "$NETMASK" broadcast "${NETWORK}.255" 116 | echo "adding routes for $DEVICE" 117 | ip route add default via "${NETWORK}.1" dev "$DEVICE" 118 | sleep 1 119 | } 120 | 121 | ######### Entrypoint 122 | 123 | echo "### Configuring USB Gadget with adb and rndis" 124 | create_device "$ID_VENDOR" "$ID_PRODUCT" "0x0223" "0x0200" "$LANG" 125 | configure_device "$MANUFACTURER" "$PRODUCT" "$SERIAL_NUMBER" "$LANG" "b.1" "Multi-Function Device" 126 | 127 | add_function "ffs.adb" "b.1" # adb 128 | add_function "rndis.usb0" "b.1" # rndis usb ethernet 129 | 130 | start_adb_daemon "$ADBD_LOG_FILE" 131 | 132 | attach_driver 133 | 134 | configure_usbnet "usb0" "192.168.7" "255.255.255.0" 135 | 136 | echo "Done setting up USB Gadget" 137 | -------------------------------------------------------------------------------- /files/system_a/etc/inittab: -------------------------------------------------------------------------------- 1 | # /etc/inittab 2 | # 3 | # Copyright (C) 2001 Erik Andersen 4 | # 5 | # Note: BusyBox init doesn't support runlevels. The runlevels field is 6 | # completely ignored by BusyBox init. If you want runlevels, use 7 | # sysvinit. 8 | # 9 | # Format for each entry: ::: 10 | # 11 | # id == tty to run on, or empty for /dev/console 12 | # runlevels == ignored 13 | # action == one of sysinit, respawn, askfirst, wait, and once 14 | # process == program to run 15 | 16 | # Startup the system 17 | ::sysinit:/bin/mount -t proc proc /proc 18 | ::sysinit:/bin/mkdir /dev/shm 19 | ::sysinit:/bin/mkdir /dev/pts 20 | ::sysinit:/bin/mount -t tmpfs /var 21 | ::sysinit:/bin/mkdir /var/log 22 | ::sysinit:/bin/mount -a 23 | ::sysinit:/bin/hostname -F /etc/hostname 24 | ::sysinit:/sbin/ifconfig lo 127.0.0.1 up 25 | ::sysinit:/sbin/route add -net 127.0.0.0 netmask 255.0.0.0 lo 26 | # now run any rc scripts 27 | ::sysinit:/etc/init.d/rcS 28 | 29 | # Set up a couple of getty's 30 | tty1::once:/sbin/getty 38400 tty1 31 | tty2::once:/sbin/getty 38400 tty2 32 | 33 | # Put a getty on the serial port 34 | #ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100 # UNSUPPORT GENERIC_SERIAL 35 | ttyS0::respawn:-/bin/sh # AMLOGIC_GENERAL_SERIAL 36 | 37 | # Logging junk 38 | #null::sysinit:/bin/touch /var/log/messages 39 | null::respawn:/sbin/syslogd -n 40 | null::respawn:/sbin/klogd -n 41 | #tty3::once:/usr/bin/tail -f /var/log/messages 42 | 43 | # Stuff to do for the 3-finger salute 44 | ::ctrlaltdel:/sbin/reboot 45 | 46 | # Stuff to do before rebooting 47 | null::shutdown:/usr/bin/killall klogd 48 | null::shutdown:/usr/bin/killall syslogd 49 | null::shutdown:/etc/init.d/rcK 50 | null::shutdown:/bin/umount -a -r 51 | null::shutdown:/sbin/swapoff -a 52 | 53 | null::sysinit:/usr/bin/remotecfg /etc/remote.conf 54 | null::sysinit:echo 0 > /sys/class/graphics/fb0/blank 55 | #null::sysinit:echo 0 > /sys/class/graphics/fb1/free_scale 56 | #null::sysinit:echo 0 > /sys/class/graphics/fb0/free_scale 57 | null::sysinit:echo 0 > /sys/module/amvdec_h264mvc/parameters/view_mode 58 | null::sysinit:echo 96000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq 59 | null::sysinit:echo interactive > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 60 | null::sysinit:echo 0 > /sys/class/freq_limit/limit 61 | null::sysinit:echo 1488000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_dflt_freq 62 | null::sysinit:echo "nameserver 8.8.8.8" >> /etc/resolv.conf 63 | #::sysinit:/lib/preinit/auto_reboot.sh 64 | -------------------------------------------------------------------------------- /flash_test_image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # flash data partition from the latest image created by build_image.sh to a connected device 4 | # expects a device already in USB Mode or USB Burn Mode 5 | # also expects a compiled version of superbird_tool in root's PATH 6 | 7 | # all config lives in image_config.sh 8 | source ./image_config.sh 9 | 10 | CURRENT_IMAGE="${EXISTING_DUMP}/data.ext4" 11 | 12 | echo "Going to flash data partition of connected device using $CURRENT_IMAGE" 13 | 14 | if [ ! -f "$CURRENT_IMAGE" ]; then 15 | echo "Could not find: $CURRENT_IMAGE" 16 | echo " need to run ./build_image.sh first!" 17 | exit 1 18 | fi 19 | 20 | sudo superbird_tool --restore_partition data "$CURRENT_IMAGE" 21 | 22 | -------------------------------------------------------------------------------- /image_config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Shared config for image scripts 4 | # shellcheck disable=SC2034 5 | 6 | # where to find an existing superbird device dump, to use for creating a debian image 7 | EXISTING_DUMP="./dumps/debian_current" 8 | 9 | # Network info 10 | HOST_NAME="superbird" 11 | USBNET_PREFIX="192.168.7" # usb network will use .1 as host device, and .2 for superbird 12 | 13 | # User info 14 | USER_NAME="superbird" 15 | # generate hash: openssl passwd -6 "superbird" 16 | # shellcheck disable=SC2016 17 | USER_PASS_HASH='$6$zeM8ZwO/Xke05h6X$UtmM0sIBznj4hxmd/UGUO1YHUr0emOn.9u7G1yQRVGR4XutYCstDzVLzpUw9PNWrhYRAEg73ovkC4JNPFlSmI1' 18 | 19 | # config for debootstrap 20 | ARCHITECTURE="arm64" 21 | DISTRO_REPO_URL="http://deb.debian.org/debian/" 22 | DISTRO_BRANCH="trixie" 23 | DISTRO_VARIANT="minbase" 24 | 25 | # you can add extra packages here to install during stage 2 26 | # will be installed like this (in chroot): apt install -y --no-install-recommends --no-install-suggests $EXTRA_PACKAGES 27 | EXTRA_PACKAGES="" 28 | 29 | # timezone and locale 30 | TIMEZONE="America/Los_Angeles" 31 | LOCALE="en_US.UTF-8" 32 | -------------------------------------------------------------------------------- /pictures/superbird_ha_landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/pictures/superbird_ha_landscape.jpg -------------------------------------------------------------------------------- /pictures/superbird_ha_portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/pictures/superbird_ha_portrait.jpg -------------------------------------------------------------------------------- /pictures/superbird_landscape_back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/pictures/superbird_landscape_back.jpg -------------------------------------------------------------------------------- /pictures/superbird_poe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/pictures/superbird_poe.jpg -------------------------------------------------------------------------------- /pictures/superbird_wall_mount.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bishopdynamics/superbird-debian-kiosk/487bf52be79d366a055d880557607129ac92d694/pictures/superbird_wall_mount.jpg -------------------------------------------------------------------------------- /setup_host.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # setup a debian/ubuntu host machine to provide internet for USB device connected 4 | # this should be run ONCE on the host machine 5 | 6 | # all config lives in image_config.sh 7 | source ./image_config.sh 8 | 9 | # need to be root 10 | if [ "$(id -u)" != "0" ]; then 11 | echo "Must be run as root" 12 | exit 1 13 | fi 14 | 15 | if [ "$(uname -s)" != "Linux" ]; then 16 | echo "Only works on Linux!" 17 | exit 1 18 | fi 19 | 20 | function remove_if_exists() { 21 | # remove a file if it exists 22 | FILEPATH="$1" 23 | if [ -f "$FILEPATH" ]; then 24 | echo "found ${FILEPATH}, removing" 25 | rm "$FILEPATH" 26 | fi 27 | } 28 | 29 | function append_if_missing() { 30 | # append string to file only if it does not already exist in the file 31 | STRING="$1" 32 | FILEPATH="$2" 33 | grep -q "$STRING" "$FILEPATH" || { 34 | echo "appending \"$STRING\" to $FILEPATH" 35 | echo "$STRING" >> "$FILEPATH" 36 | return 1 37 | } 38 | echo "Already found \"$STRING\" in $FILEPATH" 39 | return 0 40 | } 41 | 42 | function forward_port() { 43 | # usage: forward_port 44 | # forward a tcp port to access service on superbird via host 45 | # if no superbird port is provided, same port number is used for both 46 | SOURCE="$1" 47 | DEST="$2" 48 | if [ -z "$DEST" ]; then 49 | DEST="$SOURCE" 50 | fi 51 | iptables -t nat -A PREROUTING -p tcp -i eth0 --dport "$SOURCE" -j DNAT --to-destination "${USBNET_PREFIX}.2:$DEST" 52 | iptables -t nat -A PREROUTING -p tcp -i wlan0 --dport "$SOURCE" -j DNAT --to-destination "${USBNET_PREFIX}.2:$DEST" 53 | iptables -A FORWARD -p tcp -d "${USBNET_PREFIX}.2" --dport "$DEST" -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT 54 | } 55 | 56 | set -e # bail on any errors 57 | 58 | # install needed packages 59 | # NOTE: the flag "--break-system-packages" only exists on recent debian/ubuntu versions, 60 | # so we have to try with, and if there is an error try again without the flag 61 | export DEBIAN_FRONTEND=noninteractive 62 | apt install -y git htop build-essential cmake python3 python3-dev python3-pip iptables adb android-sdk-platform-tools-common iptables-persistent 63 | python3 -m pip install --break-system-packages virtualenv nuitka ordered-set || { 64 | python3 -m pip install virtualenv nuitka ordered-set 65 | } 66 | python3 -m pip install --break-system-packages git+https://github.com/superna9999/pyamlboot || { 67 | python3 -m pip install git+https://github.com/superna9999/pyamlboot 68 | } 69 | 70 | # fix usb enumeration when connecting superbird in maskroom mode 71 | echo '# Amlogic S905 series can be booted up in Maskrom Mode, and it needs a rule to show up correctly' > /etc/udev/rules.d/70-carthing-maskrom-mode.rules 72 | echo 'SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="1b8e", ATTR{idProduct}=="c003", MODE:="0666", SYMLINK+="worldcup"' >> /etc/udev/rules.d/70-carthing-maskrom-mode.rules 73 | 74 | # prevent systemd / udev from renaming usb network devices by mac address 75 | remove_if_exists /lib/systemd/network/73-usb-net-by-mac.link 76 | remove_if_exists /lib/udev/rules.d/73-usb-net-by-mac.rules 77 | 78 | # allow IP forwarding 79 | append_if_missing "net.ipv4.ip_forward = 1" /etc/sysctl.conf || { 80 | sysctl -p # reload from conf 81 | } 82 | 83 | # forwarding rules 84 | mkdir -p /etc/iptables 85 | 86 | # clear all iptables rules 87 | iptables -F 88 | iptables -X 89 | iptables -Z 90 | iptables -t nat -F 91 | iptables -t nat -X 92 | iptables -t mangle -F 93 | iptables -t mangle -X 94 | iptables -t raw -F 95 | iptables -t raw -X 96 | 97 | # rewrite iptables rules 98 | iptables -P FORWARD ACCEPT 99 | iptables -A FORWARD -o eth0 -i eth1 -s "${USBNET_PREFIX}.0/24" -m conntrack --ctstate NEW -j ACCEPT 100 | iptables -A FORWARD -o eth0 -i eth1 -s "${USBNET_PREFIX}.0/24" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 101 | iptables -A POSTROUTING -t nat -j MASQUERADE -s "${USBNET_PREFIX}.0/24" 102 | 103 | # port forwards: 104 | # 2022: ssh on superbird 105 | # 5900: vnc on superbird 106 | # 9222: chromium remote debugging on superbird 107 | 108 | forward_port 2022 22 109 | forward_port 5900 110 | forward_port 9222 111 | 112 | # persist rules to file 113 | iptables-save > /etc/iptables/rules.v4 114 | 115 | # write the usb network config 116 | mkdir -p /etc/network/interfaces.d/ 117 | 118 | cat << EOF > /etc/network/interfaces.d/usb0 119 | # generated by $0 120 | allow-hotplug usb0 121 | iface usb0 inet static 122 | address ${USBNET_PREFIX}.1 123 | netmask 255.255.255.0 124 | EOF 125 | 126 | # add superbird to /etc/hosts 127 | append_if_missing "${USBNET_PREFIX}.2 ${HOST_NAME}" "/etc/hosts" 128 | 129 | echo "Need to reboot for all changes to take effect" 130 | 131 | -------------------------------------------------------------------------------- /update_local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # update scripts and service files on a locally attached device 4 | # will not overwrite any existing buttons_settings.py or chromium_settings.sh 5 | # will not overwrite xorg.conf 6 | # will not overwrite vnc_passwd 7 | # this is intended to run on the host device, and expects key-based ssh authentication has already been setup with superbird 8 | 9 | set -e 10 | 11 | # all config lives in image_config.sh 12 | source ./image_config.sh 13 | 14 | #### Functions 15 | 16 | deploy_service() { 17 | # copy given service file and restart that service 18 | SVC_NAME="$1" 19 | echo "deploying $SVC_NAME" 20 | ssh "${USER_NAME}@${HOST_NAME}" "sudo touch /lib/systemd/system/$SVC_NAME" 21 | ssh "${USER_NAME}@${HOST_NAME}" "sudo chown ${USER_NAME} /lib/systemd/system/$SVC_NAME" 22 | scp "./files/data/lib/systemd/system/$SVC_NAME" "${USER_NAME}@${HOST_NAME}":/lib/systemd/system/ 23 | ssh "${USER_NAME}@${HOST_NAME}" "sudo systemctl restart $SVC_NAME" >/dev/null 2>&1 24 | ssh "${USER_NAME}@${HOST_NAME}" "sudo ln -sf /lib/systemd/system/$SVC_NAME /etc/systemd/system/multi-user.target.wants/$SVC_NAME" 25 | } 26 | 27 | deploy_script() { 28 | # copy given script file 29 | # does not change file mode, presumes new file is correct mode already 30 | SCR_NAME="$1" 31 | scp "./files/data/scripts/$SCR_NAME" "${USER_NAME}@${HOST_NAME}":/scripts/ 32 | } 33 | 34 | deploy_script_if_missing() { 35 | # deploy script only if it is missing 36 | # does not change file mode, presumes new file is correct mode already 37 | SCR_NAME="$1" 38 | SCR_MISSING=$(ssh "${USER_NAME}@${HOST_NAME}" "if [ ! -f /scripts/$SCR_NAME ]; then echo missing; fi") 39 | if [ "$SCR_MISSING" == "missing" ]; then 40 | deploy_script "$SCR_NAME" 41 | fi 42 | } 43 | 44 | #### Entrypoint 45 | 46 | echo "" 47 | echo "Upgrading locally connected device" 48 | 49 | echo "" 50 | echo "Installing packages" 51 | # install packages, most of which should already be installed 52 | ssh "${USER_NAME}@${HOST_NAME}" "sudo apt update && sudo apt install -y --no-install-recommends --no-install-suggests chromium python3-minimal python3-pip tigervnc-scraping-server" 53 | 54 | # install required python packages via pip 55 | ssh "${USER_NAME}@${HOST_NAME}" "sudo chown -R ${USER_NAME} /scripts" 56 | deploy_script requirements.txt 57 | ssh "${USER_NAME}@${HOST_NAME}" "sudo python3 -m pip install -r /scripts/requirements.txt --break-system-packages" 58 | 59 | echo "" 60 | echo "Deploying scripts and services" 61 | 62 | # Now deploy scripts and services 63 | 64 | # deploy inittab and fstab 65 | scp "./files/data/etc/inittab" "${USER_NAME}@${HOST_NAME}":/tmp/inittab 66 | ssh "${USER_NAME}@${HOST_NAME}" "sudo mv /tmp/inittab /etc/inittab" 67 | 68 | scp "./files/data/etc/fstab" "${USER_NAME}@${HOST_NAME}":/tmp/fstab 69 | ssh "${USER_NAME}@${HOST_NAME}" "sudo mv /tmp/fstab /etc/fstab" 70 | 71 | # check if /dev/settings is mounted, if not then format settings, and mount it (restart chromium) 72 | CFG_MISSING=$(ssh "${USER_NAME}@${HOST_NAME}" " mount |grep -q /dev/settings || echo missing") 73 | if [ "$CFG_MISSING" == "missing" ]; then 74 | echo "Migrating chromium profile at /config to use settings partition" 75 | ssh "${USER_NAME}@${HOST_NAME}" "sudo systemctl stop chromium" # dont need to start it, will get restarted when re-deployed later 76 | ssh "${USER_NAME}@${HOST_NAME}" "sudo umount /config" # just in case 77 | ssh "${USER_NAME}@${HOST_NAME}" "sudo rm -r /config" 78 | ssh "${USER_NAME}@${HOST_NAME}" "sudo mkfs.ext4 -F /dev/settings" 79 | ssh "${USER_NAME}@${HOST_NAME}" "sudo mkdir /config" 80 | ssh "${USER_NAME}@${HOST_NAME}" "sudo mount /config" 81 | fi 82 | 83 | 84 | deploy_script_if_missing buttons_settings.py 85 | deploy_script_if_missing chromium_settings.sh 86 | deploy_script_if_missing vnc_passwd 87 | 88 | deploy_script buttons_app.py 89 | deploy_script clear_display.sh 90 | deploy_script setup_backlight.sh 91 | deploy_script setup_display.sh 92 | deploy_script setup_usbgadget.sh 93 | deploy_script setup_vnc.sh 94 | deploy_script start_buttons.sh 95 | deploy_script start_chromium.sh 96 | 97 | 98 | echo "" 99 | echo "Deploying services, You can ignore the warnings about reloading units" 100 | echo "" 101 | 102 | deploy_service backlight.service 103 | deploy_service buttons.service 104 | deploy_service chromium.service 105 | deploy_service vnc.service 106 | 107 | 108 | echo "Generating /etc/hosts" 109 | HOSTS_CONTENT=$( 110 | cat <<- EOHF 111 | # generated by $0 112 | 127.0.0.1 localhost 113 | 127.0.0.1 $HOST_NAME 114 | ::1 localhost $HOST_NAME ip6-localhost ip6-loopback 115 | ff02::1 ip6-allnodes 116 | ff02::2 ip6-allrouters 117 | ${USBNET_PREFIX}.1 host 118 | EOHF 119 | ) 120 | echo "$HOSTS_CONTENT" > /tmp/hosts 121 | scp /tmp/hosts "${USER_NAME}@${HOST_NAME}":/tmp/hosts 122 | ssh "${USER_NAME}@${HOST_NAME}" "sudo cp /tmp/hosts /etc/hosts" 123 | rm /tmp/hosts 124 | 125 | deploy_service usbgadget.service 126 | 127 | echo "" 128 | echo "Done deploying to device" 129 | echo "" 130 | --------------------------------------------------------------------------------