diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..d270424 --- /dev/null +++ b/.actrc @@ -0,0 +1 @@ +-P ubuntu-latest=catthehacker/ubuntu:runner-latest diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..82ebf5c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 2 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false +indent_size = unset + +[*.py] +indent_size = 4 + +[*.sh] +indent_style = space +indent_size = 4 +binary_next_line = true +switch_case_indent = true +space_redirects = false +keep_padding = true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6834fef --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +name: Run ATX-Raspi tests +on: + pull_request: + push: + workflow_dispatch: +jobs: + flake: + name: Run Nix flake checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Initialize Nix store paths + run: | + sudo mkdir -p /nix/store + sudo chmod -R 777 /nix + # https://github.com/cachix/install-nix-action/issues/56#issuecomment-1030697681 + - name: Configure Nix store cache + uses: actions/cache@v4 + with: + key: ${{ runner.os }}-${{ runner.arch }}-nix-store + # See https://github.com/actions/cache/pull/726 and + # https://github.com/actions/cache/issues/494 for caveats on negative + # patterns. + path: | + /nix/store/** + /nix/var/nix/*/* + /nix/var/nix/db/* + /nix/var/nix/db/*/** + !/nix/var/nix/daemon-socket/socket + !/nix/var/nix/userpool/* + !/nix/var/nix/gc.lock + !/nix/var/nix/db/big-lock + !/nix/var/nix/db/reserved + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + extra_nix_config: | + system-features = benchmark big-parallel kvm nixos-test uid-range + - name: Run Nix flake checks + run: | + nix flake check -L diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6688664 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# VM disk images +*.qcow2 + +# Nix build results +result +result-* +repl-result-* + +# NixOS test driver REPL history +.nixos-test-history + +# Binary executable. +/nix/pkgs/gpiosimtest/gpiosimtest diff --git a/doc/nixos-modules.md b/doc/nixos-modules.md new file mode 100644 index 0000000..b494496 --- /dev/null +++ b/doc/nixos-modules.md @@ -0,0 +1,206 @@ +## services\.atx-raspi-shutdown\.enable + + + +Whether to enable the ATX-Raspi shutdown daemon\. + + + +*Type:* +boolean + + + +*Default:* +` false ` + + + +*Example:* +` true ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.atx-raspi-shutdown\.package + + + +Package providing the ATX-Raspi daemon\. + + + +*Type:* +package + + + +*Default:* +` ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.atx-raspi-shutdown\.chip + +The path to the GPIO chip\. + + + +*Type:* +null or absolute path + + + +*Default:* +` null ` + + + +*Example:* +` "/dev/gpiochip42" ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.atx-raspi-shutdown\.implementation + + + +GPIO daemon implementation\. “irq” selects the +edge-triggered Python implementation\. “check” selects +the polling-based shell implementation\. + + + +*Type:* +one of “check”, “irq” + + + +*Default:* +` "irq" ` + + + +*Example:* +` "check" ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.atx-raspi-shutdown\.pins\.boot + + + +Pin on ` chip ` to set high upon ATX-Raspi +service startup\. + + + +*Type:* +null or (unsigned integer, meaning >=0) + + + +*Default:* +` null ` + + + +*Example:* +` 8 ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.atx-raspi-shutdown\.pins\.shutdown + + + +Pin on ` chip ` to watch for the shutdown or +reboot button press\. + + + +*Type:* +null or (unsigned integer, meaning >=0) + + + +*Default:* +` null ` + + + +*Example:* +` 7 ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.atx-raspi-shutdown\.pulses\.reboot + + + +Minimum duration of high state on +` pins.shutdown ` to trigger reboot\. + + + +*Type:* +null or floating point number + + + +*Default:* +` null ` + + + +*Example:* +` 0.2 ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.atx-raspi-shutdown\.pulses\.shutdown + + + +Minimum duration of high state on +` pins.shutdown ` to trigger shutdown\. + + + +*Type:* +null or floating point number + + + +*Default:* +` null ` + + + +*Example:* +` 0.6 ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c24a65b --- /dev/null +++ b/flake.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1741473158, + "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", + "owner": "numtide", + "repo": "devshell", + "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1741352980, + "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1742669843, + "narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1e5b653dff12029333a6546c11e108ede13052eb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1740877520, + "narHash": "sha256-oiwv/ZK/2FhGxrCkQkB83i7GnWXPPLzoqFHpDD3uYpk=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "147dee35aab2193b174e4c0868bd80ead5ce755c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1742370146, + "narHash": "sha256-XRE8hL4vKIQyVMDXykFh4ceo3KSpuJF3ts8GKwh5bIU=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "adc195eef5da3606891cedf80c0d9ce2d3190808", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1e5ee45 --- /dev/null +++ b/flake.nix @@ -0,0 +1,83 @@ +{ + description = "Description for the project"; + + inputs = { + devshell.url = "github:numtide/devshell"; + devshell.inputs.nixpkgs.follows = "nixpkgs"; + + flake-parts.url = "github:hercules-ci/flake-parts"; + + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + treefmt-nix.url = "github:numtide/treefmt-nix"; + treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake {inherit inputs;} ({ + inputs, + moduleWithSystem, + self, + ... + }: { + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; + + imports = [ + inputs.devshell.flakeModule + inputs.treefmt-nix.flakeModule + + ./nix/checks.nix + ./nix/devshells.nix + ./nix/nixos-modules.nix + ./nix/packages.nix + ]; + + flake = { + nixosConfigurations = { + default = inputs.nixpkgs.lib.nixosSystem { + modules = [ + self.nixosModules.atx-raspi + { + boot.isContainer = true; + fileSystems."/".fsType = "tmpfs"; + nixpkgs.hostPlatform = "x86_64-linux"; + system.stateVersion = "24.11"; + } + ]; + }; + }; + }; + + perSystem = { + config, + lib, + ... + }: { + apps = lib.mapAttrs (lib.const (lib.getAttr "flakeApp")) config.devShells; + + treefmt = {config, ...}: { + flakeFormatter = true; + projectRootFile = "flake.nix"; + + programs = { + alejandra.enable = true; + gofumpt.enable = true; + ruff.enable = true; + shellcheck.enable = true; + shfmt.enable = true; + }; + + settings.formatter = lib.mkIf config.programs.shfmt.enable { + # Empty out the CLI options list so that `shfmt` uses the settings + # from `.editorconfig`. + shfmt.options = lib.mkForce []; + }; + }; + }; + }); +} diff --git a/nix/checks.nix b/nix/checks.nix new file mode 100644 index 0000000..c9aa181 --- /dev/null +++ b/nix/checks.nix @@ -0,0 +1,116 @@ +{self, ...}: { + perSystem = { + config, + lib, + pkgs, + ... + }: { + checks = { + default = config.checks.atx-raspi; + + atx-raspi = pkgs.nixosTest { + name = "atx-raspi-shutdown"; + nodes = let + common = {pkgs, ...}: { + imports = [self.nixosModules.default]; + + boot.kernelModules = ["gpio-sim"]; + + environment.systemPackages = with pkgs; [ + curl + jq + ]; + + security.polkit.debug = true; + security.polkit.enable = true; + + services.atx-raspi-shutdown.enable = true; + + systemd.services.gpiosimtest = { + wantedBy = ["multi-user.target"]; + environment.PORT = "8080"; + serviceConfig = { + ExecStart = lib.getExe config.packages.gpiosimtest; + }; + }; + + systemd.services.atx-raspi-shutdown = { + wantedBy = lib.mkForce []; # Need to start manually. + requires = ["gpiosimtest.service"]; + after = ["gpiosimtest.service"]; + environment.ATX_RASPI_DRY_RUN = "1"; + }; + }; + in { + check = {...}: { + imports = [common]; + services.atx-raspi-shutdown.implementation = "check"; + }; + + irq = {...}: { + imports = [common]; + services.atx-raspi-shutdown.implementation = "irq"; + }; + }; + testScript = {nodes, ...}: let + checkURL = "http://localhost:${nodes.check.systemd.services.gpiosimtest.environment.PORT}"; + irqURL = "http://localhost:${nodes.irq.systemd.services.gpiosimtest.environment.PORT}"; + in '' + import time + + from typing import Optional + + def systemctl_succeed(m: Machine, q: str, user: Optional[str] = None) -> str: + user_desc = "root" + if user is not None: + user_desc = user + with m.nested(f"command `systemctl {q}` must succeed as {user_desc}"): + (status, output) = m.systemctl(q, user) + + if status != 0: + m.log(f"output: {output}") + raise Exception(f"command `systemctl {q}` failed as {user_desc} (exit code {status})") + + return output + + start_all() + + check.succeed("modprobe gpio_sim 1>&2") + check.succeed("lsmod | grep gpio_sim 1>&2") + + check.wait_for_unit("dbus.service") + + systemctl_succeed(check, "cat polkit.service 1>&2") + systemctl_succeed(check, "cat atx-raspi-shutdown.service 1>&2") + + systemctl_succeed(irq, "cat polkit.service 1>&2") + systemctl_succeed(irq, "cat atx-raspi-shutdown.service 1>&2") + + check.wait_for_unit("gpiosimtest.service") + check_chip = check.succeed("curl ${checkURL}/devpath | jq --raw-output '.data.devpath'").strip() + systemctl_succeed(check, f"set-environment ATX_RASPI_CHIP={check_chip}") + + irq.wait_for_unit("gpiosimtest.service") + irq_chip = irq.succeed("curl ${irqURL}/devpath | jq --raw-output '.data.devpath'").strip() + systemctl_succeed(irq, f"set-environment ATX_RASPI_CHIP={irq_chip}") + + systemctl_succeed(check, "start atx-raspi-shutdown.service") + check.wait_for_unit("atx-raspi-shutdown.service") + check.wait_until_succeeds("test $(curl ${checkURL}/line/8/level | jq --raw-output '.data.level') = 1") + check.wait_until_succeeds("curl -X PUT ${checkURL}/line/7/pull/up | jq 1>&2") + time.sleep(5) + check.wait_until_succeeds("curl -X PUT ${checkURL}/line/7/pull/down | jq 1>&2") + check.wait_until_succeeds(f"journalctl --grep='SHUTDOWN request on chip {check_chip}'") + + systemctl_succeed(irq, "start atx-raspi-shutdown.service") + irq.wait_for_unit("atx-raspi-shutdown.service") + irq.wait_until_succeeds("test $(curl ${irqURL}/line/8/level | jq --raw-output '.data.level') = 1") + irq.wait_until_succeeds("curl -X PUT ${irqURL}/line/7/pull/up | jq 1>&2") + time.sleep(5) + irq.wait_until_succeeds("curl -X PUT ${irqURL}/line/7/pull/down | jq 1>&2") + check.wait_until_succeeds(f"journalctl --grep='SHUTDOWN request on chip {irq_chip}'") + ''; + }; + }; + }; +} diff --git a/nix/devshells.nix b/nix/devshells.nix new file mode 100644 index 0000000..aa3099f --- /dev/null +++ b/nix/devshells.nix @@ -0,0 +1,50 @@ +{ + perSystem = { + config, + lib, + pkgs, + ... + }: { + devShells = { + default = config.devShells.atx-raspi; + }; + + devshells = { + atx-raspi = {extraModulesPath, ...}: { + imports = ["${extraModulesPath}/language/go.nix"]; + + commands = + [ + { + package = pkgs.act; + } + + { + package = pkgs.libgpiod; + } + + { + package = config.treefmt.build.wrapper; + } + + { + name = "mkoptdocs"; + command = '' + cd "$(git rev-parse --show-cdup)" || exit + while read -r out_path; do + install -Dm0644 "$out_path" ./doc/nixos-modules.md + done < <(nix build "$@" --no-link --print-out-paths '.#docs') + ''; + help = "Build NixOS module options documentation"; + } + ] + ++ lib.pipe config.packages [ + (lib.flip builtins.removeAttrs ["default"]) + (builtins.attrValues) + (lib.filter (package: package ? meta.mainProgram)) + (map (package: {inherit package;})) + ]; + }; + }; + }; +} diff --git a/nix/nixos-modules.nix b/nix/nixos-modules.nix new file mode 100644 index 0000000..89453be --- /dev/null +++ b/nix/nixos-modules.nix @@ -0,0 +1,236 @@ +{ + self, + moduleWithSystem, + ... +}: { + flake = { + nixosModules = { + default = self.nixosModules.atx-raspi; + + atx-raspi = moduleWithSystem ( + {config, ...} @ perSystem: { + config, + lib, + options, + ... + }: let + inherit (lib) mkOption types; + cfg = config.services.atx-raspi-shutdown; + opts = options.services.atx-raspi-shutdown; + description = "ATX-Raspi shutdown daemon"; + polkitEnabled = config.security.polkit.enable; + wrap = lib.replaceStrings ["\n"] [" "]; + in { + options = { + services = { + atx-raspi-shutdown = { + enable = lib.mkEnableOption "the ${description}"; + + implementation = mkOption { + type = types.enum ["check" "irq"]; + default = "irq"; + example = "check"; + description = '' + GPIO daemon implementation. "irq" selects the + edge-triggered Python implementation. "check" selects + the polling-based shell implementation. + ''; + }; + + package = mkOption { + type = types.package; + default = perSystem.config.packages."atx-raspi-shutdown${cfg.implementation}"; + description = '' + Package providing the ATX-Raspi daemon. + ''; + }; + + chip = mkOption { + type = types.nullOr types.path; + default = null; + example = "/dev/gpiochip42"; + description = '' + The path to the GPIO chip. + ''; + }; + + pins = { + shutdown = mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + example = 7; + description = '' + Pin on {option}`chip` to watch for the shutdown or + reboot button press. + ''; + }; + + boot = mkOption { + inherit (opts.pins.shutdown) type; + default = null; + example = 8; + description = '' + Pin on {option}`chip` to set high upon ATX-Raspi + service startup. + ''; + }; + }; + + pulses = { + reboot = mkOption { + type = types.nullOr types.float; + default = null; + example = 0.2; + description = '' + Minimum duration of high state on + {option}`pins.shutdown` to trigger reboot. + ''; + }; + + shutdown = mkOption { + inherit (opts.pulses.reboot) type; + default = null; + example = 0.6; + description = '' + Minimum duration of high state on + {option}`pins.shutdown` to trigger shutdown. + ''; + }; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.atx-raspi-shutdown = lib.const { + imports = [ + (lib.mkIf polkitEnabled { + #serviceConfig.DynamicUser = true; + }) + ]; + + description = "The ${description}"; + + wantedBy = ["basic.target"]; + + environment = + lib.flip lib.pipe [ + (lib.filterAttrs (_: value: value != null)) + (lib.mapAttrs' (name: value: { + inherit value; + name = "ATX_RASPI_${lib.toUpper name}"; + })) + ] { + inherit (cfg) chip; + boot_pin = cfg.pins.boot; + shutdown_pin = cfg.pins.shutdown; + pulse_min = cfg.pulses.reboot; + pulse_max = cfg.pulses.shutdown; + }; + + serviceConfig = { + UMask = "0027"; + + Restart = "always"; + RestartSec = 3; + + BindPaths = + [ + #"/run/dbus/system_bus_socket" + ] + ++ lib.optional (cfg.chip != null) cfg.chip; + + ExecStart = lib.getExe cfg.package; + + # Security settings + CapabilityBoundingSet = [""]; + DevicePolicy = "closed"; + DeviceAllow = "char-gpiochip"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = false; + PrivateIPC = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service @resources" + "~@privileged" + ]; + }; + }; + + assertions = let + noNullAttrValues = attrs: lib.all (x: x != null) (builtins.attrValues attrs); + in [ + { + assertion = (noNullAttrValues cfg.pins) -> (cfg.pins.boot != cfg.pins.shutdown); + message = wrap '' + atx-raspi: boot pin and shutdown pin must be distinct + ''; + } + + { + assertion = (noNullAttrValues cfg.pulses) -> (cfg.pulses.reboot > cfg.pulses.shutdown); + message = wrap '' + atx-raspi: shutdown pulse must be longer than reboot pulse + (reboot pulse is ${toString cfg.pulses.reboot}, shutdown + pulse is ${toString cfg.pulses.poweroff}) + ''; + } + ]; + + warnings = lib.optional (!polkitEnabled) (wrap '' + atx-raspi: `security.polkit.enable` is set to `false`; running + the GPIO daemon as `root`. + ''); + + security.polkit.extraConfig = '' + // Permit ATX-Raspi service users to power off and reboot + polkit.addRule(function(action, subject) { + if ( + ( + action.id == "org.freedesktop.login1.power-off" + || action.id == "org.freedesktop.login1.power-off-multiple-sessions" + || action.id == "org.freedesktop.login1.power-off-ignore-inhibit" + || action.id == "org.freedesktop.login1.reboot" + || action.id == "org.freedesktop.login1.reboot-multiple-sessions" + || action.id == "org.freedesktop.login1.reboot-ignore-inhibit" + || action.id == "org.freedesktop.login1.suspend" + || action.id == "org.freedesktop.login1.suspend-multiple-sessions" + || action.id == "org.freedesktop.login1.suspend-ignore-inhibit" + || action.id == "org.freedesktop.login1.hibernate" + || action.id == "org.freedesktop.login1.hibernate-multiple-sessions" + || action.id == "org.freedesktop.login1.hibernate-ignore-inhibit" + ) && subject.user == "${config.systemd.services.atx-raspi-shutdown.serviceConfig.User or "atx-raspi-shutdown"}" + ) { + return polkit.Result.YES; + } + }); + ''; + }; + } + ); + }; + }; +} diff --git a/nix/packages.nix b/nix/packages.nix new file mode 100644 index 0000000..9fcfed3 --- /dev/null +++ b/nix/packages.nix @@ -0,0 +1,81 @@ +{self, ...}: { + perSystem = { + config, + pkgs, + system, + ... + }: { + packages = { + default = config.packages.atx-raspi-shutdownirq; + + atx-raspi-shutdownirq = pkgs.callPackage ({ + writers, + python3, + }: + writers.makeScriptWriter { + inherit (python3.withPackages (p: [p.libgpiod])) interpreter; + } "/bin/atx-raspi-shutdownirq" (builtins.readFile "${self}/shutdownirq.py")) {}; + + atx-raspi-shutdowncheck = pkgs.callPackage ({ + coreutils, + libgpiod, + lib, + writers, + }: + writers.writeBashBin "atx-raspi-shutdowncheck" { + makeWrapperArgs = [ + "--prefix" + "PATH" + ":" + (lib.makeBinPath [coreutils libgpiod]) + ]; + } (builtins.readFile "${self}/shutdowncheck.sh")) {}; + + docs = pkgs.callPackage ({ + nixosOptionsDoc, + lib, + eval, + }: + (nixosOptionsDoc { + options = { + inherit (eval.options.services) atx-raspi-shutdown; + }; + + # Default is currently "appendix". + documentType = "none"; + + warningsAreErrors = true; + + transformOptions = let + ourPrefix = "${toString self}/"; + moduleSource = "flake.nix"; + link = { + url = "/${moduleSource}"; + name = moduleSource; + }; + in + opt: + opt + // { + visible = opt.visible && (lib.any (lib.hasPrefix ourPrefix) opt.declarations); + declarations = map (decl: + if lib.hasPrefix ourPrefix decl + then link + else decl) + opt.declarations; + }; + }) + .optionsCommonMark) { + eval = self.nixosConfigurations.default.extendModules { + modules = [ + { + nixpkgs.hostPlatform = system; + } + ]; + }; + }; + + gpiosimtest = pkgs.callPackage ./pkgs/gpiosimtest {}; + }; + }; +} diff --git a/nix/pkgs/gpiosimtest/default.nix b/nix/pkgs/gpiosimtest/default.nix new file mode 100644 index 0000000..be82aa8 --- /dev/null +++ b/nix/pkgs/gpiosimtest/default.nix @@ -0,0 +1,13 @@ +{buildGoModule}: +buildGoModule { + pname = "gpiosimtest"; + version = "0.1.0"; + src = ./.; + vendorHash = "sha256-wufUG34GPZLdcuhVls06o3uiVf0wYUVjFQOdbYbZ5pg="; + tags = ["nomsgpack"]; + meta = { + description = "GPIO chip simulation API"; + homepage = "https://github.com/LowPowerLab/ATX-Raspi"; + mainProgram = "gpiosimtest"; + }; +} diff --git a/nix/pkgs/gpiosimtest/go.mod b/nix/pkgs/gpiosimtest/go.mod new file mode 100644 index 0000000..b72aa88 --- /dev/null +++ b/nix/pkgs/gpiosimtest/go.mod @@ -0,0 +1,39 @@ +module github.com/LowPowerLab/ATX-Raspi/gpiosimtest + +go 1.21.4 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/warthog618/go-gpiosim v0.1.1 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/warthog618/go-gpiocdev v0.9.1 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/nix/pkgs/gpiosimtest/go.sum b/nix/pkgs/gpiosimtest/go.sum new file mode 100644 index 0000000..70a0f2c --- /dev/null +++ b/nix/pkgs/gpiosimtest/go.sum @@ -0,0 +1,95 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgXYbmhuCI= +github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8= +github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4= +github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/nix/pkgs/gpiosimtest/main.go b/nix/pkgs/gpiosimtest/main.go new file mode 100644 index 0000000..4dc2270 --- /dev/null +++ b/nix/pkgs/gpiosimtest/main.go @@ -0,0 +1,201 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "strconv" + + "github.com/gin-gonic/gin" + gpiosim "github.com/warthog618/go-gpiosim" +) + +const ( + ENOLINE = -1 +) + +func retrieveLineOffset(g *gpiosim.Simpleton, spec string) (int, error) { + offset, err := strconv.Atoi(spec) + if err != nil { + return ENOLINE, err + } + + if offset < 0 || offset >= g.Config().NumLines { + return ENOLINE, fmt.Errorf("offset must be an integer between 0 and %d; got %d", g.Config().NumLines-1, offset) + } + + return offset, nil +} + +func handleLineOffset(c *gin.Context, g *gpiosim.Simpleton, spec string) (int, error) { + offset, err := retrieveLineOffset(g, spec) + if err != nil { + respondNotFound(c, err, gin.H{ + "offset": offset, + }) + } + + return offset, err +} + +func handleLevel(c *gin.Context, g *gpiosim.Simpleton, offset int) { + level, err := g.Level(offset) + if err != nil { + respondInternalServerError(c, err, gin.H{"offset": offset, "level": level}) + } else { + respondOk(c, gin.H{"offset": offset, "level": level}) + } +} + +func handlePull(c *gin.Context, g *gpiosim.Simpleton, offset int) { + pull, err := g.Pull(offset) + if err != nil { + respondInternalServerError(c, err, gin.H{"offset": offset, "pull": pull}) + } else { + respondOk(c, gin.H{"offset": offset, "pull": pull}) + } +} + +func respond(c *gin.Context, st int, err error, data gin.H) { + c.JSON(st, gin.H{"error": err, "data": data}) +} + +func respondOk(c *gin.Context, data gin.H) { + respond(c, http.StatusOK, nil, data) +} + +func respondBadRequest(c *gin.Context, err error, data gin.H) { + respond(c, http.StatusBadRequest, err, data) +} + +func respondNotFound(c *gin.Context, err error, data gin.H) { + respond(c, http.StatusNotFound, err, data) +} + +func respondInternalServerError(c *gin.Context, err error, data gin.H) { + respond(c, http.StatusInternalServerError, err, data) +} + +func main() { + lines, ok := os.LookupEnv("LINES") + var linei int + if ok { + linec, err := strconv.Atoi(lines) + if err != nil { + fmt.Printf("error parsing LINES specification %v: %v\n", lines, err) + os.Exit(1) + } else { + linei = linec + } + } else { + linei = 9 + } + + g, err := gpiosim.NewSimpleton(linei) + if err != nil { + fmt.Printf("error creating simulated GPIO: %v\n", err) + os.Exit(1) + } + + defer g.Close() + + r := gin.Default() + + r.GET("/", func(c *gin.Context) { + respondOk(c, gin.H{ + "chipname": g.ChipName(), + "config": g.Config(), + "devpath": g.DevPath(), + }) + }) + + r.GET("/chipname", func(c *gin.Context) { + respondOk(c, gin.H{"chipname": g.ChipName()}) + }) + + r.GET("/config", func(c *gin.Context) { + respondOk(c, gin.H{"config": g.Config()}) + }) + + r.GET("/devpath", func(c *gin.Context) { + respondOk(c, gin.H{"devpath": g.DevPath()}) + }) + + r.GET("/line/:offset/level", func(c *gin.Context) { + offset, err := handleLineOffset(c, g, c.Param("offset")) + if err != nil { + return + } + + handleLevel(c, g, offset) + }) + + r.GET("/line/:offset/pull", func(c *gin.Context) { + offset, err := handleLineOffset(c, g, c.Param("offset")) + if err != nil { + return + } + + handlePull(c, g, offset) + }) + + r.PUT("/line/:offset/pull/down", func(c *gin.Context) { + offset, err := handleLineOffset(c, g, c.Param("offset")) + if err != nil { + return + } + + if err := g.Pulldown(offset); err != nil { + respondInternalServerError(c, err, gin.H{"offset": offset}) + } else { + handlePull(c, g, offset) + } + }) + + r.PUT("/line/:offset/pull/up", func(c *gin.Context) { + offset, err := handleLineOffset(c, g, c.Param("offset")) + if err != nil { + return + } + + if err := g.Pullup(offset); err != nil { + respondInternalServerError(c, err, gin.H{"offset": offset}) + } else { + handlePull(c, g, offset) + } + }) + + r.PUT("/line/:offset/pull/:bias", func(c *gin.Context) { + offset, err := handleLineOffset(c, g, c.Param("offset")) + if err != nil { + return + } + + bias, err := strconv.Atoi(c.Param("bias")) + if err != nil { + respondBadRequest(c, err, gin.H{"offset": offset, "bias": bias}) + return + } + + if err := g.SetPull(offset, bias); err != nil { + respondInternalServerError(c, err, gin.H{"offset": offset, "bias": bias}) + } else { + handlePull(c, g, offset) + } + }) + + r.PUT("/line/:offset/pull/toggle", func(c *gin.Context) { + offset, err := handleLineOffset(c, g, c.Param("offset")) + if err != nil { + return + } + + if err := g.Toggle(offset); err != nil { + respondInternalServerError(c, err, gin.H{"offset": offset}) + } else { + handlePull(c, g, offset) + } + }) + + r.Run() +} diff --git a/shutdowncheck.sh b/shutdowncheck.sh new file mode 100755 index 0000000..49a9304 --- /dev/null +++ b/shutdowncheck.sh @@ -0,0 +1,212 @@ +#!/bin/sh + +# ATXRaspi/MightyHat interrupt based shutdown/reboot script +# Script by Felix Rusu + +set -eu + +header() { + if [ "$#" -lt 2 ]; then + echo 1>&2 'internal error: usage: header [...]' + return 64 # EX_USAGE + fi + + padding="${1:-0}" + shift + + if [ "$((padding+0))" != "$padding" ]; then + echo 1>&2 'internal error: padding must be an integer' + return 64 # EX_USAGE + fi + + left='' + while [ "$padding" -gt 0 ]; do + left="${left} " + padding="$((padding-1))" + done + + echo "==========================================================================================" + for line in "$@"; do + echo "${left}${line}" + done + echo "==========================================================================================" +} + +f2nsec() { + case "${1?}" in + *.*.*) + echo 1>&2 "Not a valid decimal number: ${1}" + return 1 + ;; + *.*) + __f2nsec_whole="${1%%.*}" + __f2nsec_fractional="${1#*.}" + + __f2nsec_exp="${#__f2nsec_fractional}" + __f2nsec_scale=1 + + while [ "$__f2nsec_exp" -gt 0 ]; do + __f2nsec_scale="$(( __f2nsec_scale * 10 ))" + __f2nsec_exp="$(( __f2nsec_exp - 1 ))" + done + + echo "$(( (__f2nsec_whole * 1000000000) + ((__f2nsec_fractional * 1000000000) / __f2nsec_scale) ))" + ;; + *) + echo "$(( "$1" * 1000000000 ))" + ;; + esac +} + +ATX_RASPI_PULSE_MIN="${ATX_RASPI_PULSE_MIN:-0.2}" +ATX_RASPI_PULSE_MAX="${ATX_RASPI_PULSE_MAX:-0.6}" + +REBOOTPULSEMINIMUM="$(f2nsec "$ATX_RASPI_PULSE_MIN")" #reboot pulse signal should be at least this long +REBOOTPULSEMAXIMUM="$(f2nsec "$ATX_RASPI_PULSE_MAX")" #reboot pulse signal should be at most this long + +#This is GPIO 7 (pin 26 on the pinout diagram). +#This is an input from ATXRaspi to the Pi. +#When button is held for ~3 seconds, this pin will become HIGH signaling to this script to poweroff the Pi. +SHUTDOWN="${ATX_RASPI_SHUTDOWN_PIN:-7}" + +#Added reboot feature (with ATXRaspi R2.6 (or ATXRaspi 2.5 with blue dot on chip) +#Hold ATXRaspi button for at least 500ms but no more than 2000ms and a reboot HIGH pulse of 500ms length will be issued +#This is GPIO 8 (pin 24 on the pinout diagram). +#This is an output from Pi to ATXRaspi and signals that the Pi has booted. +#This pin is asserted HIGH as soon as this script runs (by writing "1" to /sys/class/gpio/gpio8/value) +BOOT="${ATX_RASPI_BOOT_PIN:-8}" + +CHIP="${ATX_RASPI_CHIP:-/dev/gpiochip0}" + +failed='' + +if [ "$REBOOTPULSEMINIMUM" -le 0 ]; then + failed="${failed:+"${failed}, "}ATX_RASPI_PULSE_MIN (${ATX_RASPI_PULSE_MIN}) must be greater than 0" +fi + +if [ "$REBOOTPULSEMAXIMUM" -le "$REBOOTPULSEMINIMUM" ]; then + failed="${failed:+"${failed}, "}ATX_RASPI_PULSE_MAX (${ATX_RASPI_PULSE_MAX}) must be greater than ATX_RASPI_PULSE_MIN (${ATX_RASPI_PULSE_MIN})" +fi + +if [ "$SHUTDOWN" -lt 0 ]; then + failed="${failed:+"${failed}, "}ATX_RASPI_SHUTDOWN_PIN (${SHUTDOWN}) must be greater than 0" +fi + +if [ "$BOOT" -lt 0 ]; then + failed="${failed:+"${failed}, "}ATX_RASPI_BOOT_PIN (${BOOT}) must be greater than 0" +fi + +if [ "$SHUTDOWN" -eq "$BOOT" ]; then + failed="${failed:+"${failed}, "}ATX_RASPI_SHUTDOWN_PIN (${SHUTDOWN}) must be distinct from ATX_RASPI_BOOT_PIN (${BOOT})" +fi + +if [ -n "${failed:-}" ]; then + echo 1>&2 "ERROR: $failed" + exit 1 +fi + +if [ -n "${ATX_RASPI_DRY_RUN:-}" ]; then + handle_press() { + echo "[dry-run] $*" + } +else + handle_press() { + "$@" + } +fi + +if { command -v gpioset && command -v gpiomon ; } 1>/dev/null 2>&1; then + init_shutdown_pin() { + # NOP + : + } + + init_boot_pin() { + gpioset -z -b pull-up -t 0 -c "${CHIP?}" "${BOOT?}=1" + } + + watch_shutdown_pin() { + gpiomon -c "${CHIP?}" -F '%e %S' "${SHUTDOWN?}" | while read -r event seconds; do + case "$event" in + # Rising + 1) + pulseStart="$(f2nsec "$seconds")" + ;; + # Falling + 2) + pulseEnd="$(f2nsec "$seconds")" + pulseStart="${pulseStart:-"$pulseEnd"}" + pulseDuration="$(( pulseEnd - pulseStart ))" + + if [ "$pulseDuration" -gt "$REBOOTPULSEMAXIMUM" ]; then + header 12 "SHUTDOWN request on chip ${CHIP?} from GPIO${SHUTDOWN}, halting Rpi ..." + handle_press poweroff + return + elif [ "$pulseDuration" -gt "$REBOOTPULSEMINIMUM" ]; then + header 12 "REBOOT request on chip ${CHIP?} from GPIO${SHUTDOWN?}, recycling Rpi ..." + handle_press reboot + return + else + unset pulseStart pulseEnd + fi + ;; + *) + echo 1>&2 "WARNING: unrecognized event '${event}' on chip ${CHIP?} for GPIO${SHUTDOWN?}; ignoring." + ;; + esac + done + } +elif [ -e /sys/class/gpio/export ]; then + init_shutdown_pin() { + echo "${SHUTDOWN?}" > /sys/class/gpio/export + echo in > "/sys/class/gpio/gpio${SHUTDOWN?}/direction" + } + + init_boot_pin() { + echo "${BOOT?}" > /sys/class/gpio/export + echo out > "/sys/class/gpio/gpio${BOOT?}/direction" + echo 1 > "/sys/class/gpio/gpio${BOOT?}/value" + } + + watch_shutdown_pin() { + #This loop continuously checks if the shutdown button was pressed on + #ATXRaspi (GPIO7 to become HIGH), and issues a shutdown when that happens. + #It sleeps as long as that has not happened. + while true; do + shutdownSignal="$(cat "/sys/class/gpio/gpio${SHUTDOWN}/value")" + if [ "$shutdownSignal" = 0 ]; then + sleep 0.2 + else + pulseStart="$(date +%s%N)" # mark the time when Shutoff signal went HIGH (milliseconds since epoch) + while [ "$shutdownSignal" = 1 ]; do + sleep 0.02 + if [ "$(( "$(date +%s%N)" - pulseStart ))" -gt "$REBOOTPULSEMAXIMUM" ]; then + header 12 "SHUTDOWN request from GPIO${SHUTDOWN}, halting Rpi ..." + handle_press poweroff + return + fi + shutdownSignal="$(cat "/sys/class/gpio/gpio${SHUTDOWN}/value")" + done + #pulse went LOW, check if it was long enough, and trigger reboot + if [ "$(( "$(date +%s%N)" - pulseStart ))" -gt "$REBOOTPULSEMINIMUM" ]; then + header 12 "REBOOT request from GPIO${SHUTDOWN}, recycling Rpi ..." + handle_press reboot + return + fi + fi + done + } +else + # shellcheck disable=SC2016 + echo 1>&2 'FATAL: missing `libgpiod` tools (`gpioset`, `gpiomon`), and legacy sysfs GPIO interface is unavailable; terminating.' + exit 1 +fi + +init_shutdown_pin +init_boot_pin + +header 3 \ + "ATXRaspi shutdown POLLING script started: asserted pins ($SHUTDOWN=input,LOW; $BOOT=output,HIGH)" \ + "Waiting for GPIO$SHUTDOWN to become HIGH (short HIGH pulse=REBOOT, long pulse HIGH=SHUTDOWN)..." + +watch_shutdown_pin diff --git a/shutdowncheckOpenElec.sh b/shutdowncheckOpenElec.sh new file mode 120000 index 0000000..cfe9e86 --- /dev/null +++ b/shutdowncheckOpenElec.sh @@ -0,0 +1 @@ +shutdowncheck.sh \ No newline at end of file diff --git a/shutdownchecksetup.sh b/shutdownchecksetup.sh index 67dd567..d5d9994 100644 --- a/shutdownchecksetup.sh +++ b/shutdownchecksetup.sh @@ -1,130 +1,168 @@ -#!/bin/bash - -OPTION=$(whiptail --title "ATXRaspi/MightyHat shutdown/reboot script setup" --menu "\nChoose your script type option below:\n\n(Note: changes require reboot to take effect)" 15 78 4 \ -"1" "Install INTERRUPT based script /etc/shutdownirq.py (recommended)" \ -"2" "Install POLLING based script /etc/shutdowncheck.sh (classic)" \ -"3" "Disable any existing shutdown script" 3>&1 1>&2 2>&3) - -exitstatus=$? -if [ $exitstatus = 0 ]; then - sudo sed -e '/shutdown/ s/^#*/#/' -i /etc/rc.local - - if [ $OPTION = 1 ]; then - echo '#!/usr/bin/python -# ATXRaspi/MightyHat interrupt based shutdown/reboot script -# Script by Tony Pottier, Felix Rusu -import RPi.GPIO as GPIO -import os -import sys -import time - -GPIO.setmode(GPIO.BCM) - -pulseStart = 0.0 -REBOOTPULSEMINIMUM = 0.2 #reboot pulse signal should be at least this long (seconds) -REBOOTPULSEMAXIMUM = 1.0 #reboot pulse signal should be at most this long (seconds) -SHUTDOWN = 7 #GPIO used for shutdown signal -BOOT = 8 #GPIO used for boot signal - -# Set up GPIO 8 and write that the PI has booted up -GPIO.setup(BOOT, GPIO.OUT, initial=GPIO.HIGH) - -# Set up GPIO 7 as interrupt for the shutdown signal to go HIGH -GPIO.setup(SHUTDOWN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) - -print("\n==========================================================================================") -print(" ATXRaspi shutdown IRQ script started: asserted pins (",SHUTDOWN, "=input,LOW; ",BOOT,"=output,HIGH)") -print(" Waiting for GPIO", SHUTDOWN, "to become HIGH (short HIGH pulse=REBOOT, long HIGH=SHUTDOWN)...") -print("==========================================================================================") -try: - while True: - GPIO.wait_for_edge(SHUTDOWN, GPIO.RISING) - shutdownSignal = GPIO.input(SHUTDOWN) - pulseStart = time.time() #register time at which the button was pressed - while shutdownSignal: - time.sleep(0.2) - if(time.time() - pulseStart >= REBOOTPULSEMAXIMUM): - print("\n=====================================================================================") - print(" SHUTDOWN request from GPIO", SHUTDOWN, ", halting Rpi ...") - print("=====================================================================================") - os.system("sudo poweroff") - sys.exit() - shutdownSignal = GPIO.input(SHUTDOWN) - if time.time() - pulseStart >= REBOOTPULSEMINIMUM: - print("\n=====================================================================================") - print(" REBOOT request from GPIO", SHUTDOWN, ", recycling Rpi ...") - print("=====================================================================================") - os.system("sudo reboot") - sys.exit() - if GPIO.input(SHUTDOWN): #before looping we must make sure the shutdown signal went low - GPIO.wait_for_edge(SHUTDOWN, GPIO.FALLING) -except: - pass -finally: - GPIO.cleanup()' > /etc/shutdownirq.py - sudo sed -i '$ i python /etc/shutdownirq.py &' /etc/rc.local - elif [ $OPTION = 2 ]; then - echo '#!/bin/bash -# ATXRaspi/MightyHat interrupt based shutdown/reboot script -# Script by Felix Rusu - -#This is GPIO 7 (pin 26 on the pinout diagram). -#This is an input from ATXRaspi to the Pi. -#When button is held for ~3 seconds, this pin will become HIGH signalling to this script to poweroff the Pi. -SHUTDOWN=7 -REBOOTPULSEMINIMUM=200 #reboot pulse signal should be at least this long -REBOOTPULSEMAXIMUM=600 #reboot pulse signal should be at most this long -echo "$SHUTDOWN" > /sys/class/gpio/export -echo "in" > /sys/class/gpio/gpio$SHUTDOWN/direction -#Added reboot feature (with ATXRaspi R2.6 (or ATXRaspi 2.5 with blue dot on chip) -#Hold ATXRaspi button for at least 500ms but no more than 2000ms and a reboot HIGH pulse of 500ms length will be issued -#This is GPIO 8 (pin 24 on the pinout diagram). -#This is an output from Pi to ATXRaspi and signals that the Pi has booted. -#This pin is asserted HIGH as soon as this script runs (by writing "1" to /sys/class/gpio/gpio8/value) -BOOT=8 -echo "$BOOT" > /sys/class/gpio/export -echo "out" > /sys/class/gpio/gpio$BOOT/direction -echo "1" > /sys/class/gpio/gpio$BOOT/value - -echo -e "\n==========================================================================================" -echo " ATXRaspi shutdown POLLING script started: asserted pins ($SHUTDOWN=input,LOW; $BOOT=output,HIGH)" -echo " Waiting for GPIO$SHUTDOWN to become HIGH (short HIGH pulse=REBOOT, long HIGH=SHUTDOWN)..." -echo "==========================================================================================" - -#This loop continuously checks if the shutdown button was pressed on ATXRaspi (GPIO7 to become HIGH), and issues a shutdown when that happens. -#It sleeps as long as that has not happened. -while [ 1 ]; do - shutdownSignal=$(cat /sys/class/gpio/gpio$SHUTDOWN/value) - if [ $shutdownSignal = 0 ]; then - /bin/sleep 0.2 - else - pulseStart=$(date +%s%N | cut -b1-13) # mark the time when Shutoff signal went HIGH (milliseconds since epoch) - while [ $shutdownSignal = 1 ]; do - /bin/sleep 0.02 - if [ $(($(date +%s%N | cut -b1-13)-$pulseStart)) -gt $REBOOTPULSEMAXIMUM ]; then - echo -e "\n=====================================================================================" - echo " SHUTDOWN request from GPIO", SHUTDOWN, ", halting Rpi ..." - echo "=====================================================================================" - sudo poweroff - exit - fi - shutdownSignal=$(cat /sys/class/gpio/gpio$SHUTDOWN/value) - done - #pulse went LOW, check if it was long enough, and trigger reboot - if [ $(($(date +%s%N | cut -b1-13)-$pulseStart)) -gt $REBOOTPULSEMINIMUM ]; then - echo -e "\n=====================================================================================" - echo " REBOOT request from GPIO", SHUTDOWN, ", recycling Rpi ..." - echo "=====================================================================================" - sudo reboot - exit +#!/bin/sh + +set -eu + +SHUTDOWNCHECK_OWNER="${SHUTDOWNCHECK_OWNER:-LowPowerLab}" +SHUTDOWNCHECK_REPO="${SHUTDOWNCHECK_REPO:-ATX-Raspi}" +SHUTDOWNCHECK_REV="${SHUTDOWNCHECK_REV:-master}" +SHUTDOWNCHECK_BASEURL="${SHUTDOWNCHECK_BASEURL:-"https://raw.githubusercontent.com/${SHUTDOWNCHECK_OWNER}/${SHUTDOWNCHECK_REPO}/${SHUTDOWNCHECK_REV}"}" + +# We provide a fallback in case `EUID` is not set; silence shellcheck violation +# warning that "In POSIX sh, EUID is undefined" +# shellcheck disable=SC3028 +if [ "${EUID:-$(id -u 2> /dev/null || :)}" != 0 ]; then + run_as_root() { + sudo "$@" + } +else + run_as_root() { + "$@" + } +fi + +if command -v curl 1> /dev/null 2>&1; then + fetch() { + run_as_root curl -fsSLo "$2" "$1" + } +elif command -v wget 1> /dev/null 2>&1; then + fetch() { + run_as_root wget -o "$2" "$1" + } +else + fetch() { + echo 1>&2 "neither curl nor wget are available; cannot fetch '$1' into '$2'" + return 127 + } +fi + +install_interrupt_script() { + dest="${1:-/etc/shutdownirq.py}" + fetch "${SHUTDOWNCHECK_BASEURL}/shutdownirq.py" "$dest" + run_as_root chmod +x "$dest" + if [ -f /etc/rc.local ]; then + run_as_root sed -i "$ i python ${dest} &" /etc/rc.local fi - fi -done' > /etc/shutdowncheck.sh -sudo chmod +x /etc/shutdowncheck.sh -sudo sed -i '$ i /etc/shutdowncheck.sh &' /etc/rc.local +} + +install_polling_script() { + dest="${1:-/etc/shutdowncheck.sh}" + fetch "${SHUTDOWNCHECK_BASEURL}/shutdowncheck.sh" "$dest" + run_as_root chmod +x "$dest" + if [ -f /etc/rc.local ]; then + run_as_root sed -i "\$ i ${dest} &" /etc/rc.local fi - - echo "You chose option" $OPTION ": All done!" +} + +looks_like_elec_distro() { + # shellcheck disable=SC1091 + . /etc/os-release 1> /dev/null 2>&1 || : + + case "${NAME:-}" in + *ELEC) + return 0 + ;; + esac + + case "${ID:-}" in + *elec) + return 0 + ;; + esac + + # shellcheck disable=SC3028 + # *ELEC distros: + # 1. Use `root` for shell sessions, + # 2. Have the directory `/storage/.config`, and + # 3. Use a `sudo` wrapper that issues a warning about not needing sudo + # and then exits with a non-zero status. + [ "${EUID:-$(id -u 2> /dev/null || :)}" -eq 0 ] && [ -d /storage/.config ] && ! sudo true +} + +if looks_like_elec_distro; then + dest="${SHUTDOWNCHECK_POLLING_DEST:-${SHUTDOWNCHECK_DEST:-/storage/.config/shutdowncheck.sh}}" + install_polling_script "$dest" + run_as_root "${SHELL:-/bin/sh}" "-$-" -c " +echo '#!/bin/sh +( +${dest} +)&' > /storage/.config/autostart.sh +" + chmod 777 /storage/.config/autostart.sh + chmod 777 "$dest" + exit +fi + +if command -v whiptail 1> /dev/null 2>&1; then + get_script_type() { + whiptail --title "ATXRaspi/MightyHat shutdown/reboot script setup" --menu "\nChoose your script type option below:\n\n(Note: changes require reboot to take effect)" 15 78 4 \ + "1" "Install INTERRUPT based script /etc/shutdownirq.py (recommended)" \ + "2" "Install POLLING based script /etc/shutdowncheck.sh (classic)" \ + "3" "Disable any existing shutdown script" 3>&1 1>&2 2>&3 + } +elif (help select) 1> /dev/null 2>&1; then + # Eval this, as otherwise we're liable to get a syntax error from shells that + # do not understand `select ...; do ...; done` + eval ' + get_script_type() { + echo 1>&2 "ATXRaspi/MightyHat shutdown/reboot script setup" + echo 1>&2 "Choose your script type option below (note: changes require reboot to take effect)" + + # shellcheck disable=SC3043 + local PS3="Choose your script type option: " + + # shellcheck disable=SC3008 + select CHOICE in "Install INTERRUPT based script /etc/shutdownirq.py (recommended)" \ + "Install POLLING based script /etc/shutdowncheck.sh (classic)" \ + "Disable any existing shutdown script"; do + if [ -n "${CHOICE:-}" ]; then + echo "$REPLY" + break + else + echo 1>&2 "Not a valid choice: ${REPLY}" + fi + done + } + ' +else + get_script_type() { + echo 1>&2 'ATXRaspi/MightyHat shutdown/reboot script setup' + echo 1>&2 "Choose your script type option below (note: changes require reboot to take effect)" + echo 1>&2 "\ +1) Install INTERRUPT based script /etc/shutdownirq.py (recommended) +2) Install POLLING based script /etc/shutdowncheck.sh (classic) +3) Disable any existing shutdown script" + + while true; do + echo 1>&2 'Choose your script type option: ' + + if read -r REPLY; then + case "${REPLY:-}" in + 1 | 2 | 3) + echo "$REPLY" + return + ;; + *) + echo 1>&2 "Not a valid choice: ${REPLY}" + ;; + esac + fi + done + } +fi + +if OPTION="$(get_script_type)"; then + run_as_root sed -e '/shutdown/ s/^#*/#/' -i /etc/rc.local + + case "$OPTION" in + 1) + install_interrupt_script "${SHUTDOWNCHECK_INTERRUPT_DEST:-${SHUTDOWNCHECK_DEST:-}}" + ;; + 2) + install_polling_script "${SHUTDOWNCHECK_POLLING_DEST:-${SHUTDOWNCHECK_DEST:-}}" + ;; + esac + + echo "You chose option ${OPTION}: All done!" else echo "Shutdown/Reboot script setup was aborted." fi diff --git a/shutdownchecksetupOpenElec.sh b/shutdownchecksetupOpenElec.sh deleted file mode 100644 index ff8c728..0000000 --- a/shutdownchecksetupOpenElec.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/sh -echo '#!/bin/bash - -#This is GPIO 7 (pin 26 on the pinout diagram). -#This is an input from ATXRaspi to the Pi. -#When button is held for ~3 seconds, this pin will become HIGH signalling to this script to poweroff the Pi. -SHUTDOWN=7 -REBOOTPULSEMINIMUM=200 #reboot pulse signal should be at least this long -REBOOTPULSEMAXIMUM=600 #reboot pulse signal should be at most this long -echo "$SHUTDOWN" > /sys/class/gpio/export -echo "in" > /sys/class/gpio/gpio$SHUTDOWN/direction - -#Added reboot feature (with ATXRaspi R2.6 (or ATXRaspi 2.5 with blue dot on chip) -#Hold ATXRaspi button for at least 500ms but no more than 2000ms and a reboot HIGH pulse of 500ms length will be issued - -#This is GPIO 8 (pin 24 on the pinout diagram). -#This is an output from Pi to ATXRaspi and signals that the Pi has booted. -#This pin is asserted HIGH as soon as this script runs (by writing "1" to /sys/class/gpio/gpio8/value) -BOOT=8 -echo "$BOOT" > /sys/class/gpio/export -echo "out" > /sys/class/gpio/gpio$BOOT/direction -echo "1" > /sys/class/gpio/gpio$BOOT/value - -echo "ATXRaspi shutdown script started: asserted pins ($SHUTDOWN=input,LOW; $BOOT=output,HIGH). Waiting for GPIO$SHUTDOWN to become HIGH..." - -#This loop continuously checks if the shutdown button was pressed on ATXRaspi (GPIO7 to become HIGH), and issues a shutdown when that happens. -#It sleeps as long as that has not happened. -while [ 1 ]; do - shutdownSignal=$(cat /sys/class/gpio/gpio$SHUTDOWN/value) - if [ $shutdownSignal = 0 ]; then - /bin/sleep 0.2 - else - pulseStart=$(date +%s%N | cut -b1-13) # mark the time when Shutoff signal went HIGH (milliseconds since epoch) - while [ $shutdownSignal = 1 ]; do - /bin/sleep 0.02 - if [ $(($(date +%s%N | cut -b1-13)-$pulseStart)) -gt $REBOOTPULSEMAXIMUM ]; then - echo "ATXRaspi triggered a shutdown signal, halting Rpi ... " - poweroff - exit - fi - shutdownSignal=$(cat /sys/class/gpio/gpio$SHUTDOWN/value) - done - #pulse went LOW, check if it was long enough, and trigger reboot - if [ $(($(date +%s%N | cut -b1-13)-$pulseStart)) -gt $REBOOTPULSEMINIMUM ]; then - echo "ATXRaspi triggered a reboot signal, recycling Rpi ... " - reboot - exit - fi - fi -done' > /storage/.config/shutdowncheck.sh -echo '#!/bin/bash -( -/storage/.config/shutdowncheck.sh -)&' > /storage/.config/autostart.sh -chmod 777 /storage/.config/autostart.sh -chmod 777 /storage/.config/shutdowncheck.sh \ No newline at end of file diff --git a/shutdownchecksetupOpenElec.sh b/shutdownchecksetupOpenElec.sh new file mode 120000 index 0000000..27c0bc0 --- /dev/null +++ b/shutdownchecksetupOpenElec.sh @@ -0,0 +1 @@ +shutdownchecksetup.sh \ No newline at end of file diff --git a/shutdownirq.py b/shutdownirq.py new file mode 100755 index 0000000..db863b6 --- /dev/null +++ b/shutdownirq.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python + +# ATXRaspi/MightyHat interrupt based shutdown/reboot script +# Script by Tony Pottier, Felix Rusu + +import os +import sys +import time + +# Reboot pulse signal should be at least this long (seconds). +REBOOTPULSEMINIMUM = float(os.environ.get("ATX_RASPI_PULSE_MIN", 0.2)) + +# Reboot pulse signal should be at most this long (seconds). +REBOOTPULSEMAXIMUM = float(os.environ.get("ATX_RASPI_PULSE_MAX", 1.0)) + +# GPIO pin used for shutdown signal. +SHUTDOWN = int(os.environ.get("ATX_RASPI_SHUTDOWN_PIN", 7)) + +# GPIO pin used for boot signal. +BOOT = int(os.environ.get("ATX_RASPI_BOOT_PIN", 8)) + +failed = [msg for (cond, msg) in [ + (REBOOTPULSEMINIMUM > 0, "ATX_RASPI_PULSE_MIN ({0}) must be greater than 0".format(REBOOTPULSEMINIMUM)), + (REBOOTPULSEMAXIMUM > REBOOTPULSEMINIMUM, "ATX_RASPI_PULSE_MAX ({0} must be greater than ATX_RASPI_PULSE_MIN ({1})".format(REBOOTPULSEMAXIMUM, REBOOTPULSEMINIMUM)), + (SHUTDOWN >= 0, "ATX_RASPI_SHUTDOWN_PIN ({0}) must be greater than 0".format(SHUTDOWN)), + (BOOT >= 0, "ATX_RASPI_BOOT_PIN ({0}) must be greater than 0".format(BOOT)), + (SHUTDOWN != 0, "ATX_RASPI_SHUTDOWN_PIN ({0}) must be distinct from ATX_RASPI_BOOT_PIN ({1})".format(SHUTDOWN, BOOT)), +] if not cond] + +if len(failed) > 0: + raise ValueError(", ".join(failed)) + +if os.environ.get("ATX_RASPI_DRY_RUN", "") == "": + def handle_press(*command): + os.system(*command) + sys.exit() +else: + def handle_press(*command): + print("[dry-run] ", " ".join(command)) + sys.exit() + +if os.environ.get("ATX_RASPI_DRY_RUN", "") == "": + def handle_press(*command): + os.system(*command) + sys.exit() +else: + def handle_press(*command): + print("[dry-run] ", " ".join(command)) + sys.exit() + +def diag(*msgs): + linelen = max([len(msg) for msg in msgs]) + 2 + wrapper = "=" * linelen + print(wrapper) + for msg in msgs: + print(" {0}".format(msg)) + print(wrapper) + +def announce(): + print() + diag( + "ATXRaspi shutdown IRQ script started: asserted pins ({0}=input,LOW; {1}=output,HIGH)".format(SHUTDOWN, BOOT), + "Waiting for GPIO {0} to become HIGH (short HIGH pulse=REBOOT, long HIGH pulse=SHUTDOWN)...".format(SHUTDOWN), + ) + +try: + import gpiod + from gpiod.line import Direction, Edge, Value + have_gpiod = True +except ImportError: + have_gpiod = False + +if have_gpiod: + + CHIP = os.environ.get("ATX_RASPI_CHIP", "/dev/gpiochip0") + CONSUMER = "atx-raspi" + CONFIG = { + SHUTDOWN: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.BOTH), + BOOT: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE) + } + + pulse_start = None + + with gpiod.request_lines(CHIP, consumer=CONSUMER, config=CONFIG) as request: + + announce() + + try: + while True: + for event in request.read_edge_events(): + if event.event_type is event.Type.RISING_EDGE: + pulse_start = time.time() + elif event.event_type is event.Type.FALLING_EDGE: + pulse_end = time.time() + + if pulse_start is None: + pulse_start = pulse_end + + pulse_duration = pulse_end - pulse_start + + if pulse_duration >= REBOOTPULSEMAXIMUM: + print() + diag("SHUTDOWN request on chip {0} from GPIO{1}, halting Rpi ...".format(CHIP, SHUTDOWN)) + handle_press("poweroff") + sys.exit() + elif pulse_duration >= REBOOTPULSEMINIMUM: + print() + diag("REBOOT request on chip {0} from GPIO{1}, recycling Rpi ...".format(CHIP, SHUTDOWN)) + handle_press("reboot") + sys.exit() + else: + pulse_start = None + pulse_end = None + except Exception: + pass +else: + import RPi.GPIO as GPIO + + # Set up GPIO 8 and write that the PI has booted up + GPIO.setup(BOOT, GPIO.OUT, initial=GPIO.HIGH) + + # Set up GPIO 7 as interrupt for the shutdown signal to go HIGH + GPIO.setup(SHUTDOWN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + + announce() + + try: + while True: + GPIO.wait_for_edge(SHUTDOWN, GPIO.RISING) + shutdown_signal = GPIO.input(SHUTDOWN) + pulse_start = time.time() # register time at which the button was pressed + while shutdown_signal: + time.sleep(0.2) + if(time.time() - pulse_start >= REBOOTPULSEMAXIMUM): + print() + diag("SHUTDOWN request from GPIO{0}, halting Rpi ...".format(SHUTDOWN)) + handle_press("poweroff") + sys.exit() + shutdown_signal = GPIO.input(SHUTDOWN) + if time.time() - pulse_start >= REBOOTPULSEMINIMUM: + print() + diag("REBOOT request from GPIO{0}, recycling Rpi ...".format(SHUTDOWN)) + handle_press("reboot") + sys.exit() + if GPIO.input(SHUTDOWN): # before looping we must make sure the shutdown signal went low + GPIO.wait_for_edge(SHUTDOWN, GPIO.FALLING) + except Exception: + pass + finally: + GPIO.cleanup()