├── .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 |
7 |
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 | 
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 |
--------------------------------------------------------------------------------