Last active
November 16, 2022 10:21
-
-
Save lud/6be0dfe6b52932e486d1aca0986dcb02 to your computer and use it in GitHub Desktop.
Heat the computer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env elixir | |
Mix.install([:jason]) | |
defmodule Heater do | |
import Kernel, except: [spawn_link: 1] | |
def spawn_link() do | |
Kernel.spawn_link(fn -> init() end) | |
end | |
defp init() do | |
send(self(), :heat) | |
loop_idle() | |
end | |
defp loop_heating(tasks) do | |
receive do | |
:heat -> loop_heating(tasks) | |
:stop -> end_tasks(tasks) | |
end | |
end | |
defp loop_idle() do | |
receive do | |
:heat -> loop_heating(start_tasks()) | |
:stop -> :ok | |
end | |
end | |
defp start_tasks() do | |
_tasks = for _ <- 1..System.schedulers_online(), do: Task.async(&infinite_task/0) | |
end | |
defp end_tasks(tasks) do | |
for t <- tasks, do: Task.shutdown(t) | |
end | |
defp infinite_task() do | |
infinite_task([0]) | |
end | |
defp infinite_task(hash) do | |
hash |> :erlang.md5() |> infinite_task() | |
end | |
end | |
defmodule Control do | |
def run(max) do | |
{pid, ref} = spawn_monitor(fn -> init(max) end) | |
receive do | |
{:DOWN, ^ref, :process, ^pid, 0} -> | |
_exit_code = 0 | |
{:DOWN, ^ref, :process, ^pid, 1} -> | |
IO.puts([ | |
"error when executing powermetrics. ", | |
IO.ANSI.bright(), | |
"did you run with sudo?", | |
IO.ANSI.reset() | |
]) | |
_exit_code = 1 | |
{:DOWN, ^ref, :process, ^pid, exit_code} when is_integer(exit_code) -> | |
exit_code | |
{:DOWN, ^ref, :process, ^pid, :normal} -> | |
_exit_code = 0 | |
{:DOWN, ^ref, :process, ^pid, reason} -> | |
IO.puts([ | |
"error when executing control process: ", | |
inspect(reason) | |
]) | |
_exit_code = 1 | |
end | |
end | |
defp init(max) do | |
heater = Heater.spawn_link() | |
state = %{min_temp: nil, max_temp: max} | |
loop(state, heater) | |
end | |
defp loop(%{max_temp: max} = state, heater) do | |
temp = get_temperature() | |
state = maybe_put_min(state, temp) | |
display_temp(state, temp) | |
if temp >= max do | |
IO.puts("\nReached desired temperature") | |
terminate(heater) | |
else | |
send(heater, :heat) | |
# sleeping to not spam sensors | |
Process.sleep(1000) | |
loop(state, heater) | |
end | |
end | |
defp maybe_put_min(%{min_temp: nil} = state, temp) do | |
%{state | min_temp: temp} | |
end | |
defp maybe_put_min(%{min_temp: min} = state, temp) when temp < min do | |
%{state | min_temp: temp} | |
end | |
defp maybe_put_min(state, _) do | |
state | |
end | |
defp display_temp(%{min_temp: min, max_temp: max}, cur) do | |
domain = | |
case max - min do | |
0 -> 1 | |
d -> d | |
end | |
ratio = (cur - min) / domain | |
ratio = max(ratio, 0) | |
range = max(io_columns(), 20) | |
filled_size = trunc(range * ratio) | |
right_text = "#{trunc(ratio * 100)}% " | |
right_len = String.length(right_text) | |
left_text = "Current temperature: #{cur}°C" |> String.slice(0, range - right_len) | |
left_len = String.length(left_text) | |
spacing = range - right_len - left_len | |
message = | |
[left_text, List.duplicate(" ", spacing), right_text] | |
|> :erlang.iolist_to_binary() | |
{filled_text, unfilled_text} = String.split_at(message, filled_size) | |
IO.write([ | |
"\r", | |
IO.ANSI.inverse(), | |
filled_text, | |
IO.ANSI.reset(), | |
unfilled_text | |
]) | |
end | |
defp io_columns() do | |
case :io.columns() do | |
{:ok, v} -> v | |
_ -> 80 | |
end | |
end | |
defp terminate(heater) do | |
ref = Process.monitor(heater) | |
IO.puts("Unlinking heater") | |
Process.unlink(heater) | |
send(heater, :stop) | |
receive do | |
{:DOWN, ^ref, :process, ^heater, :normal} -> exit(0) | |
{:DOWN, ^ref, :process, ^heater, reason} -> exit(reason) | |
end | |
end | |
defp get_temperature() do | |
case System.shell("sensors -j 2>/dev/null") do | |
{json, 0} -> json |> Jason.decode!() |> read_metrics() | |
{_, n} -> exit(n) | |
end | |
end | |
defp read_metrics(%{"acpitz-acpi-0" => temps}) do | |
temps = | |
for {"temp" <> n, vals} <- temps do | |
_temp = Map.fetch!(vals, "temp#{n}_input") | |
end | |
sum = Enum.reduce(temps, &Kernel.+/2) | |
avg = sum / length(temps) | |
avg | |
end | |
end | |
degrees = | |
case System.argv() do | |
[] -> | |
50 | |
[arg | _] -> | |
case Integer.parse(arg) do | |
{deg, ""} -> | |
deg | |
_ -> | |
IO.puts("Could not parse '#{arg}'") | |
System.stop() | |
end | |
end | |
IO.puts("Target temperature: #{degrees}°C") | |
Control.run(degrees) | |
|> System.stop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment