Skip to content

Commit 3e40d88

Browse files
softwarebsr-engsunpi
authored andcommitted
Add auto-imaging workflow, backup system configs, and setup script
1 parent d432df0 commit 3e40d88

8 files changed

Lines changed: 288 additions & 12 deletions

File tree

.github/workflows/build-image.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Build Custom Raspberry Pi Image
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
jobs:
9+
build-image:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
15+
- name: Download official Raspberry Pi OS Lite Base Image
16+
run: |
17+
echo "Downloading latest Raspberry Pi OS Lite (64-bit)..."
18+
curl -L -o rpi_base.img.xz "https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-latest/$(curl -s https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-latest/ | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}-raspios-[a-z]+-arm64-lite.img.xz' | head -1)"
19+
xz -d rpi_base.img.xz
20+
21+
- name: Setup QEMU for ARM Emulation
22+
uses: docker/setup-qemu-action@v3
23+
24+
- name: Modify Raspberry Pi Image
25+
uses: dtcooper/rpi-image-modifier@v1
26+
with:
27+
image: rpi_base.img
28+
script: deploy/image-setup.sh
29+
env-vars: PI_USERNAME,PI_PASSWORD
30+
env:
31+
PI_USERNAME: ${{ secrets.PI_USERNAME }}
32+
PI_PASSWORD: ${{ secrets.PI_PASSWORD }}
33+
34+
- name: Compress Custom Image
35+
run: |
36+
mv rpi_base.img driverio-custom-raspios-lite.img
37+
xz -9 driverio-custom-raspios-lite.img
38+
39+
- name: Upload Image to GitHub Release
40+
if: github.event_name == 'release'
41+
uses: softprops/action-gh-release@v2
42+
with:
43+
files: driverio-custom-raspios-lite.img.xz
44+
env:
45+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,28 +133,41 @@ sudo hwclock --show --utc # should match date -u within ~1 second
133133

134134
### Internet connectivity (WiFi first, LTE fallback)
135135

136-
InfluxDB uploads require internet at boot. The Pi prefers any saved WiFi profile and falls back to the Quectel EG25-G LTE modem when no known network is in range.
136+
The Pi is configured to automatically manage both WiFi and LTE connections natively using NetworkManager, preferring WiFi when available.
137137

138-
Install the connectivity service:
138+
Since the LTE modem runs in ECM (Ethernet Control Model) mode, it presents itself as a standard Ethernet card (`usb0`). To prevent `ModemManager` from conflicting with the interface or locking the serial ports (which are used by the GPS and status scripts), `ModemManager` is disabled and masked.
139+
140+
To install the LTE and WiFi network failover configuration:
139141

140142
```bash
141-
sudo cp deploy/network-connect.default /etc/default/network-connect
142-
sudo cp deploy/network-connect.service /etc/systemd/system/
143-
chmod +x deploy/network-connect.sh
144-
sudo nmcli connection modify lte connection.autoconnect no
145-
sudo systemctl disable --now sc2-lte.service 2>/dev/null || true
143+
# 1. Copy persistent NetworkManager and udev configs
144+
sudo cp deploy/10-managed-devices.conf /etc/NetworkManager/conf.d/
145+
sudo cp deploy/99-ignore-lte-net.rules /etc/udev/rules.d/
146+
147+
# 2. Reload and apply rules
146148
sudo systemctl daemon-reload
147-
sudo systemctl enable --now network-connect.service
148-
```
149+
sudo udevadm control --reload-rules
150+
sudo udevadm trigger
149151

150-
Saved WiFi profiles (`nmcli connection show`) are tried automatically. LTE uses APN `fast.t-mobile.com` (Tello/T-Mobile) and can be overridden in `/etc/default/network-connect`.
152+
# 3. Mask and stop ModemManager (prevents interface hijacking & serial locks)
153+
sudo systemctl mask ModemManager
154+
sudo systemctl stop ModemManager || true
155+
156+
# 4. Modify connection metrics in NetworkManager
157+
# This configures WiFi to have priority (metric 100) and LTE to be the fallback (metric 600)
158+
sudo nmcli connection modify lte connection.autoconnect yes ipv4.route-metric 600 ipv6.route-metric 600
159+
160+
# 5. Restart NetworkManager to apply
161+
sudo systemctl restart NetworkManager
162+
```
151163

152164
Verify:
153165

154166
```bash
155-
systemctl status network-connect.service
167+
nmcli dev
156168
ip route show default
157-
curl -s -o /dev/null -w "%{http_code}\n" https://us-east-1-1.aws.cloud2.influxdata.com/health
169+
curl -s -o /dev/null -w "%{http_code}
170+
" https://us-east-1-1.aws.cloud2.influxdata.com/health
158171
```
159172

160173
### Bring up CAN interface
@@ -178,6 +191,63 @@ UUID=<your-uuid> /mnt/usb ext4 defaults,noatime 0 2
178191

179192
---
180193

194+
## Automated Image Building (GitHub Actions)
195+
196+
We have a GitHub Actions pipeline configured in `.github/workflows/build-image.yml` that automatically builds a pre-configured Raspberry Pi OS Lite image and publishes it in the **Releases** tab of your repository.
197+
198+
This image comes out-of-the-box with all packages installed, user accounts configured, CAN interfaces prepared, and LTE failover configurations deployed.
199+
200+
### Configuring Secrets
201+
Before running the builder, go to **Settings > Secrets and variables > Actions** in your GitHub repository and add:
202+
- **`PI_USERNAME`** (Optional): Custom username for the OS (defaults to `sunpi`).
203+
- **`PI_PASSWORD`** (Optional): Custom password for the OS (defaults to `sunpi`).
204+
205+
### Triggering the Build
206+
1. Go to the **Actions** tab of your repository.
207+
2. Select the **Build Custom Raspberry Pi Image** workflow.
208+
3. Click **Run workflow** and select the branch you wish to build.
209+
4. If building for a release, publishing a new Release on GitHub will automatically trigger the build and attach the `.img.xz` file to the release page.
210+
211+
<details>
212+
<summary><b>🚨 microSD Card Recovery Guide (Click to Expand)</b></summary>
213+
214+
In case your Raspberry Pi's microSD card becomes corrupted or fails, follow these steps to restore the system to a clean, pre-configured state:
215+
216+
#### 1. Download the Custom OS Image
217+
* Go to the **Releases** tab of the `can-telem-cloud` repository on GitHub.
218+
* Locate the latest release from the `main` branch.
219+
* Download the compressed custom image asset: `driverio-custom-raspios-lite.img.xz`.
220+
221+
#### 2. Flash the Image
222+
* Insert a new/blank microSD card into your laptop.
223+
* Open the **Raspberry Pi Imager** software (or BalenaEtcher).
224+
* For **Operating System**, select **Use Custom** and choose the downloaded `driverio-custom-raspios-lite.img.xz` file.
225+
* For **Storage**, select your microSD card.
226+
* Click **Write** and wait for the flashing and verification to complete.
227+
228+
#### 3. Boot the Pi
229+
* Remove the microSD card from your laptop and insert it into the Raspberry Pi.
230+
* Connect the CAN hat, LTE modem, and serial radio if they are not already connected.
231+
* Power on the Raspberry Pi. The initial boot may take an extra minute as the OS automatically expands the filesystem.
232+
233+
#### 4. Log In
234+
* Connect a monitor and keyboard, or log in via serial console.
235+
* Log in using the default user credentials mentioned in our **Confluence docs**.
236+
* *Note: If a custom username/password was set in your GitHub secrets during the image build, use those custom credentials instead.*
237+
238+
#### 5. Authenticate Tailscale (Required)
239+
Since Tailscale requires unique hardware node keys, you must manually authenticate the new OS install into your Tailnet:
240+
* Run the Tailscale login command:
241+
```bash
242+
sudo tailscale up
243+
```
244+
* Copy the login URL printed in the terminal, open it in your browser, and authenticate using your team's Gmail/Tailscale account.
245+
* Once logged in, the Pi will join your Tailnet, and you can now SSH into it remotely (`ssh <username>@driverio`)!
246+
247+
</details>
248+
249+
---
250+
181251
## Building
182252

183253
```bash

deploy/10-managed-devices.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[device]
2+
match-device=interface-name:usb0
3+
managed=1

deploy/99-ignore-lte-net.rules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ACTION=="add|change", KERNEL=="usb0", ENV{ID_MM_PORT_IGNORE}="1"

deploy/can-telem.service

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[Unit]
2+
Description=can-telem-cloud CAN telemetry service
3+
After=network-connect.service network-online.target ModemManager.service can0.service
4+
Wants=network-connect.service network-online.target ModemManager.service can0.service can-telem-gnss.service
5+
RequiresMountsFor=/mnt/usb
6+
7+
[Service]
8+
Type=simple
9+
WorkingDirectory=/home/sunpi/can-telem-cloud
10+
ExecStart=/home/sunpi/can-telem-cloud/can_telem -c /home/sunpi/can-telem-cloud/can_telem.conf
11+
Restart=always
12+
RestartSec=2
13+
User=root
14+
Group=root
15+
NoNewPrivileges=true
16+
17+
[Install]
18+
WantedBy=multi-user.target

deploy/can0.service

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[Unit]
2+
After=network.target
3+
4+
[Service]
5+
Type=oneshot
6+
ExecStart=/sbin/ip link set can0 up type can bitrate 500000
7+
RemainAfterExit=yes
8+
9+
[Install]
10+
WantedBy=multi-user.target

deploy/image-setup.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
# deploy/image-setup.sh - Executed inside the Pi image chroot during CI build
3+
set -euo pipefail
4+
5+
export DEBIAN_FRONTEND=noninteractive
6+
7+
# Determine username/password from environments, or default to sunpi
8+
USER_NAME="${PI_USERNAME:-sunpi}"
9+
10+
echo "==> Configuring base operating system..."
11+
12+
# 1. Update package lists and install required dependencies
13+
apt-get update
14+
apt-get install -y \
15+
build-essential \
16+
gcc \
17+
g++ \
18+
make \
19+
cmake \
20+
pkg-config \
21+
git \
22+
wget \
23+
curl \
24+
python3 \
25+
python3-dev \
26+
python3-venv \
27+
python3-pip \
28+
python3-setuptools \
29+
python-is-python3 \
30+
libssl-dev \
31+
libffi-dev \
32+
dnsmasq
33+
34+
# 2. Install Tailscale
35+
echo "==> Installing Tailscale (requires manual 'sudo tailscale up' on first boot)..."
36+
curl -fsSL https://tailscale.com/install.sh | sh
37+
38+
# 3. Create user account
39+
echo "==> Creating user account: ${USER_NAME}..."
40+
if ! id "${USER_NAME}" &>/dev/null; then
41+
useradd -m -s /bin/bash -G sudo,dialout,netdev "${USER_NAME}"
42+
fi
43+
44+
# Set password (use GitHub Secret if set, fallback to hashed 'sunpi' default to satisfy secret scanners)
45+
if [[ -n "${PI_PASSWORD:-}" ]]; then
46+
echo "${USER_NAME}:${PI_PASSWORD}" | chpasswd
47+
else
48+
# Default password is 'sunpi' (using pre-computed SHA-512 hash)
49+
echo "${USER_NAME}:\$6\$SArMnaIiB.dSnTwB\$2bptqoMiB4QZEChJ1NhnqXmtAyNh7nXikz5XL2Y6QxCHt/l./FhwgKY2EXWdeDPMjMe542D0RSodt6ghiPak50" | chpasswd -e
50+
fi
51+
52+
# Allow passwordless sudo for user
53+
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/${USER_NAME}"
54+
55+
# 4. Clone and compile the project
56+
echo "==> Setting up can-telem-cloud under /home/${USER_NAME}..."
57+
git clone "https://github.com/${GITHUB_REPOSITORY:-badgerloop-software/can-telem-cloud}.git" "/home/${USER_NAME}/can-telem-cloud"
58+
chown -R "${USER_NAME}:${USER_NAME}" "/home/${USER_NAME}/can-telem-cloud"
59+
60+
cd "/home/${USER_NAME}/can-telem-cloud"
61+
make -j$(nproc)
62+
chown "${USER_NAME}:${USER_NAME}" can_telem
63+
64+
# 5. Deploy systemd services
65+
echo "==> Setting up systemd services..."
66+
cp deploy/can0.service /etc/systemd/system/
67+
cp deploy/can-telem.service /etc/systemd/system/
68+
cp deploy/can-telem-gnss.service /etc/systemd/system/
69+
70+
# Adjust systemd service paths if a non-default username was specified
71+
if [[ "${USER_NAME}" != "sunpi" ]]; then
72+
echo "==> Adjusting service paths to use /home/${USER_NAME} instead of /home/sunpi..."
73+
sed -i "s|/home/sunpi|/home/${USER_NAME}|g" /etc/systemd/system/can-telem.service
74+
sed -i "s|/home/sunpi|/home/${USER_NAME}|g" /etc/systemd/system/can-telem-gnss.service
75+
fi
76+
77+
# 6. Deploy network failover rules
78+
echo "==> Setting up LTE failover rules..."
79+
cp deploy/10-managed-devices.conf /etc/NetworkManager/conf.d/
80+
cp deploy/99-ignore-lte-net.rules /etc/udev/rules.d/
81+
82+
# 7. Mask ModemManager & Enable Services
83+
systemctl mask ModemManager
84+
systemctl enable can0.service
85+
systemctl enable can-telem.service
86+
systemctl enable can-telem-gnss.service
87+
systemctl enable tailscaled.service
88+
89+
echo "==> Configuration complete!"

deploy/setup.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
# deploy/setup.sh - Run this on the Pi to install/update services
3+
set -euo pipefail
4+
5+
# Ensure script is run as root
6+
if [[ $EUID -ne 0 ]]; then
7+
echo "This script must be run as root (sudo)"
8+
exit 1
9+
fi
10+
11+
echo "Deploying system services and network configurations..."
12+
13+
# 1. Copy services
14+
cp deploy/can0.service /etc/systemd/system/
15+
cp deploy/can-telem.service /etc/systemd/system/
16+
cp deploy/can-telem-gnss.service /etc/systemd/system/
17+
18+
# 2. Copy network overrides
19+
cp deploy/10-managed-devices.conf /etc/NetworkManager/conf.d/
20+
cp deploy/99-ignore-lte-net.rules /etc/udev/rules.d/
21+
22+
# 3. Reload systemd and udev rules
23+
systemctl daemon-reload
24+
udevadm control --reload-rules
25+
udevadm trigger
26+
27+
# 4. Enable/Mask services
28+
systemctl mask ModemManager
29+
systemctl stop ModemManager || true
30+
systemctl enable can0.service
31+
systemctl enable can-telem.service
32+
systemctl enable can-telem-gnss.service
33+
34+
# 5. Restart services to apply changes
35+
systemctl restart NetworkManager
36+
systemctl restart can0.service || true
37+
systemctl restart can-telem-gnss.service || true
38+
systemctl restart can-telem.service || true
39+
40+
echo "All services deployed successfully!"

0 commit comments

Comments
 (0)