├── .gitignore ├── nix ├── overlays │ └── default.nix ├── pkgs │ └── argonone-utils.nix └── hardware │ ├── i2c.nix │ └── power-button.nix ├── go.mod ├── shell.nix ├── go.sum ├── LICENSE ├── cmd └── argonone-fancontrold │ └── main.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | -------------------------------------------------------------------------------- /nix/overlays/default.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | { 3 | argonone-fancontrold = super.callPackage ../pkgs/argonone-fancontrold.nix {}; 4 | } 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mgdm/argonone-utils 2 | 3 | go 1.16 4 | 5 | require ( 6 | periph.io/x/conn/v3 v3.6.8 7 | periph.io/x/host/v3 v3.7.0 8 | ) 9 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | 3 | with pkgs; 4 | 5 | mkShell { 6 | buildInputs = [ 7 | git 8 | go 9 | gotools 10 | gopls 11 | go-outline 12 | gocode 13 | gopkgs 14 | gocode-gomod 15 | godef 16 | golint 17 | ]; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /nix/pkgs/argonone-utils.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | pkgs.buildGoModule rec { 4 | name = "argonone-fancontrold"; 5 | version = "0.1.0"; 6 | 7 | src = pkgs.fetchFromGitHub { 8 | owner = "mgdm"; 9 | repo = "argonone-utils"; 10 | rev = "main"; 11 | sha256 = "1cf544i24zbgprfj51yli2l3fwalcsb2vb5p7r257k73q457hgzc"; 12 | }; 13 | 14 | vendorSha256 = "18qwmg249lr7xb9f7lrrflhsr7drx750ndqd8hirq5hgj4c4f66k"; 15 | } 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | periph.io/x/conn/v3 v3.6.8 h1:fnNSwSoKPzpoLOSxml70EInaP6YrrqcucP3KDfNxpmU= 2 | periph.io/x/conn/v3 v3.6.8/go.mod h1:3OD27w9YVa5DS97VsUxsPGzD9Qrm5Ny7cF5b6xMMIWg= 3 | periph.io/x/d2xx v0.0.1 h1:7iCO/aVK6k9GSZ45DcpMU8sOOcVLCqO/cheSm7nMUG0= 4 | periph.io/x/d2xx v0.0.1/go.mod h1:38Euaaj+s6l0faIRHh32a+PrjXvxFTFkPBEQI0TKg34= 5 | periph.io/x/host/v3 v3.7.0 h1:9CP/j0FcJmR+PRHlNzAmhV6Mt3GXoWnPmRhknJlQhnE= 6 | periph.io/x/host/v3 v3.7.0/go.mod h1:okb5m0yUYLTM/dnMYWMBX47w4owTzyCPLpZUQb35nhs= 7 | -------------------------------------------------------------------------------- /nix/hardware/i2c.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let cfg = config.hardware.raspberry-pi."4".i2c-bcm2708; 4 | in { 5 | options.hardware = { 6 | raspberry-pi."4".i2c-bcm2708 = { 7 | enable = lib.mkEnableOption '' 8 | Enable the Raspberry Pi 4 hardware i2c controller. 9 | ''; 10 | }; 11 | }; 12 | 13 | config = lib.mkIf cfg.enable { 14 | hardware.deviceTree = { 15 | overlays = [ 16 | { 17 | name = "i2c0"; 18 | dtsText = '' 19 | 20 | /dts-v1/; 21 | /plugin/; 22 | 23 | /{ 24 | compatible = "raspberrypi,4-model-b"; 25 | 26 | fragment@1 { 27 | target = <&i2c1>; 28 | __overlay__ { 29 | status = "okay"; 30 | }; 31 | }; 32 | }; 33 | ''; 34 | } 35 | ]; 36 | }; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Michael Maclean 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /cmd/argonone-fancontrold/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "periph.io/x/conn/v3/driver/driverreg" 11 | "periph.io/x/conn/v3/i2c" 12 | "periph.io/x/conn/v3/i2c/i2creg" 13 | _ "periph.io/x/host/v3/bcm283x" 14 | _ "periph.io/x/host/v3/rpi" 15 | ) 16 | 17 | var temperatureBands = []map[string]int{ 18 | {"temp": 50, "speed": 10}, 19 | {"temp": 60, "speed": 50}, 20 | {"temp": 65, "speed": 100}, 21 | } 22 | 23 | func check(e error) { 24 | if e != nil { 25 | panic(e) 26 | } 27 | } 28 | 29 | func initI2C() (*i2c.Dev, error) { 30 | _, err := driverreg.Init() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | b, err := i2creg.Open("") 36 | if err != nil { 37 | return nil, fmt.Errorf("Failed to initialise I2C: %w", err) 38 | } 39 | 40 | d := &i2c.Dev{Addr: 26, Bus: b} 41 | return d, nil 42 | } 43 | 44 | func getTemperature() (int, error) { 45 | t, err := os.ReadFile("/sys/class/thermal/thermal_zone0/temp") 46 | 47 | if err != nil { 48 | return 0, err 49 | } 50 | 51 | st := strings.TrimSpace(string(t)) 52 | 53 | i, err := strconv.Atoi(st) 54 | 55 | if err != nil { 56 | return 0, err 57 | } 58 | 59 | return i / 1000, nil 60 | } 61 | 62 | func selectFanSpeed(temperature int) int { 63 | targetSpeed := 0 64 | 65 | for _, v := range temperatureBands { 66 | if temperature > v["temp"] { 67 | targetSpeed = v["speed"] 68 | } 69 | } 70 | 71 | return targetSpeed 72 | } 73 | 74 | func setFanSpeed(device *i2c.Dev, speed int) error { 75 | _, err := device.Write([]byte{byte(speed)}) 76 | return err 77 | } 78 | 79 | func monitor(device *i2c.Dev) { 80 | temp, err := getTemperature() 81 | check(err) 82 | 83 | fanSpeed := selectFanSpeed(temp) 84 | fmt.Printf("Current temperature: %d; Target fan speed: %d\n", temp, fanSpeed) 85 | 86 | setFanSpeed(device, fanSpeed) 87 | check(err) 88 | } 89 | 90 | func main() { 91 | device, err := initI2C() 92 | check(err) 93 | 94 | ticker := time.NewTicker(30 * time.Second) 95 | 96 | for range ticker.C { 97 | monitor(device) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /nix/hardware/power-button.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let cfg = config.hardware.argon-one.power-button; 4 | in { 5 | options.hardware = { 6 | argon-one.power-button = { 7 | enable = lib.mkEnableOption '' 8 | Use the Argon ONE power button to shut down the machine 9 | ''; 10 | }; 11 | }; 12 | 13 | config = lib.mkIf cfg.enable { 14 | # Add a script to make the case power board turn off the power once 15 | # the Pi has shut down 16 | 17 | systemd.services.argonone-power-off = { 18 | wantedBy = [ "poweroff.target" ]; 19 | after = [ "systemd-poweroff.service" ]; 20 | 21 | script = "${pkgs.i2c-tools}/bin/i2cset -y 1 0x01a 0xff"; 22 | 23 | unitConfig = { DefaultDependencies = "no"; }; 24 | 25 | serviceConfig = { 26 | Type = "oneshot"; 27 | User = "root"; 28 | Group = "i2c"; 29 | TimeoutStartSec = "0"; 30 | }; 31 | }; 32 | 33 | # Add the device tree overlay to expose the power button for the gpio-keys module 34 | hardware.deviceTree = { 35 | overlays = [{ 36 | name = "power-button"; 37 | dtsText = '' 38 | /dts-v1/; 39 | /plugin/; 40 | 41 | / { 42 | compatible = "raspberrypi,4-model-b"; 43 | 44 | fragment@0 { 45 | // Configure the gpio pin controller 46 | target = <&gpio>; 47 | __overlay__ { 48 | pin_state: button_pins@0 { 49 | brcm,pins = <4>; // gpio number 50 | brcm,function = <0>; // 0 = input, 1 = output 51 | brcm,pull = <1>; // 0 = none, 1 = pull down, 2 = pull up 52 | }; 53 | }; 54 | }; 55 | fragment@1 { 56 | target-path = "/"; 57 | __overlay__ { 58 | button: button@0 { 59 | compatible = "gpio-keys"; 60 | pinctrl-names = "default"; 61 | pinctrl-0 = <&pin_state>; 62 | status = "okay"; 63 | 64 | key: key { 65 | linux,code = <116>; 66 | gpios = <&gpio 4 1>; 67 | label = "KEY_POWER"; 68 | }; 69 | }; 70 | }; 71 | }; 72 | 73 | __overrides__ { 74 | gpio = <&key>,"gpios:4", 75 | <&button>,"reg:0", 76 | <&pin_state>,"brcm,pins:0", 77 | <&pin_state>,"reg:0"; 78 | label = <&key>,"label"; 79 | keycode = <&key>,"linux,code:0"; 80 | gpio_pull = <&pin_state>,"brcm,pull:0"; 81 | active_high = <&key>,"gpios:4"; 82 | }; 83 | 84 | }; 85 | ''; 86 | }]; 87 | }; 88 | }; 89 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Argon ONE utils 2 | 3 | This repo provides two things: 4 | 5 | * A port of the logic of the [Argon ONE fan control daemon script](https://download.argon40.com/argon1.sh) to Go instead of Python. 6 | * Some systemd and device tree configuration to make the power button shut down the machine cleanly. 7 | 8 | **This probably won't work for you**. I use this on my [Raspberry Pi 4B running NixOS](https://mgdm.net/weblog/nixos-on-raspberry-pi-4/). Everything is hard coded to work on that machine with no accounting for other OSes or configurations. 9 | 10 | **There's no warranty of any kind for this.** 11 | 12 | ## I'm using NixOS! How do I set up the fan control daemon? 13 | 14 | The Argon ONE's fan is controlled using I2C. On Raspbian, enabling I2C is a simple matter of using `raspi-config`, which will add a line similar to `dtparam=i2c_arm=on` to `/boot/config.txt`, and rebooting. This doesn't work for me on NixOS even using the Raspberry Pi vendor kernel, so I had to work on a device tree overlay to enable it instead. For some reason I think the I2C bus names under NixOS are swapped, so this code works with `i2c-1` whereas on Raspbian I believe it would be `i2c-0`, though I have not tested this. 15 | 16 | The overlay looks like [nix/hardware/i2c.nix](nix/hardware/i2c.nix). Copy that somewhere under `/etc/nixos` and include that in your `configuration.nix` as follows: 17 | 18 | ```nix 19 | { 20 | imports = 21 | [ 22 | ./hardware-configuration.nix 23 | ./hardware/i2c.nix 24 | ]; 25 | 26 | # Other settings go here... 27 | 28 | hardware.raspberry-pi."4".i2c.enable = true; 29 | } 30 | ``` 31 | 32 | (This option name uses the convention of the [nixos-hardware overlay](https://github.com/NixOS/nixos-hardware/), to which I will eventually PR this). 33 | 34 | Then to configure the service: 35 | 36 | ```nix 37 | { 38 | # Import the overlay to provide the argonone-utils package 39 | nixpkgs.overlays = [ (import ./overlays) ]; 40 | 41 | systemd.services.argonone-fancontrold = { 42 | enable = true; 43 | wantedBy = [ "default.target" ]; 44 | 45 | serviceConfig = { 46 | DynamicUser = true; 47 | Group = "i2c"; 48 | 49 | ExecStart = "${pkgs.argonone-utils}/bin/argonone-fancontrold"; 50 | }; 51 | }; 52 | } 53 | ``` 54 | 55 | In `hardware-configuration.nix`, I made the following additions: 56 | 57 | ```nix 58 | { 59 | boot.initrd.kernelModules = [ "i2c-dev" "i2c-bcm2835" ]; 60 | boot.kernelModules = [ "i2c-dev" "i2c-bcm2835" ]; 61 | hardware.i2c.enable = true; # This adds the i2c group 62 | } 63 | ``` 64 | 65 | Hopefully that should be it! 66 | 67 | ## I'm using NixOS! How do I make the power button work? 68 | 69 | Most of the functionality for making the power button turn the machine off already exists in the kernel and systemd. There's no need for a script to monitor the GPIOs unless you want to do something different like a quick press for reboot or a long press for shut down. The supplied Argon ONE script monitors the GPIO at 100Hz in order to do this. 70 | 71 | I thought about rewriting this like I did with the fan control, but using interrupts, but then I got told by [@adventureloop](https://github.com/adventureloop/) about the `gpio-keys` kernel module. This needs some device tree overlays to set up on NixOS (again, it's probably simpler on Raspbian), but once the overlay is applied systemd will respond to a double tap on the power button by shutting down. This is only the first step--it doesn't turn off the power to the Pi. However, with a quick systemd one-shot unit, we can poke the right I2C command at the board to make it cut the power just after everything else is shut down. 72 | 73 | To use it: 74 | * include [power-button.nix](nix/hardware/power-button.nix) in your configuration 75 | * add `gpio-keys` to boot.kernelModules (I will add this to power-button.nix eventually) 76 | * enable it using `hardware.argon-one.power-button.enable = true;` 77 | 78 | After a rebuild, a double tap the power button should cleanly shut down and turn the machine off. 79 | --------------------------------------------------------------------------------