diff --git a/.github/workflows/plc-gate.yml b/.github/workflows/plc-gate.yml new file mode 100644 index 0000000..ab71891 --- /dev/null +++ b/.github/workflows/plc-gate.yml @@ -0,0 +1,77 @@ +name: PLC export and version stamp + +on: + pull_request: + branches: [main] + +jobs: + gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch main + run: git fetch origin main + + - name: ArfBot.xml must differ from main + run: | + set -euo pipefail + if git diff --quiet origin/main...HEAD -- Codesys/ArfBot.xml; then + echo "::error::Codesys/ArfBot.xml has no diff vs main." + echo "Export PLCopen XML (Project → Export) after changing the PLC project or stamping GVL_Version.sPlcVersion." + exit 1 + fi + echo "ArfBot.xml differs from main." + + - name: If ArfBot.project changed, XML must also change + run: | + set -euo pipefail + if git diff --quiet origin/main...HEAD -- Codesys/ArfBot.project; then + echo "ArfBot.project unchanged; skip paired-export check." + exit 0 + fi + if git diff --quiet origin/main...HEAD -- Codesys/ArfBot.xml; then + echo "::error::ArfBot.project changed but ArfBot.xml did not. Re-export PLCopen XML." + exit 1 + fi + echo "Project and XML both changed." + + - name: GVL_Version.sPlcVersion must be stamped and bumped + run: | + set -euo pipefail + python3 - <<'PY' + import re + import subprocess + import sys + + def extract(path): + try: + text = open(path, encoding="utf-8", errors="replace").read() + except OSError: + return "" + m = re.search( + r'.*?") + if main and pr == main: + print("::error::PLC XML changed but sPlcVersion is still %r. Bump GVL_Version and re-export." % pr) + sys.exit(1) + PY diff --git a/AGENTS.md b/AGENTS.md index 3b5e2c2..913378b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,7 @@ When starting a new session in this repo, read these files first: ### `scripts/` - Raspberry Pi Linux installer: `scripts/install-pi.sh`. - Default target is Raspberry Pi OS **64-bit Lite** with CODESYS Control **Raspberry Pi 64 SL**. 32-bit is no longer the documented path. -- `curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/main/scripts/install-pi.sh | bash` (clones this repo if needed). Flags: `--plc-only` (no camera/OpenCV; still DualSense + Bluetooth/animator web), `--vision-only` (no DualSense/CODESYS; still vision + animator web). +- `curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/main/scripts/install-pi.sh | bash` fetches the installer from `main`, then clones the **latest GitHub release** tag (same tree as the release source zip). `--ref main` for tip-of-tree; `--ref ` to pin. Flags: `--plc-only` (no camera/OpenCV; still DualSense + Bluetooth/animator web), `--vision-only` (no DualSense/CODESYS; still vision + animator web). Writes `/var/opt/codesys/PlcLogic/Application/RELEASE` (one-line tag) for the HMI to read via SysFile as `Application/RELEASE`. - Recommended PLC Pi order: image the Pi, Tools → Update Raspberry Pi, Multiple Download of `ArfBot.project`, then the installer. If the runtime was missing, re-run afterward only to set `SysProcess=AllowAll`. - Covers camera overlay, OpenCV, the combined web UI (`Web/` → `Application/Web` on :5000), vision/controller systemd services, and CODESYS `SysProcess=AllowAll` when the runtime is already present. It does not install the CODESYS Windows IDE, runtime, licenses, or Arduino/Teensy firmware. diff --git a/Codesys/ArfBot.project b/Codesys/ArfBot.project index 4e0a940..bcae45d 100644 Binary files a/Codesys/ArfBot.project and b/Codesys/ArfBot.project differ diff --git a/Codesys/ArfBot.xml b/Codesys/ArfBot.xml index 3765642..bdf7698 100644 --- a/Codesys/ArfBot.xml +++ b/Codesys/ArfBot.xml @@ -1,7 +1,7 @@  - - + + https://github.com/dalethomas81/ArfBotOS @@ -1572,6 +1572,40 @@ RESERVED: INT:=46; + + + + + + + + + + git describe --tags --always --dirty + git describe --tags --always main + + + + + + + + + + + + + + + + + + + + 02cb4726-2332-4e35-9d3d-9ab324dd7797 + + + @@ -3730,10 +3764,100 @@ FirstScan:=FALSE; + + + + + + + release + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + // get release tag to display on HMI +GVL_Version.sRelease := ''; +_hRelease := SysFile.SysFileOpen( + szFile := 'Application/RELEASE', + am := SysFile.ACCESS_MODE.AM_READ, + pResult := ADR(_udiReleaseResult)); +IF _hRelease <> SysTypes.RTS_INVALID_HANDLE THEN + _udiReleaseRead := SysFile.SysFileRead( + hFile := _hRelease, + pbyBuffer := ADR(_abyRelease), + ulSize := SIZEOF(_abyRelease) - 1, + pResult := ADR(_udiReleaseResult)); + SysFile.SysFileClose(_hRelease); + + _nRelease := LWORD_TO_UDINT(_udiReleaseRead); + IF _nRelease > 80 THEN + _nRelease := 80; + END_IF + WHILE _nRelease > 0 AND + (_abyRelease[_nRelease - 1] = 10 + OR _abyRelease[_nRelease - 1] = 13 + OR _abyRelease[_nRelease - 1] = 0) DO + _nRelease := _nRelease - 1; + END_WHILE + + MemUtils.MemCpy( + pbyDest := ADR(GVL_Version.sRelease), + pbySrc := ADR(_abyRelease), + dwSize := UDINT_TO_DWORD(_nRelease)); + _pReleaseLen := ADR(GVL_Version.sRelease) - 1; + _pReleaseLen^ := UDINT_TO_BYTE(_nRelease); + + GVL_Version.sVersionLine := CONCAT( + CONCAT('Installer ', GVL_Version.sRelease), + CONCAT(' PLC ', GVL_Version.sPlcVersion)); + +END_IF +IF GVL_Version.sRelease = '' THEN + GVL_Version.sVersionLine := 'unknown version'; + +END_IF + // check if arfbot has been initialized IF NOT PersistentVars.Flags[GVL.INIT_POS_IDX].Value THEN // poses @@ -25099,11 +25223,11 @@ END_IF - + @@ -25115,14 +25239,14 @@ END_IF + - - + - + @@ -36674,6 +36798,7 @@ END_IF + diff --git a/scripts/install-pi.sh b/scripts/install-pi.sh index 7cfb5ec..30ea466 100755 --- a/scripts/install-pi.sh +++ b/scripts/install-pi.sh @@ -8,6 +8,12 @@ # Does NOT install: Raspberry Pi OS, the CODESYS Windows IDE/runtime, # CODESYS licenses, or Arduino/Teensy firmware. # +# curl always fetches this script from main so installer fixes ship without a +# release. The clone itself defaults to the latest GitHub release tag (same +# tree as the release source zip). Override with --ref / ARFBOT_REF: +# --ref main tip of main +# --ref v2.2026.37.1-AlarmHistory pin a release +# # PLC Pi (recommended): image 64-bit Lite, Tools > Update Raspberry Pi (64 SL), # download ArfBot.project, then: # curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/main/scripts/install-pi.sh | bash @@ -28,16 +34,18 @@ set -euo pipefail DEFAULT_REPO_URL="https://github.com/dalethomas81/ArfBotOS.git" -# Keep this matching the branch this file is published from so -# `curl | bash` clones the same tree it just downloaded. -DEFAULT_REPO_REF="main" +# Always curl install-pi.sh from main. The clone ref is resolved later: +# empty / "latest" -> GitHub releases/latest tag. +INSTALLER_SCRIPT_REF="main" REPO_URL="${ARFBOT_REPO:-${DEFAULT_REPO_URL}}" -REPO_REF="${ARFBOT_REF:-${DEFAULT_REPO_REF}}" +REPO_REF="${ARFBOT_REF:-}" -VISION_DST="/var/opt/codesys/PlcLogic/Application/Vision" -WEB_DST="/var/opt/codesys/PlcLogic/Application/Web" -CONTROLLER_DST="/var/opt/codesys/PlcLogic/Application/Controller" +APPLICATION_DST="/var/opt/codesys/PlcLogic/Application" +VISION_DST="${APPLICATION_DST}/Vision" +WEB_DST="${APPLICATION_DST}/Web" +CONTROLLER_DST="${APPLICATION_DST}/Controller" +RELEASE_FILE="${APPLICATION_DST}/RELEASE" VENV_DIR="/opt/arfbot/venv" UDEV_RULES_DST="/etc/udev/rules.d/70-ps5-controller.rules" @@ -68,8 +76,8 @@ usage() { ArfBotOS Raspberry Pi installer Usage: - curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${DEFAULT_REPO_REF}/scripts/install-pi.sh | bash - curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${DEFAULT_REPO_REF}/scripts/install-pi.sh | bash -s -- [options] + curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash + curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash -s -- [options] sudo ./scripts/install-pi.sh [options] Options: @@ -84,7 +92,8 @@ Options: --skip-codesys Do not patch CODESYSControl.cfg. --repo URL Git remote to clone when not run from a checkout (default: ${DEFAULT_REPO_URL}). - --ref REF Branch or tag to clone (default: ${DEFAULT_REPO_REF}). + --ref REF Branch or tag to clone. Default: latest GitHub release. + --ref main for tip-of-tree. --ref latest is the default. --no-start Enable systemd units but do not start them. --recreate-venv Delete /opt/arfbot/venv and create it again. --force Allow running on a machine that is not a Raspberry Pi. @@ -101,13 +110,13 @@ Typical PLC Pi (Raspberry Pi OS 64-bit Lite): 1. Image the Pi and SSH in 2. From Windows CODESYS: Tools > Update Raspberry Pi (Raspberry Pi 64 SL) 3. Multiple Download of ArfBot.project - 4. curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${DEFAULT_REPO_REF}/scripts/install-pi.sh | bash + 4. curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash PLC Pi without vision (same order, then): - curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${DEFAULT_REPO_REF}/scripts/install-pi.sh | bash -s -- --plc-only + curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash -s -- --plc-only Dedicated vision Pi (64-bit Lite, no CODESYS): - curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${DEFAULT_REPO_REF}/scripts/install-pi.sh | bash -s -- --vision-only + curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash -s -- --vision-only If you ran this installer before the CODESYS runtime was present, run it again afterward so it can set SysProcess Command=AllowAll. It will not redo @@ -175,6 +184,62 @@ parse_args() { fi } +github_repo_slug() { + local url="$1" + url="${url%.git}" + url="${url%/}" + if [[ "${url}" =~ github.com[:/]+([^/]+)/([^/]+) ]]; then + printf '%s/%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + return 0 + fi + return 1 +} + +parse_tag_name_from_release_json() { + local json="$1" + local tag="" + if command -v python3 >/dev/null 2>&1; then + tag="$(printf '%s' "${json}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])' 2>/dev/null || true)" + fi + if [[ -z "${tag}" ]]; then + tag="$(printf '%s' "${json}" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)" + fi + if [[ -z "${tag}" ]]; then + return 1 + fi + printf '%s\n' "${tag}" +} + +fetch_latest_release_tag() { + local slug="" + local json="" + local tag="" + local api="" + slug="$(github_repo_slug "${REPO_URL}")" \ + || die "cannot resolve latest release for ${REPO_URL}; pass --ref TAG or --ref main" + api="https://api.github.com/repos/${slug}/releases/latest" + if ! command -v curl >/dev/null 2>&1; then + die "curl is required to resolve the latest release; pass --ref TAG or --ref main" + fi + json="$(curl -fsSL -A "ArfBotOS-install-pi" -H "Accept: application/vnd.github+json" "${api}")" \ + || die "could not query GitHub releases for ${slug}; pass --ref TAG or --ref main" + tag="$(parse_tag_name_from_release_json "${json}")" \ + || die "GitHub latest-release response had no tag_name; pass --ref TAG or --ref main" + printf '%s\n' "${tag}" +} + +resolve_repo_ref() { + local slug="" + if [[ -n "${REPO_REF}" && "${REPO_REF}" != "latest" ]]; then + return 0 + fi + slug="$(github_repo_slug "${REPO_URL}" || true)" + log "resolving latest GitHub release${slug:+ for ${slug}}" + REPO_REF="$(fetch_latest_release_tag)" + log "using latest release ${REPO_REF}" + export ARFBOT_REF="${REPO_REF}" +} + resolve_script_paths() { local source="${BASH_SOURCE[0]:-$0}" SCRIPT_FILE="" @@ -321,6 +386,8 @@ ensure_git() { bootstrap_from_git() { local dest dest="$(user_home)/ArfBotOS" + resolve_repo_ref + export ARFBOT_REF="${REPO_REF}" log "not running from an ArfBotOS checkout; cloning ${REPO_URL} (${REPO_REF}) into ${dest}" ensure_git if is_true "${DRY_RUN}"; then @@ -347,6 +414,8 @@ bootstrap_from_git() { die "clone is missing ${next} (check --ref ${REPO_REF})" fi chmod +x "${next}" || true + # Write before exec so an older cloned installer still leaves the tag file. + write_release_file log "re-executing ${next}" exec bash "${next}" "$@" } @@ -384,6 +453,32 @@ require_pi() { die "this installer is meant to run on a Raspberry Pi (use --force to override)" } +release_tag() { + local tag="${REPO_REF:-}" + if [[ -z "${tag}" && -n "${REPO_ROOT}" && -d "${REPO_ROOT}/.git" ]] && command -v git >/dev/null 2>&1; then + tag="$(git -C "${REPO_ROOT}" describe --tags --always --dirty 2>/dev/null || true)" + fi + if [[ -z "${tag}" ]]; then + tag="unknown" + fi + printf '%s\n' "${tag}" +} + +write_release_file() { + local tag="" + tag="$(release_tag)" + tag="${tag%$'\n'}" + log "writing release tag ${tag} to ${RELEASE_FILE}" + if is_true "${DRY_RUN}"; then + printf 'DRY-RUN: printf %%s\\n %s > %s\n' "${tag}" "${RELEASE_FILE}" + return 0 + fi + as_root mkdir -p "${APPLICATION_DST}" + printf '%s\n' "${tag}" | as_root tee "${RELEASE_FILE}" >/dev/null + as_root chmod 644 "${RELEASE_FILE}" + chown_to_invoking_user "${RELEASE_FILE}" +} + copy_file() { local src="$1" local dst="$2" @@ -916,6 +1011,9 @@ configure_codesys() { chown_deploy_tree() { local home="" + if [[ -e "${RELEASE_FILE}" ]]; then + chown_to_invoking_user "${RELEASE_FILE}" + fi if ! is_true "${SKIP_VISION}"; then chown_to_invoking_user "${VISION_DST}" fi @@ -1014,6 +1112,14 @@ PY if is_true "${DRY_RUN}"; then log "dry-run: skipping destination file checks" + elif [[ -s "${RELEASE_FILE}" ]]; then + log "ok: ${RELEASE_FILE} ($(tr -d '\n' < "${RELEASE_FILE}"))" + else + warn "missing ${RELEASE_FILE}" + failed=1 + fi + if is_true "${DRY_RUN}"; then + : elif ! is_true "${SKIP_VISION}"; then local required_files=( "${VISION_DST}/PyServer.py" @@ -1077,6 +1183,8 @@ print_next_steps() { ArfBotOS Pi install finished mode: $(install_mode) clone / files: ${REPO_ROOT} + git ref: ${REPO_REF:-local checkout} + release file: ${RELEASE_FILE} venv: ${VENV_DIR} EOF if ! is_true "${SKIP_VISION}"; then @@ -1132,11 +1240,11 @@ Controller dest: ${CONTROLLER_DST} Venv: ${VENV_DIR} PLC Pi: - curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${REPO_REF}/scripts/install-pi.sh | bash - curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${REPO_REF}/scripts/install-pi.sh | bash -s -- --plc-only + curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash + curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash -s -- --plc-only Dedicated vision Pi: - curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${REPO_REF}/scripts/install-pi.sh | bash -s -- --vision-only + curl -sSL https://raw.githubusercontent.com/dalethomas81/ArfBotOS/${INSTALLER_SCRIPT_REF}/scripts/install-pi.sh | bash -s -- --vision-only EOF } @@ -1160,6 +1268,7 @@ main() { deploy_vision_files deploy_web_files deploy_controller_files + write_release_file install_systemd_units configure_codesys chown_deploy_tree