Initial public khodo snapshot
ci / checks (push) Successful in 38m58s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit 4f9a269f85
99 changed files with 59585 additions and 0 deletions
Binary file not shown.
+94
View File
@@ -0,0 +1,94 @@
Copyright © 2018, Steinberg Media Technologies GmbH (http://www.steinberg.net/),
with Reserved Font Name "Petaluma".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
{
"modulus": {
"logging": {
"level": "info"
},
"errors": {
"include_category": true,
"include_code": true,
"include_message": true,
"include_location": true
}
},
"runtime": {
"hardware": "real",
"headless": false,
"exit_after_startup": false,
"assets_path": "/usr/share/product-vamp",
"heartbeat_path": "/run/product-vamp/heartbeat.json"
},
"database": {
"path": "/var/lib/product-vamp/vamp.db",
"migrations_path": "/usr/share/product-vamp/migrations"
},
"control": {
"endpoint": {
"name": "product-vamp-control",
"protocol": "http",
"bind": "0.0.0.0:8080",
"timeout_ms": 5000,
"http": {
"max_body_size": 65536
}
},
"ap_ssid": "Vamp Control",
"ap_passphrase": "vamp-control",
"ap_interface": "wlan0",
"zero_ip": "192.168.42.1",
"zero_cidr": "192.168.42.1/24"
},
"display": {
"title": "Product Vamp",
"display": {
"width": 1024,
"height": 600,
"target_fps": 60,
"idle_fps": 15,
"fullscreen": true
},
"touch": {
"max_points": 5
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"package": "product-vamp",
"version": "@PROJECT_VERSION@",
"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",
"image_tag": "@VAMP_ZERO_IMAGE_TAG@",
"default_config": "zero/config/vamp.example.json",
"control_endpoint": "http://192.168.42.1:8080"
},
"pico": {
"device": "raspberry-pi-pico-2w",
"display": "waveshare-2inch-capacitive-touch-lcd",
"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@
}
}
+18
View File
@@ -0,0 +1,18 @@
CREATE TABLE songs (
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 120),
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
composer TEXT NOT NULL DEFAULT '',
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms)
) STRICT;
-- vamp-statement
CREATE TABLE page_state (
id INTEGER PRIMARY KEY CHECK(id = 1),
song_id TEXT NOT NULL CHECK(length(trim(song_id)) BETWEEN 1 AND 120),
page_index INTEGER NOT NULL CHECK(page_index >= 0),
next_count INTEGER NOT NULL DEFAULT 0 CHECK(next_count >= 0),
previous_count INTEGER NOT NULL DEFAULT 0 CHECK(previous_count >= 0),
last_command TEXT NOT NULL DEFAULT '',
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= 0)
) STRICT;
@@ -0,0 +1,5 @@
ALTER TABLE songs ADD COLUMN part TEXT NOT NULL DEFAULT '';
-- vamp-statement
ALTER TABLE songs ADD COLUMN source_path TEXT NOT NULL DEFAULT '';
-- vamp-statement
CREATE INDEX idx_songs_title_part ON songs(title, part);
@@ -0,0 +1,14 @@
ALTER TABLE songs ADD COLUMN key_signature TEXT NOT NULL DEFAULT '';
-- vamp-statement
CREATE TABLE practice_exercises (
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 120),
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
composer TEXT NOT NULL DEFAULT '',
key_signature TEXT NOT NULL DEFAULT 'C',
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
source_path TEXT NOT NULL DEFAULT '',
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms)
) STRICT;
-- vamp-statement
CREATE INDEX idx_practice_exercises_title ON practice_exercises(title);
@@ -0,0 +1,9 @@
ALTER TABLE practice_exercises ADD COLUMN practice_type_id TEXT NOT NULL DEFAULT 'general';
-- vamp-statement
ALTER TABLE practice_exercises ADD COLUMN practice_type_title TEXT NOT NULL DEFAULT 'General Practice';
-- vamp-statement
ALTER TABLE practice_exercises ADD COLUMN lesson_group_id TEXT NOT NULL DEFAULT 'general';
-- vamp-statement
ALTER TABLE practice_exercises ADD COLUMN lesson_group_title TEXT NOT NULL DEFAULT 'General';
-- vamp-statement
CREATE INDEX idx_practice_exercises_hierarchy ON practice_exercises(practice_type_id, lesson_group_id, source_path, title);
+20
View File
@@ -0,0 +1,20 @@
# Product Vamp Pico 2 W Wiring
The Pico firmware targets the Waveshare 2inch Capacitive Touch LCD module using the vendor default Pico pin map.
| LCD Pin | Pico 2 W Pin |
| --- | --- |
| 3V3 | 3V3 |
| GND | GND |
| MISO | GP12 |
| MOSI | GP11 |
| SCLK | GP10 |
| SD_CS | NC |
| LCD_CS | GP9 |
| LCD_DC | GP14 |
| LCD_RST | GP13 |
| LCD_BL | GP15 |
| TP_SDA | GP6 |
| TP_SCL | GP7 |
| TP_INT | GP8 |
| TP_RST | GP5 |
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 7 ]; then
echo "usage: build_pico_firmware.sh BUILD_DIR OUTPUT_UF2 SDK_VERSION WIFI_SSID WIFI_PASSPHRASE ZERO_HOST ZERO_PORT" >&2
exit 2
fi
source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
build_dir=$1
output_uf2=$2
sdk_version=$3
wifi_ssid=$4
wifi_passphrase=$5
zero_host=$6
zero_port=$7
pico_sdk_path=${VAMP_PICO_SDK_PATH:-${PICO_SDK_PATH:-}}
sdk_args=(
-DPICO_SDK_FETCH_FROM_GIT=ON
"-DPICO_SDK_FETCH_FROM_GIT_TAG=${sdk_version}"
)
if [ -n "$pico_sdk_path" ]; then
pico_sdk_path=$(cd "$pico_sdk_path" && pwd)
sdk_args=(
"-DPICO_SDK_PATH=${pico_sdk_path}"
-DPICO_SDK_FETCH_FROM_GIT=OFF
)
fi
mkdir -p "$(dirname "$output_uf2")"
unset CC CXX CPP AS AR LD RANLIB STRIP CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
for tool in cmake ninja; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "missing required tool: $tool" >&2
exit 1
fi
done
if [ -z "${PICO_TOOLCHAIN_PATH:-}" ]; then
for tool in arm-none-eabi-gcc arm-none-eabi-g++; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "missing required tool: $tool, or set PICO_TOOLCHAIN_PATH" >&2
exit 1
fi
done
fi
cmake -E rm -f "$build_dir/CMakeCache.txt"
cmake -E rm -rf "$build_dir/CMakeFiles"
cmake \
-S "$source_dir" \
-B "$build_dir" \
-G Ninja \
-DVAMP_PICO_FIRMWARE_PROJECT=ON \
-DPICO_BOARD=pico2_w \
-DPICO_GCC_TRIPLE=arm-none-eabi \
"${sdk_args[@]}" \
"-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"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 3 ]; then
echo "usage: build_zero_image.sh CONTEXT_DIR OUTPUT_TAR IMAGE_TAG" >&2
exit 2
fi
context_dir=$1
output_tar=$2
image_tag=$3
mkdir -p "$(dirname "$output_tar")"
rm -f "$output_tar"
if docker buildx version >/dev/null 2>&1; then
docker buildx build \
--platform linux/arm64 \
--tag "$image_tag" \
--load \
"$context_dir"
else
docker build --platform linux/arm64 --tag "$image_tag" "$context_dir"
fi
docker run --rm --platform linux/arm64 "$image_tag" /usr/bin/vamp-zero --version
docker save "$image_tag" --output "$output_tar"
+218
View File
@@ -0,0 +1,218 @@
#!/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"
DEFAULT_VERSION = "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
return name, registry_url
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, version, path):
return "/".join((
registry_url.rstrip("/"),
urllib.parse.quote(package_name, safe=""),
urllib.parse.quote(version, safe=""),
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, version):
archives = sorted(dist_dir.glob(f"{package_name}-{version}-*.tar.gz"))
artifacts = []
for archive in archives:
artifacts.append(archive)
checksum = archive.with_name(archive.name + ".sha256")
if checksum.is_file():
artifacts.append(checksum)
if not artifacts:
raise SystemExit(f"no artifacts found matching {dist_dir}/{package_name}-{version}-*.tar.gz")
missing = [path for path in artifacts if not path.is_file()]
if missing:
raise SystemExit("missing artifact files: " + ", ".join(str(path) for path in missing))
return artifacts
def upload_file(registry_url, package_name, version, path, auth):
url = file_upload_url(registry_url, package_name, version, 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}/{version}/{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("--version", default=DEFAULT_VERSION)
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 = 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, args.version)
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 args.version in existing_versions:
delete_version(parsed, prefix, owner, package_name, args.version, auth)
for artifact in artifacts:
upload_file(registry_url, package_name, args.version, artifact, auth)
for version in existing_versions:
if version != args.version:
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:]))
+26
View File
@@ -0,0 +1,26 @@
FROM debian:trixie-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
libdrm2 \
libegl1 \
libgbm1 \
libgl1 \
libgles2 \
libglvnd0 \
libstdc++6 \
libudev1 \
libx11-6 \
libxcb1 \
sqlite3 \
tini \
util-linux \
&& rm -rf /var/lib/apt/lists/*
COPY usr/ /usr/
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/bin/vamp-zero", "--config", "/etc/product-vamp/vamp.json"]
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
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
required_packages=(
docker.io
docker-cli
network-manager
sqlite3
ca-certificates
python3
)
if [ "$(id -u)" -ne 0 ]; then
echo "install-zero.sh must be run as root" >&2
exit 1
fi
if [ ! -f "$image_tar" ]; then
echo "missing Docker archive: $image_tar" >&2
exit 1
fi
missing_packages=()
for package in "${required_packages[@]}"; do
if ! dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -qx 'install ok installed'; then
missing_packages+=("$package")
fi
done
if [ "${#missing_packages[@]}" -eq 0 ]; then
echo "Required OS packages already installed; skipping apt."
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
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
exit 1
fi
fi
systemctl enable --now docker NetworkManager
install -d -m 0755 "$config_dir" "$state_dir" "$run_dir" "$share_dir"
cp -R "$zero_dir/share/product-vamp/." "$share_dir/"
if [ ! -f "$config_dir/vamp.json" ]; then
cp "$zero_dir/config/vamp.example.json" "$config_dir/vamp.json"
fi
docker load -i "$image_tar"
install -m 0755 "$zero_dir/start-ap.sh" /usr/local/bin/product-vamp-start-ap
install -m 0755 "$zero_dir/start-zero.sh" /usr/local/bin/product-vamp-start-zero
install -m 0644 "$zero_dir/systemd/product-vamp-ap.service" /etc/systemd/system/product-vamp-ap.service
install -m 0644 "$zero_dir/systemd/product-vamp-zero.service" /etc/systemd/system/product-vamp-zero.service
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
echo "Product Vamp Zero installed. Control AP defaults to SSID 'Vamp Control' at http://192.168.42.1:8080."
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=Product Vamp Wi-Fi control access point
After=NetworkManager.service
Requires=NetworkManager.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/product-vamp-start-ap
[Install]
WantedBy=multi-user.target
+22
View File
@@ -0,0 +1,22 @@
[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
Conflicts=getty@tty1.service
[Service]
Restart=always
RestartSec=3
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
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'
[Install]
WantedBy=multi-user.target
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
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}
configure_connection() {
nmcli connection modify "$connection" \
connection.interface-name "$interface" \
802-11-wireless.mode ap \
802-11-wireless.band bg \
802-11-wireless.channel 6 \
802-11-wireless.ssid "$ssid" \
802-11-wireless-security.key-mgmt wpa-psk \
802-11-wireless-security.proto rsn \
802-11-wireless-security.pairwise ccmp \
802-11-wireless-security.group ccmp \
802-11-wireless-security.pmf 1 \
802-11-wireless-security.psk "$passphrase" \
ipv4.method shared \
ipv4.addresses "$cidr" \
ipv6.method ignore \
connection.autoconnect yes
}
if [ -r "$config" ]; then
ssid=$(python3 -c 'import json,sys; data=json.load(open(sys.argv[1])); print(data.get("control",{}).get("ap_ssid","Vamp Control"))' "$config")
passphrase=$(python3 -c 'import json,sys; data=json.load(open(sys.argv[1])); print(data.get("control",{}).get("ap_passphrase","vamp-control"))' "$config")
interface=$(python3 -c 'import json,sys; data=json.load(open(sys.argv[1])); print(data.get("control",{}).get("ap_interface","wlan0"))' "$config")
cidr=$(python3 -c 'import json,sys; data=json.load(open(sys.argv[1])); print(data.get("control",{}).get("zero_cidr","192.168.42.1/24"))' "$config")
fi
if nmcli -t -f NAME connection show | grep -Fxq "$connection"; then
configure_connection
else
nmcli connection add type wifi ifname "$interface" con-name "$connection" autoconnect yes ssid "$ssid"
configure_connection
fi
nmcli connection up "$connection"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
container_name=${PRODUCT_VAMP_ZERO_CONTAINER:-product-vamp-zero}
image_tag=${PRODUCT_VAMP_ZERO_IMAGE:-product-vamp-zero:dev}
config_dir=${PRODUCT_VAMP_CONFIG_DIR:-/etc/product-vamp}
state_dir=${PRODUCT_VAMP_STATE_DIR:-/var/lib/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"
if command -v chvt >/dev/null 2>&1 && [ -c "$display_tty" ]; then
tty_number=${display_tty#/dev/tty}
case "$tty_number" in
''|*[!0-9]*)
;;
*)
chvt "$tty_number" || true
;;
esac
fi
if [ -c "$display_tty" ]; then
if command -v setterm >/dev/null 2>&1; then
setterm --cursor off > "$display_tty" || printf '\033[?25l' > "$display_tty" || true
else
printf '\033[?25l' > "$display_tty" || true
fi
fi
docker rm -f "$container_name" >/dev/null 2>&1 || true
echo "Starting Product Vamp Zero container '$container_name' from '$image_tag' on '$display_tty'"
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 "$state_dir:/var/lib/product-vamp" \
-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"'