Binary file not shown.
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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@
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 |
|
||||
Executable
+67
@@ -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"
|
||||
Executable
+27
@@ -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
@@ -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:]))
|
||||
@@ -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"]
|
||||
Executable
+75
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+44
@@ -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"
|
||||
@@ -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"'
|
||||
Reference in New Issue
Block a user