@@ -54,16 +54,15 @@ RUN git clone \
|
||||
https://github.com/raspberrypi/pico-sdk.git \
|
||||
"$PICO_SDK_PATH"
|
||||
|
||||
RUN python3 -m venv /opt/modulus-python
|
||||
RUN python3 -m venv /opt/modulus_python
|
||||
|
||||
ENV PATH=/opt/modulus-python/bin:$PATH
|
||||
ENV PATH=/opt/modulus_python/bin:$PATH
|
||||
|
||||
COPY generator/requirements.txt /tmp/vamp-build-requirements.txt
|
||||
ARG MODULUS_TOOLS=build/build_tools
|
||||
COPY ${MODULUS_TOOLS}/docker/format-requirements.txt /tmp/modulus-format-requirements.txt
|
||||
COPY ${MODULUS_TOOLS}/docker/format_requirements.txt /tmp/modulus_format_requirements.txt
|
||||
|
||||
RUN python -m pip install --no-cache-dir \
|
||||
conan==2.26.2 -r /tmp/vamp-build-requirements.txt -r /tmp/modulus-format-requirements.txt
|
||||
conan==2.26.2 -r /tmp/modulus_format_requirements.txt
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN mkdir -p /opt && touch /opt/modulus-build-image
|
||||
RUN mkdir -p /opt && touch /opt/modulus_build_image
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"runtime": {
|
||||
"headless": true,
|
||||
"assets_path": "build/build_x86_debug/generated-assets",
|
||||
"assets_path": "build/build_x86_debug/generated_assets",
|
||||
"heartbeat_path": "build/build_x86_debug/runtime/heartbeat.json"
|
||||
},
|
||||
"database": {
|
||||
"path": "build/build_x86_debug/generated-assets/catalog.db"
|
||||
"path": "build/build_x86_debug/generated_assets/catalog.db"
|
||||
},
|
||||
"control": {
|
||||
"endpoint": {
|
||||
"name": "product-vamp-control",
|
||||
"name": "product_vamp_control",
|
||||
"protocol": "http",
|
||||
"bind": "127.0.0.1:8080"
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
"runtime": {
|
||||
"headless": false,
|
||||
"exit_after_startup": false,
|
||||
"assets_path": "/usr/share/product-vamp",
|
||||
"heartbeat_path": "/run/product-vamp/heartbeat.json"
|
||||
"assets_path": "/usr/share/product_vamp",
|
||||
"heartbeat_path": "/run/product_vamp/heartbeat.json"
|
||||
},
|
||||
"database": {
|
||||
"path": "/usr/share/product-vamp/catalog.db"
|
||||
"path": "/usr/share/product_vamp/catalog.db"
|
||||
},
|
||||
"control": {
|
||||
"endpoint": {
|
||||
"name": "product-vamp-control",
|
||||
"name": "product_vamp_control",
|
||||
"protocol": "http",
|
||||
"bind": "0.0.0.0:8080",
|
||||
"timeout_ms": 5000,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"package": "product-vamp",
|
||||
"package": "product_vamp",
|
||||
"git_sha": "@VAMP_GIT_SHA@",
|
||||
"zero": {
|
||||
"device": "raspberry-pi-zero-2w",
|
||||
"display": "waveshare-zero-disp-7a",
|
||||
"docker_archive": "zero/product-vamp-zero-linux-arm64.docker.tar",
|
||||
"docker_archive": "zero/product_vamp_zero_linux_arm64.docker.tar",
|
||||
"image_tag": "@VAMP_ZERO_IMAGE_TAG@",
|
||||
"default_config": "zero/config/vamp.example.json",
|
||||
"control_endpoint": "http://192.168.42.1:8080"
|
||||
@@ -12,7 +12,7 @@
|
||||
"pico": {
|
||||
"device": "raspberry-pi-pico-2w",
|
||||
"display": "waveshare-2inch-capacitive-touch-lcd",
|
||||
"firmware": "pico/product-vamp-pico2w.uf2",
|
||||
"firmware": "pico/product_vamp_pico2w.uf2",
|
||||
"default_wifi_ssid": "@VAMP_PICO_WIFI_SSID@",
|
||||
"default_zero_host": "@VAMP_ZERO_HOST@",
|
||||
"default_zero_port": @VAMP_ZERO_PORT@
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -lt 7 ] || [ "$#" -gt 8 ]; then
|
||||
echo "usage: build_pico_firmware.sh BUILD_DIR OUTPUT_UF2 SDK_VERSION WIFI_SSID WIFI_PASSPHRASE ZERO_HOST ZERO_PORT [MODULUS_INCLUDE_DIR]" >&2
|
||||
if [ "$#" -ne 11 ]; then
|
||||
echo "usage: build_pico_firmware.sh BUILD_DIR OUTPUT_UF2 SDK_VERSION WIFI_SSID WIFI_PASSPHRASE ZERO_HOST ZERO_PORT MODULUS_INCLUDE_DIR PYTHON RUNNER MODULE_ROOT" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -15,6 +15,9 @@ wifi_passphrase=$5
|
||||
zero_host=$6
|
||||
zero_port=$7
|
||||
modulus_include_dir=${8:-}
|
||||
python=$9
|
||||
runner=${10}
|
||||
module_root=${11}
|
||||
|
||||
pico_sdk_path=${VAMP_PICO_SDK_PATH:-${PICO_SDK_PATH:-}}
|
||||
sdk_args=(
|
||||
@@ -67,10 +70,13 @@ cmake \
|
||||
-DPICO_GCC_TRIPLE=arm-none-eabi \
|
||||
"${sdk_args[@]}" \
|
||||
"${modulus_args[@]}" \
|
||||
"-DPython3_EXECUTABLE=${python}" \
|
||||
"-DMODULUS_PYTHON_RUNNER=${runner}" \
|
||||
"-DMODULUS_PYTHON_ROOT=${module_root}" \
|
||||
"-DVAMP_PICO_WIFI_SSID=${wifi_ssid}" \
|
||||
"-DVAMP_PICO_WIFI_PASSPHRASE=${wifi_passphrase}" \
|
||||
"-DVAMP_ZERO_HOST=${zero_host}" \
|
||||
"-DVAMP_ZERO_PORT=${zero_port}"
|
||||
|
||||
cmake --build "$build_dir" --target product-vamp-pico2w --parallel "${VAMP_BUILD_JOBS:-2}"
|
||||
cmake -E copy "$build_dir/product-vamp-pico2w.uf2" "$output_uf2"
|
||||
cmake --build "$build_dir" --target product_vamp_pico2w --parallel "${VAMP_BUILD_JOBS:-2}"
|
||||
cmake -E copy "$build_dir/product_vamp_pico2w.uf2" "$output_uf2"
|
||||
|
||||
@@ -23,5 +23,5 @@ else
|
||||
docker build --platform linux/arm64 --tag "$image_tag" "$context_dir"
|
||||
fi
|
||||
|
||||
docker run --rm --platform linux/arm64 "$image_tag" /usr/bin/vamp-zero --build-info
|
||||
docker run --rm --platform linux/arm64 "$image_tag" /usr/bin/vamp_zero --build-info
|
||||
docker save "$image_tag" --output "$output_tar"
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
DEFAULT_REGISTRY_URL = "https://git.thyme-labs.com/api/packages/khodo/generic"
|
||||
REGISTRY_SLOT = "latest"
|
||||
PAGE_LIMIT = 50
|
||||
|
||||
def read_config(path):
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise SystemExit(f"failed to read {path}: {error}") from error
|
||||
package = data.get("package", {})
|
||||
if not isinstance(package, dict):
|
||||
raise SystemExit(f"{path}: package must be an object")
|
||||
name = package.get("name")
|
||||
if not isinstance(name, str) or not name:
|
||||
raise SystemExit(f"{path}: package.name must be a non-empty string")
|
||||
artifact = package.get("artifact", {})
|
||||
if not isinstance(artifact, dict):
|
||||
artifact = {}
|
||||
registry_url = artifact.get("registry_url")
|
||||
if not isinstance(registry_url, str) or not registry_url:
|
||||
registry_url = DEFAULT_REGISTRY_URL
|
||||
targets = artifact.get("targets")
|
||||
if not isinstance(targets, list) or not targets or any(not isinstance(target, str) or target not in data.get("tooling", {}).get("targets", {}) for target in targets):
|
||||
raise SystemExit(f"{path}: package.artifact.targets must name configured build targets")
|
||||
return name, registry_url, targets
|
||||
|
||||
def parse_registry_url(registry_url):
|
||||
parsed = urllib.parse.urlparse(registry_url.rstrip("/"))
|
||||
parts = [urllib.parse.unquote(part) for part in parsed.path.split("/") if part]
|
||||
if len(parts) < 4 or parts[-4] != "api" or parts[-3] != "packages" or parts[-1] != "generic":
|
||||
raise SystemExit("package artifact registry_url must end with /api/packages/{owner}/generic")
|
||||
owner = parts[-2]
|
||||
prefix = "/" + "/".join(urllib.parse.quote(part, safe="") for part in parts[:-4]) if parts[:-4] else ""
|
||||
return parsed, prefix, owner
|
||||
|
||||
def build_url(parsed, path, query=""):
|
||||
return urllib.parse.urlunparse((
|
||||
parsed.scheme,
|
||||
parsed.netloc,
|
||||
path,
|
||||
"",
|
||||
query,
|
||||
"",
|
||||
))
|
||||
|
||||
def quote_path(*parts):
|
||||
return "/".join(urllib.parse.quote(part, safe="") for part in parts)
|
||||
|
||||
def version_list_url(parsed, prefix, owner, package_name, page):
|
||||
path = f"{prefix}/api/v1/packages/{quote_path(owner, 'generic', package_name)}"
|
||||
query = urllib.parse.urlencode({
|
||||
"page": page,
|
||||
"limit": PAGE_LIMIT,
|
||||
})
|
||||
return build_url(parsed, path, query)
|
||||
|
||||
def version_delete_url(parsed, prefix, owner, package_name, version):
|
||||
path = f"{prefix}/api/packages/{quote_path(owner, 'generic', package_name, version)}"
|
||||
return build_url(parsed, path)
|
||||
|
||||
def file_upload_url(registry_url, package_name, path):
|
||||
return "/".join((
|
||||
registry_url.rstrip("/"),
|
||||
urllib.parse.quote(package_name, safe=""),
|
||||
REGISTRY_SLOT,
|
||||
urllib.parse.quote(path.name, safe=""),
|
||||
))
|
||||
|
||||
def authorization(user, token):
|
||||
encoded = base64.b64encode(f"{user}:{token}".encode("utf-8")).decode("ascii")
|
||||
return f"Basic {encoded}"
|
||||
|
||||
def request(method, url, auth, data=None, headers=None, missing_ok=False):
|
||||
request_headers = {
|
||||
"Authorization": auth,
|
||||
**(headers or {}),
|
||||
}
|
||||
request = urllib.request.Request(url, data=data, method=method, headers=request_headers)
|
||||
try:
|
||||
return urllib.request.urlopen(request)
|
||||
except urllib.error.HTTPError as error:
|
||||
if missing_ok and error.code == 404:
|
||||
return None
|
||||
body = error.read().decode("utf-8", errors="replace")
|
||||
message = f"{method} {url} failed with HTTP {error.code}"
|
||||
if body:
|
||||
message += f": {body}"
|
||||
raise SystemExit(message) from error
|
||||
except urllib.error.URLError as error:
|
||||
raise SystemExit(f"{method} {url} failed: {error.reason}") from error
|
||||
|
||||
def list_versions(parsed, prefix, owner, package_name, auth):
|
||||
versions = []
|
||||
page = 1
|
||||
while True:
|
||||
response = request("GET", version_list_url(parsed, prefix, owner, package_name, page), auth, missing_ok=True)
|
||||
if response is None:
|
||||
return []
|
||||
with response:
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("Gitea package version list response was not an array")
|
||||
if not data:
|
||||
break
|
||||
for item in data:
|
||||
if isinstance(item, dict) and isinstance(item.get("version"), str):
|
||||
versions.append(item["version"])
|
||||
if len(data) < PAGE_LIMIT:
|
||||
break
|
||||
page += 1
|
||||
return sorted(set(versions))
|
||||
|
||||
def delete_version(parsed, prefix, owner, package_name, version, auth):
|
||||
url = version_delete_url(parsed, prefix, owner, package_name, version)
|
||||
response = request("DELETE", url, auth, missing_ok=True)
|
||||
if response is None:
|
||||
print(f"Package version already absent: {package_name}/{version}")
|
||||
return
|
||||
with response:
|
||||
if response.status != 204:
|
||||
raise SystemExit(f"DELETE {url} returned HTTP {response.status}")
|
||||
print(f"Deleted package version: {package_name}/{version}")
|
||||
|
||||
def discover_artifacts(dist_dir, package_name, targets):
|
||||
artifacts = []
|
||||
for target in sorted(set(targets)):
|
||||
archive = dist_dir / f"{package_name}-{target}.tar.gz"
|
||||
artifacts.extend((archive, archive.with_name(archive.name + ".sha256")))
|
||||
missing = [path for path in artifacts if not path.is_file()]
|
||||
if missing:
|
||||
found = sorted(path.name for path in dist_dir.glob(f"{package_name}-*.tar.gz*") if path.is_file())
|
||||
raise SystemExit("missing artifact files: " + ", ".join(str(path) for path in missing)
|
||||
+ "\nFound: " + (", ".join(found) or "none")
|
||||
+ "\nPublish the current Modulus tooling, run configure --update, and recreate the package so the builder and publisher use the same filenames.")
|
||||
return artifacts
|
||||
|
||||
def upload_file(registry_url, package_name, path, auth):
|
||||
url = file_upload_url(registry_url, package_name, path)
|
||||
headers = {
|
||||
"Content-Length": str(path.stat().st_size),
|
||||
"Content-Type": "application/octet-stream",
|
||||
}
|
||||
with path.open("rb") as input_file:
|
||||
response = request("PUT", url, auth, data=input_file, headers=headers)
|
||||
with response:
|
||||
if response.status != 201:
|
||||
raise SystemExit(f"PUT {url} returned HTTP {response.status}")
|
||||
print(f"Uploaded package file: {package_name}/{path.name}")
|
||||
|
||||
def parse_args(argv):
|
||||
parser = argparse.ArgumentParser(description="Replace Product Vamp's rolling Gitea generic package.")
|
||||
parser.add_argument("--config", type=pathlib.Path, default=pathlib.Path("modulus.json"))
|
||||
parser.add_argument("--dist-dir", type=pathlib.Path, default=pathlib.Path("build/packages"))
|
||||
parser.add_argument("--user", default=None)
|
||||
parser.add_argument("--token", default=None)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
def main(argv):
|
||||
args = parse_args(argv)
|
||||
package_name, registry_url, targets = read_config(args.config)
|
||||
user = args.user or os_environ("PACKAGE_REGISTRY_USER")
|
||||
token = args.token or os_environ("PACKAGE_REGISTRY_TOKEN")
|
||||
auth = authorization(user, token)
|
||||
parsed, prefix, owner = parse_registry_url(registry_url)
|
||||
artifacts = discover_artifacts(args.dist_dir, package_name, targets)
|
||||
existing_versions = list_versions(parsed, prefix, owner, package_name, auth)
|
||||
# Gitea rejects duplicate file names within a generic package version, so
|
||||
# replacing the rolling release requires deleting the old version first.
|
||||
if REGISTRY_SLOT in existing_versions:
|
||||
delete_version(parsed, prefix, owner, package_name, REGISTRY_SLOT, auth)
|
||||
for artifact in artifacts:
|
||||
upload_file(registry_url, package_name, artifact, auth)
|
||||
for version in existing_versions:
|
||||
if version != REGISTRY_SLOT:
|
||||
delete_version(parsed, prefix, owner, package_name, version, auth)
|
||||
return 0
|
||||
|
||||
def os_environ(name):
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
raise SystemExit(f"{name} is required")
|
||||
return value
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM debian:trixie-slim
|
||||
|
||||
LABEL org.khodo.product=product-vamp-zero
|
||||
LABEL org.khodo.product=product_vamp_zero
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -26,4 +26,4 @@ RUN apt-get update \
|
||||
COPY usr/ /usr/
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/usr/bin/vamp-zero", "--config", "/etc/product-vamp/vamp.json"]
|
||||
CMD ["/usr/bin/vamp_zero", "--config", "/etc/product_vamp/vamp.json"]
|
||||
|
||||
@@ -3,24 +3,31 @@ set -euo pipefail
|
||||
|
||||
bundle_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
zero_dir="$bundle_dir/zero"
|
||||
image_tar="$zero_dir/product-vamp-zero-linux-arm64.docker.tar"
|
||||
config_dir=/etc/product-vamp
|
||||
state_dir=/var/lib/product-vamp
|
||||
run_dir=/run/product-vamp
|
||||
share_dir=/usr/share/product-vamp
|
||||
image_tar="$zero_dir/product_vamp_zero_linux_arm64.docker.tar"
|
||||
config_dir=/etc/product_vamp
|
||||
state_dir=/var/lib/product_vamp
|
||||
run_dir=/run/product_vamp
|
||||
share_dir=/usr/share/product_vamp
|
||||
image_tag=
|
||||
connection=product-vamp-control
|
||||
connection=product_vamp_control
|
||||
replacement_started=false
|
||||
image_loaded=false
|
||||
previous_image=
|
||||
stage_dir=
|
||||
legacy_targets=(
|
||||
/etc/product-vamp /var/lib/product-vamp /run/product-vamp /usr/share/product-vamp
|
||||
/usr/local/bin/product-vamp-start-ap /usr/local/bin/product-vamp-start-zero
|
||||
/etc/systemd/system/product-vamp-ap.service /etc/systemd/system/product-vamp-zero.service
|
||||
/etc/systemd/system/product-vamp-ap.service.d /etc/systemd/system/product-vamp-zero.service.d
|
||||
/run/systemd/system/product-vamp-ap.service.d /run/systemd/system/product-vamp-zero.service.d
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
if [ "$status" -ne 0 ] && [ "$replacement_started" = true ]; then
|
||||
systemctl stop product-vamp-zero.service product-vamp-ap.service >/dev/null 2>&1 || true
|
||||
docker rm -f product-vamp-zero >/dev/null 2>&1 || true
|
||||
systemctl stop product_vamp_zero.service product_vamp_ap.service >/dev/null 2>&1 || true
|
||||
docker rm -f product_vamp_zero >/dev/null 2>&1 || true
|
||||
nmcli connection down "$connection" >/dev/null 2>&1 || true
|
||||
echo "Installation failed; Product Vamp services are stopped. Rerun the installer." >&2
|
||||
elif [ "$status" -ne 0 ] && [ "$image_loaded" = true ] && [ -n "$previous_image" ]; then
|
||||
@@ -61,7 +68,7 @@ required_packages=(
|
||||
)
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "install-zero.sh must be run as root" >&2
|
||||
echo "install_zero.sh must be run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -83,12 +90,12 @@ else
|
||||
echo "Installing missing OS packages: ${missing_packages[*]}"
|
||||
if ! apt-get update -o APT::Update::Error-Mode=any; then
|
||||
echo "Failed to refresh apt package indexes." >&2
|
||||
echo "Connect the Zero to upstream internet, then rerun sudo zero/install-zero.sh." >&2
|
||||
echo "Connect the Zero to upstream internet, then rerun sudo zero/install_zero.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! apt-get install -y --no-install-recommends "${missing_packages[@]}"; then
|
||||
echo "Failed to install required OS packages: ${missing_packages[*]}" >&2
|
||||
echo "Connect the Zero to upstream internet, then rerun sudo zero/install-zero.sh." >&2
|
||||
echo "Connect the Zero to upstream internet, then rerun sudo zero/install_zero.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -119,24 +126,25 @@ mapfile -t image_fields <<< "$image_details"
|
||||
image_tag=${image_fields[0]}
|
||||
expected_image=${image_fields[1]}
|
||||
|
||||
for target in "$config_dir" "$state_dir" "$run_dir" "$share_dir" \
|
||||
/usr/local/bin/product-vamp-start-ap /usr/local/bin/product-vamp-start-zero \
|
||||
/etc/systemd/system/product-vamp-ap.service.d /etc/systemd/system/product-vamp-zero.service.d \
|
||||
/run/systemd/system/product-vamp-ap.service.d /run/systemd/system/product-vamp-zero.service.d \
|
||||
/etc/systemd/system/product-vamp-ap.service /etc/systemd/system/product-vamp-zero.service; do
|
||||
for target in "${legacy_targets[@]}" "$config_dir" "$state_dir" "$run_dir" "$share_dir" \
|
||||
/usr/local/bin/product_vamp_start_ap /usr/local/bin/product_vamp_start_zero \
|
||||
/etc/systemd/system/product_vamp_ap.service.d /etc/systemd/system/product_vamp_zero.service.d \
|
||||
/run/systemd/system/product_vamp_ap.service.d /run/systemd/system/product_vamp_zero.service.d \
|
||||
/etc/systemd/system/product_vamp_ap.service /etc/systemd/system/product_vamp_zero.service; do
|
||||
validate_target "$target"
|
||||
done
|
||||
|
||||
stage_dir=$(mktemp -d /tmp/product-vamp-install-XXXXXX)
|
||||
stage_dir=$(mktemp -d /tmp/product_vamp_install_XXXXXX)
|
||||
install -m 0644 "$zero_dir/config/vamp.example.json" "$stage_dir/vamp.json"
|
||||
install -m 0755 "$zero_dir/start-ap.sh" "$stage_dir/start-ap.sh"
|
||||
install -m 0755 "$zero_dir/start-zero.sh" "$stage_dir/start-zero.sh"
|
||||
install -m 0644 "$zero_dir/systemd/product-vamp-ap.service" "$stage_dir/product-vamp-ap.service"
|
||||
install -m 0644 "$zero_dir/systemd/product-vamp-zero.service" "$stage_dir/product-vamp-zero.service"
|
||||
bash -n "$stage_dir/start-ap.sh"
|
||||
bash -n "$stage_dir/start-zero.sh"
|
||||
install -m 0755 "$zero_dir/start_ap.sh" "$stage_dir/start_ap.sh"
|
||||
install -m 0755 "$zero_dir/start_zero.sh" "$stage_dir/start_zero.sh"
|
||||
install -m 0644 "$zero_dir/systemd/product_vamp_ap.service" "$stage_dir/product_vamp_ap.service"
|
||||
install -m 0644 "$zero_dir/systemd/product_vamp_zero.service" "$stage_dir/product_vamp_zero.service"
|
||||
bash -n "$stage_dir/start_ap.sh"
|
||||
bash -n "$stage_dir/start_zero.sh"
|
||||
previous_image=$(docker image inspect --format '{{.Id}}' "$image_tag" 2>/dev/null || true)
|
||||
previous_images=$(docker image ls --quiet --no-trunc --filter label=org.khodo.product=product-vamp-zero)
|
||||
previous_images=$(docker image ls --quiet --no-trunc --filter label=org.khodo.product=product_vamp_zero)
|
||||
legacy_images=$(docker image ls --quiet --no-trunc --filter label=org.khodo.product=product-vamp-zero)
|
||||
image_loaded=true
|
||||
docker load -i "$image_tar"
|
||||
new_image=$(docker image inspect --format '{{.Id}}' "$image_tag")
|
||||
@@ -146,45 +154,50 @@ if [ "$new_image" != "$expected_image" ]; then
|
||||
fi
|
||||
# Probe the actual image and staged configuration without touching host state or hardware.
|
||||
docker run --rm --network none --mount "type=bind,src=$stage_dir,dst=/install,readonly" \
|
||||
"$new_image" /usr/bin/vamp-zero --config /install/vamp.json --once
|
||||
"$new_image" /usr/bin/vamp_zero --config /install/vamp.json --once
|
||||
|
||||
echo "Resetting all Product Vamp data and settings, including control Wi-Fi credentials."
|
||||
echo "A connection through the control Wi-Fi may disconnect."
|
||||
replacement_started=true
|
||||
for service in product-vamp-zero.service product-vamp-ap.service; do
|
||||
for service in product_vamp_zero.service product_vamp_ap.service product-vamp-zero.service product-vamp-ap.service; do
|
||||
if systemctl cat "$service" >/dev/null 2>&1; then
|
||||
systemctl stop "$service"
|
||||
systemctl disable "$service"
|
||||
fi
|
||||
done
|
||||
if docker container inspect product-vamp-zero >/dev/null 2>&1; then
|
||||
docker rm -f product-vamp-zero
|
||||
fi
|
||||
for target in "$config_dir" "$state_dir" "$run_dir" "$share_dir" \
|
||||
/etc/systemd/system/product-vamp-ap.service.d /etc/systemd/system/product-vamp-zero.service.d \
|
||||
/run/systemd/system/product-vamp-ap.service.d /run/systemd/system/product-vamp-zero.service.d; do
|
||||
for container in product_vamp_zero product-vamp-zero; do
|
||||
if docker container inspect "$container" >/dev/null 2>&1; then
|
||||
docker rm -f "$container"
|
||||
fi
|
||||
done
|
||||
for target in "${legacy_targets[@]}" "$config_dir" "$state_dir" "$run_dir" "$share_dir" \
|
||||
/etc/systemd/system/product_vamp_ap.service.d /etc/systemd/system/product_vamp_zero.service.d \
|
||||
/run/systemd/system/product_vamp_ap.service.d /run/systemd/system/product_vamp_zero.service.d; do
|
||||
validate_target "$target"
|
||||
rm -rf --one-file-system -- "$target"
|
||||
done
|
||||
install -d -m 0755 "$config_dir" "$run_dir"
|
||||
install -m 0644 "$stage_dir/vamp.json" "$config_dir/vamp.json"
|
||||
printf '%s\n' "$new_image" > "$config_dir/image-id"
|
||||
install -m 0755 "$stage_dir/start-ap.sh" /usr/local/bin/product-vamp-start-ap
|
||||
install -m 0755 "$stage_dir/start-zero.sh" /usr/local/bin/product-vamp-start-zero
|
||||
install -m 0644 "$stage_dir/product-vamp-ap.service" /etc/systemd/system/product-vamp-ap.service
|
||||
install -m 0644 "$stage_dir/product-vamp-zero.service" /etc/systemd/system/product-vamp-zero.service
|
||||
printf '%s\n' "$new_image" > "$config_dir/image_id"
|
||||
install -m 0755 "$stage_dir/start_ap.sh" /usr/local/bin/product_vamp_start_ap
|
||||
install -m 0755 "$stage_dir/start_zero.sh" /usr/local/bin/product_vamp_start_zero
|
||||
install -m 0644 "$stage_dir/product_vamp_ap.service" /etc/systemd/system/product_vamp_ap.service
|
||||
install -m 0644 "$stage_dir/product_vamp_zero.service" /etc/systemd/system/product_vamp_zero.service
|
||||
|
||||
if nmcli -t -f NAME connection show | grep -Fxq "$connection"; then
|
||||
nmcli connection delete "$connection"
|
||||
fi
|
||||
for profile in "$connection" product-vamp-control; do
|
||||
if nmcli -t -f NAME connection show | grep -Fxq "$profile"; then
|
||||
nmcli connection delete "$profile"
|
||||
fi
|
||||
done
|
||||
systemctl daemon-reload
|
||||
systemctl disable --now getty@tty1.service >/dev/null 2>&1 || true
|
||||
systemctl enable --now getty@tty2.service >/dev/null 2>&1 || true
|
||||
systemctl enable --now product-vamp-ap.service
|
||||
systemctl enable product-vamp-zero.service
|
||||
systemctl restart product-vamp-zero.service
|
||||
systemctl enable --now product_vamp_ap.service
|
||||
systemctl enable product_vamp_zero.service
|
||||
systemctl restart product_vamp_zero.service
|
||||
healthy=false
|
||||
for attempt in $(seq 1 60); do
|
||||
if docker exec product-vamp-zero /usr/bin/vamp-zero --config /etc/product-vamp/vamp.json --health-check; then
|
||||
if docker exec product_vamp_zero /usr/bin/vamp_zero --config /etc/product_vamp/vamp.json --health-check; then
|
||||
healthy=true
|
||||
break
|
||||
fi
|
||||
@@ -194,7 +207,7 @@ if [ "$healthy" != true ]; then
|
||||
echo "Product Vamp did not become healthy after installation." >&2
|
||||
exit 1
|
||||
fi
|
||||
for image in $(printf '%s\n' "$previous_image" "$previous_images" | sort -u); do
|
||||
for image in $(printf '%s\n' "$previous_image" "$previous_images" "$legacy_images" | sort -u); do
|
||||
if [ "$image" != "$new_image" ]; then
|
||||
if [ -z "$(docker ps -aq --filter "ancestor=$image")" ]; then
|
||||
docker image rm "$image" || echo "Warning: could not remove old Product Vamp image $image" >&2
|
||||
@@ -6,7 +6,7 @@ Requires=NetworkManager.service
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/bin/product-vamp-start-ap
|
||||
ExecStart=/usr/local/bin/product_vamp_start_ap
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,7 +1,7 @@
|
||||
[Unit]
|
||||
Description=Product Vamp Zero display and control service
|
||||
After=docker.service product-vamp-ap.service getty@tty1.service
|
||||
Requires=docker.service product-vamp-ap.service
|
||||
After=docker.service product_vamp_ap.service getty@tty1.service
|
||||
Requires=docker.service product_vamp_ap.service
|
||||
Conflicts=getty@tty1.service
|
||||
|
||||
[Service]
|
||||
@@ -14,9 +14,9 @@ TTYVTDisallocate=yes
|
||||
StandardInput=tty-force
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
ExecStartPre=/usr/bin/install -d -m 0755 /run/product-vamp
|
||||
ExecStart=/usr/local/bin/product-vamp-start-zero
|
||||
ExecStop=/bin/sh -c '/usr/bin/docker stop product-vamp-zero >/dev/null 2>&1 || true'
|
||||
ExecStartPre=/usr/bin/install -d -m 0755 /run/product_vamp
|
||||
ExecStart=/usr/local/bin/product_vamp_start_zero
|
||||
ExecStop=/bin/sh -c '/usr/bin/docker stop product_vamp_zero >/dev/null 2>&1 || true'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
config=${PRODUCT_VAMP_CONFIG:-/etc/product-vamp/vamp.json}
|
||||
config=${PRODUCT_VAMP_CONFIG:-/etc/product_vamp/vamp.json}
|
||||
ssid=${PRODUCT_VAMP_AP_SSID:-Vamp Control}
|
||||
passphrase=${PRODUCT_VAMP_AP_PASSPHRASE:-vamp-control}
|
||||
interface=${PRODUCT_VAMP_AP_INTERFACE:-wlan0}
|
||||
cidr=${PRODUCT_VAMP_ZERO_CIDR:-192.168.42.1/24}
|
||||
connection=${PRODUCT_VAMP_AP_CONNECTION:-product-vamp-control}
|
||||
connection=${PRODUCT_VAMP_AP_CONNECTION:-product_vamp_control}
|
||||
|
||||
configure_connection() {
|
||||
nmcli connection modify "$connection" \
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
container_name=${PRODUCT_VAMP_ZERO_CONTAINER:-product-vamp-zero}
|
||||
config_dir=${PRODUCT_VAMP_CONFIG_DIR:-/etc/product-vamp}
|
||||
container_name=${PRODUCT_VAMP_ZERO_CONTAINER:-product_vamp_zero}
|
||||
config_dir=${PRODUCT_VAMP_CONFIG_DIR:-/etc/product_vamp}
|
||||
image_tag=${PRODUCT_VAMP_ZERO_IMAGE:-}
|
||||
if [ -z "$image_tag" ]; then
|
||||
read -r image_tag < "$config_dir/image-id"
|
||||
read -r image_tag < "$config_dir/image_id"
|
||||
fi
|
||||
run_dir=${PRODUCT_VAMP_RUN_DIR:-/run/product-vamp}
|
||||
run_dir=${PRODUCT_VAMP_RUN_DIR:-/run/product_vamp}
|
||||
display_tty=${PRODUCT_VAMP_DISPLAY_TTY:-/dev/tty1}
|
||||
|
||||
install -d -m 0755 "$run_dir"
|
||||
@@ -37,7 +37,7 @@ echo "Starting Product Vamp Zero container '$container_name' from '$image_tag' o
|
||||
|
||||
exec docker run --rm --name "$container_name" --network host --privileged \
|
||||
--env PRODUCT_VAMP_DISPLAY_TTY="$display_tty" \
|
||||
-v "$config_dir:/etc/product-vamp:ro" \
|
||||
-v "$run_dir:/run/product-vamp" \
|
||||
-v "$config_dir:/etc/product_vamp:ro" \
|
||||
-v "$run_dir:/run/product_vamp" \
|
||||
"$image_tag" \
|
||||
sh -lc 'display_tty=${PRODUCT_VAMP_DISPLAY_TTY:-/dev/tty1}; exec setsid --wait --ctty /usr/bin/vamp-zero --config /etc/product-vamp/vamp.json < "$display_tty"'
|
||||
sh -lc 'display_tty=${PRODUCT_VAMP_DISPLAY_TTY:-/dev/tty1}; exec setsid --wait --ctty /usr/bin/vamp_zero --config /etc/product_vamp/vamp.json < "$display_tty"'
|
||||
Reference in New Issue
Block a user