diff --git a/README.md b/README.md index b9468dc..903b844 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,58 @@ -# `wifi` Application +# RuhNet ESP32 `airconditioner` Control App -Welcome to the `wifi` AtomVM application. +This is an AtomVM application, written in Erlang, that controls an air conditioning unit. -The `wifi` AtomVM application demonstrates how to configure an ESP32 device for both Station (STA) and Access Point (AP) modes, allowing an ESP32 device to join an existing WiFi network or to serve as a WiFi access point for other devices. +Recently, one of my window airconditioning units started exhibiting issues (strange behavior). After a bit of diagnosis, it appeared to be the [custom] microcontroller that had failed. This particular unit has 4 relays which control the compressor (on/off) and three fan speeds. There are also two thermistors; one to monitor the air temperature for the thermostat, and another on the evaporator coil for freeze protection monitoring. +The original microcontroller was connected to a ULN2003 darlington transistor driver IC, which turned the relays on/off based on logic level outputs of the microcontroller. I removed the original microcontroller from the board, and jumpered connections from the two thermistors and the ULN2003 inputs to a multi-wire cable which originally connected the control board to a front panel display/buttons board. +That multi-wire cable now connects to my ESP32 (just a standard devkit board), which is the new "brains" of the system. -For more information about programming on the AtomVM platform, see the [AtomVM Programmers Guide](https://www.atomvm.net/doc/master/programmers-guide.html). +The ESP32 monitors the temperature of the thermistors, and periodically checks them against temperature cutoff values supplied by a thermostat process (and a fixed freeze prevention cutoff temperature for the evaporator coil sensor). -For general information about building and executing Erlang AtomVM example programs, see the Erlang example program [README](../README.md). +Most things are supervised, and auto-restarted if they crash. -> **IMPORTANT** If you are running this example program on a device that supports WiFi (e.g., the ESP32 or Pico W), you must first copy the `src/config.erl-template` file to set `src/config.erl` and edit the WiFi Access Point SSID and PSK to which the ESP32 device is to connect before building this application: +There is a 6 minute compressor delay initiated at bootup and any time the compressor has stopped, to prevent short cycling and damaging it. - sta => [ - {ssid, "my_sta_ssid"}, - {psk, "my_sta_psk"} - ] +Modes of operation are: +- `off` +- `cool` +- `energy_saver` +- `fan_only` + +The `energy_saver` mode is the same as `cool`, except the fan is turned off whenever the compressor has stopped. + +MQTT topics are subscribed to for general control of mode, fan speeds, and thermostat. The main control topic allows setting debug mode, showing memory usage, uptime, etc. The system also publishes status information, debug logging (if enabled), thermostat setpoint, etc. periodically. + +The thermostat span can be set as well, and allows you to set the temperature swing between turning the compressor on and off. Small span values (less than 3) are split in half, whereas values larger than that are biased 75% toward the cool side. + +Example 1: +- thermostat `temp` value set to 70 degrees +- thermostat `span` set to 2 degrees +- when temperature reaches 71 degrees, compressor will engage +- compressor will stay on until temperature drops to 69 degrees + +Example 2: +- thermostat `temp` value set to 70 degrees +- thermostat `span` set to 8 degrees +- when temperature reaches 72 degrees (25% of span value), compressor will engage +- compressor will stay on until temperature drops to 64 degrees (six degrees is 75% of the span value of 8 degrees). + +The thermostat temperature and span can be saved into NVS (non volatile storage) by sending it a `save` command. + +In addition to using the built-in thermistor as the source for the temperature to compare with the thermostat setting, you can set a remote MQTT topic to subscribe to for temperature readings. I have a small ESP8266 temp sensor that publishes readings to MQTT, so I can place it in an adjoining room and the AC will keep the temperature regulated in the other room adjacent to where the unit actually is. Send `temp_source {TOPIC}` on the main control channel to set a remote temperature feed. This setting is saved in NVS. To revert back to the internal thermistor measurement, send `temp_source local`. + +You can also set timers for actions to be executed after a delay: publishing `timer cool 2h` on the main control topic turns the unit on cool mode in 2 hours from now; publishing `timer 69 15m` on the thermostat topic will set the thermostat to 69 degrees (F) in 15 minutes. + +There is also a generic output, controlled by the {DEVICENAME}/out1 feed, which I have connected to a relay module that turns on/off some extra house fans external to the AC unit. Timers work for that output as well. + +> Set your WiFi values in `src/config.erl-template` and rename it `src/config.erl`. + +> The `src/app.hrl` file contains defines for pin assignments and such. (You can add additional pins for generic outputs and they should work without other code modifications, etc.) + +On my particular unit I left the I2C pins [default 21,22] unused, in case I later want to add an I2C temp sensor. Since the thermistors are read using the ADC on the ESP32, *you must compile AtomVM with the ADC driver.* + +### TODO/Potential Feature Additions +- Save state in NVS, so that the mode state can be resumed after a power loss. (?) +- HTTP server that allows changing configuration options such as the WiFi client settings, MQTT broker, etc. +- HTTP server that allows setting mode/thermostat/etc. without MQTT. +- Button reading input for changing mode/thermostat. +- LCD Display diff --git a/rebar.config b/rebar.config index 1715fe7..498b962 100644 --- a/rebar.config +++ b/rebar.config @@ -1,5 +1,6 @@ {erl_opts, [debug_info]}. {deps, [ + {atomvm_lib, {git, "https://github.com/atomvm/atomvm_lib.git", {branch, "master"}}}, {mqtt_client, {git, "https://github.com/atomvm/atomvm_mqtt_client.git", {branch, "master"}}}, {atomvm_adc, {git, "https://github.com/atomvm/atomvm_adc.git", {branch, "master"}}} ]}. diff --git a/src/airconditioner.erl b/src/airconditioner.erl index fbaa2f3..8ea7846 100644 --- a/src/airconditioner.erl +++ b/src/airconditioner.erl @@ -33,20 +33,22 @@ init([]) -> util:beep(180, 100), util:beep(440, 200), util:beep(180, 50), + %ok = ledc_pwm:start(), + MaxRestarts = 5, % Kill everyone and die if more than MaxRestarts failures per MaxSecBetweenRestarts seconds MaxSecBetweenRestarts = 10, -% Worker1 = {led, {led, start_link, []}, permanent, 2000, worker, [led]}, + %Worker = {led, {led, start_link, []}, permanent, 2000, worker, [led]}, Debugger = worker(debugger, start_link, []), - LedControl = worker(led, start_link, []), Thermostat = worker(thermostat, start_link, []), TempReader = worker(temperature, start_link, []), - ButtonWatcher = worker(button_watcher, start_link, []), MainControl = worker(control, start_link, []), NetworkSup = supervisor(network_sup, []), + %LedControl = worker(led, start_link, []), + %ButtonWatcher = worker(button_watcher, start_link, []), {ok, {{one_for_one, MaxRestarts, MaxSecBetweenRestarts}, [ Debugger - ,LedControl + %,LedControl ,TempReader ,Thermostat ,MainControl diff --git a/src/app.hrl b/src/app.hrl index b801eb3..ceb7d94 100644 --- a/src/app.hrl +++ b/src/app.hrl @@ -15,7 +15,7 @@ %PINS/PORTS/PERIPHERALS -define(COMPRESSOR_LED, 2). % -define(STATUS_LED, 0). % --define(STATUS_LED2, 15). % +-define(STATUS_LED2, 33). % -define(COMPRESSOR, 4). % -define(BEEPER, 5). %VSPI -define(FAN1, 25). % @@ -40,7 +40,7 @@ -define(SPI_CS, 15). %HSPI --define(GENERIC_OUTPUT_ACTIVE_LOW, true). +-define(GENERIC_OUTPUT_ACTIVE_LOW, false). -define(GENERIC_OUTPUTS, [?OUT1]). -define(OUTPUT_PINS, [ @@ -66,7 +66,7 @@ %-define(MEASUREINTERVAL, 2000). -define(SAFETY_DELAY, 360000). --define(COIL_TEMP_LIMIT, 42). +-define(COIL_TEMP_LIMIT, 44). -define(THERMOSTAT_DEFAULT, <<"67">>). %BINARY integer only, not float or integer -define(THERMOSTAT_SPAN_DEFAULT, <<"4.0">>). %BINARY FLOAT ONLY -define(MIN_THERMOSTAT, 55). diff --git a/src/control.erl b/src/control.erl index 2886c9c..85a55fe 100644 --- a/src/control.erl +++ b/src/control.erl @@ -11,7 +11,7 @@ -behavior(gen_server). -export([start_link/0, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). --export([set_mode/1, cycle_mode/0, set_fan/1, cycle_fan/0, set_safe/0, all_off/0 ]). %, load/0, save/0]). +-export([set_mode/1, cycle_mode/0, set_fan/1, cycle_fan/0, set_safe/0, all_off/0, validate_mode/1 ]). %, load/0, save/0]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% @doc This module is the master control for the air conditioner. @@ -75,7 +75,7 @@ handle_cast(_Msg, State) -> %%% MODE SELECTION off|cool|energy_saver|fan_only handle_call({set_mode, Mode}, _From, _State=[{mode, M}, {fan, F}, {compressor, C}, {compressor_safe, CS}]) -> erlang:send(?MODULE, {set_mode, Mode}), - {reply, ok, [{mode, Mode}, {fan, F}, {compressor, C}, {compressor_safe, CS}] }; + {reply, ok, [{mode, M}, {fan, F}, {compressor, C}, {compressor_safe, CS}] }; %%% FAN SELECTION 1|2|3 handle_call({set_fan, FanNew}, _From, _State=[{mode, Mode}, {fan, F}, {compressor, C}, {compressor_safe, CS}]) -> @@ -127,8 +127,10 @@ handle_info({set_mode, Mode}, _State=[{mode, M}, {fan, F}, {compressor, C}, {com end, State = [{mode, Mode}, {fan, F}, {compressor, Comp}, {compressor_safe, CSafe}], notify(?TOPIC_STATUS, status_binary(State, "mode_changed")), - spawn(fun() -> util:beep(440, 100) end), - spawn(fun() -> led:flash(?STATUS_LED, 100, 4) end), + %spawn(fun() -> util:beep(440, 200) end), + %spawn(fun() -> led:flash(?STATUS_LED, 100, 4) end), + %spawn(?MODULE, modechange_feedback, [Mode]), + modechange_feedback(Mode), {noreply, State}; %%% FAN SELECTION 1|2|3 @@ -139,8 +141,11 @@ handle_info({set_fan, FanNew}, _State=[{mode, Mode}, {fan, FanPrev}, {compressor FanPrev; _NewFan -> spawn(fun() -> - timer:sleep(1000), %no reason for this other than to seem neat :) - fan(FanNew) + timer:sleep(800), %no reason for this other than to seem neat :) + case Mode of + off -> FanNew; %don't actually turn fan on, since mode is off. + _ -> fan(FanNew) + end end), case FanNew of 3 -> 3; @@ -244,60 +249,64 @@ handle_info(operation_loop, [{mode, Mode}, {fan, F}, {compressor, on}, {compress %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Convenience functions: %%% +set_fan(Fan) -> + gen_server:call(?MODULE, {set_fan, Fan}). + +cycle_fan() -> + gen_server:cast(?MODULE, cycle_fan). + +cycle_mode() -> + gen_server:cast(?MODULE, cycle_mode). + set_mode(Mode) -> % off|cool|energy_saver|fan_only case validate_mode(Mode) of invalid -> invalid_mode; ValidMode -> gen_server:call(?MODULE, {set_mode, ValidMode}) end. -set_status_led(Mode) -> - case Mode of - off -> util:set_output(?STATUS_LED, off); - on -> util:set_output(?STATUS_LED, on); - cool -> util:set_output(?STATUS_LED, on); - energy_saver -> util:set_output(?STATUS_LED, on); - fan_only -> util:set_output(?STATUS_LED, on); - _ -> util:set_output(?STATUS_LED, off) - end. - -%set_mode(Mode) -> % off|cool|energy_saver|fan_only -% case mode_valid(Mode) of -% true -> gen_server:call(?MODULE, {set_mode, Mode}); -% _ -> invalid_mode -% end. - validate_mode(Mode) -> case Mode of off -> off; "off" -> off; "OFF" -> off; + <<"off">> -> off; cool -> cool; "cool" -> cool; "COOL" -> cool; + <<"cool">> -> cool; energy_saver -> energy_saver; "energy_saver" -> energy_saver; "ENERGY_SAVER" -> energy_saver; + <<"energy_saver">> -> energy_saver; fan_only -> fan_only; "fan_only" -> fan_only; "FAN_ONLY" -> fan_only; + <<"fan_only">> -> fan_only; fan -> fan_only; "fan" -> fan_only; "FAN" -> fan_only; + <<"fan">> -> fan_only; _ -> invalid end. +%set_mode(Mode) -> % off|cool|energy_saver|fan_only +% case mode_valid(Mode) of +% true -> gen_server:call(?MODULE, {set_mode, Mode}); +% _ -> invalid_mode +% end. -mode_valid(Mode) -> - case Mode of - off -> true; - cool -> true; - energy_saver -> true; - fan_only -> true; - _ -> false - end. +%mode_valid(Mode) -> +% case Mode of +% off -> true; +% cool -> true; +% energy_saver -> true; +% fan_only -> true; +% _ -> false +% end. -set_fan(Fan) -> - gen_server:call(?MODULE, {set_fan, Fan}). +modechange_feedback(Mode) -> + spawn(fun() -> util:beep(440, 200) end), + led:set_status_led(Mode). set_safe() -> util:beep(1000, 200), @@ -344,12 +353,6 @@ compressor_set_outputs(C) -> util:set_output(?COMPRESSOR_LED, C), C. -cycle_mode() -> - gen_server:cast(?MODULE, cycle_mode). - -cycle_fan() -> - gen_server:cast(?MODULE, cycle_fan). - fan_on(F) -> case F of 0 -> fan(3); @@ -367,16 +370,19 @@ fan(F) -> 1 -> util:set_output(?FAN2, off), %turn off the ones that may be on before turning on the one required util:set_output(?FAN3, off), + timer:sleep(200), util:set_output(?FAN1, on), 1; 2 -> util:set_output(?FAN1, off), util:set_output(?FAN3, off), + timer:sleep(200), util:set_output(?FAN2, on), 2; 3 -> util:set_output(?FAN1, off), util:set_output(?FAN2, off), + timer:sleep(200), util:set_output(?FAN3, on), 3; _ -> 0 @@ -391,7 +397,6 @@ notify(Topic, Msg) -> status_binary(State) -> status_binary(State, "ok"). - status_binary([{mode, Mode}, {fan, Fan}, {compressor, Comp}, {compressor_safe, CS}], StatusMsg) -> [{temp, TStat}, {span, Span}] = thermostat:get_thermostat(), [{temp, _}, {coiltemp, CoilTemp}, {templocal, TempLocal}, {tempremote, TempRemote}, {tempsrc, TempSrc}] = temperature:get_temperature(), @@ -412,12 +417,12 @@ status_binary([{mode, Mode}, {fan, Fan}, {compressor, Comp}, {compressor_safe, C ,"\"mode\":\"", BMode/binary, "\"," ,"\"fan\":", BFan/binary, "," ,"\"compressor\":\"", BComp/binary, "\"," + ,"\"thermostat\":", BTStat/binary, "," ,"\"temp_local\":", BTempLocal/binary, "," ,"\"temp_remote\":", BTempRemote/binary, "," - ,"\"thermostat\":", BTStat/binary, "," + ,"\"coil\":", BCoilTemp/binary, "," ,"\"tempsource\":\"", BTempSrc/binary, "\"," ,"\"span\":", BSpan/binary, "," - ,"\"coil\":", BCoilTemp/binary, "," ,"\"comp_safe\":\"", BCS/binary, "\"," ,"\"time\":\"", BTime/binary, "\"," ,"\"status\":\"", BStatusMsg/binary, "\"" diff --git a/src/debugger.erl b/src/debugger.erl index cebba97..a8af75f 100644 --- a/src/debugger.erl +++ b/src/debugger.erl @@ -23,6 +23,9 @@ init(DebugStatus) -> %erlang:send_after(60000, self(), time), {ok, {DebugStatus, erlang:universaltime()}}. %set state of default debug status and bootup time +handle_call(get, _From, State={DebugType, _}) -> + %io:format("GETTING DEBUG TYPE~n"), + {reply, DebugType, State}; handle_call(enable, _From, _State={_, BootTime}) -> {reply, ok, {true, BootTime}}; handle_call(disable, _From, _State={_, BootTime}) -> @@ -41,8 +44,10 @@ handle_call(Call, _From, State) -> handle_cast(Msg, State={DebugType, _}) -> select_output(DebugType, Msg), +% erlang:display(Call), {noreply, State}. + %handle_info(time, State) -> % {{Year, Month, Day}, {Hour, Minute, Second}} = erlang:universaltime(), % debugger:format("[DEBUG] Time: ~p/~2..0p/~2..0p ~2..0p:~2..0p:~2..0p ~n", [ Year, Month, Day, Hour, Minute, Second ]), @@ -65,12 +70,14 @@ select_output(Mode, Msg) -> case Mode of console_only -> %io:format("[DEBUG]: ~p~n", [Msg]); - io:format(Msg); + io:format(Msg), + io:format("\n"); mqtt_only -> notify(?TOPIC_DEBUG, Msg); true -> %io:format("[DEBUG]: ~p~n", [Msg]), io:format(Msg), + io:format("\n"), notify(?TOPIC_DEBUG, Msg); false -> ok end. @@ -78,7 +85,7 @@ select_output(Mode, Msg) -> notify(Topic, Msg) -> case whereis(mqtt_client) of undefined -> io:format("[DEBUG MQTT dead] MSG:~p", [Msg]); - _Pid -> mqtt:publish_and_forget(Topic, Msg) % this notify/2 is called from handle_call/handle_info, so need to return quickly. + _Pid -> spawn(mqtt:publish_and_forget(Topic, Msg)) end. enable() -> @@ -95,13 +102,16 @@ console_only() -> log(Msg) -> gen_server:cast(?MODULE, Msg). +% DebugType = gen_server:call(?MODULE, get), +% select_output(DebugType, Msg). format(Msg) -> log(Msg). format(FmtMsg, FmtArgs) -> Msg = io_lib:format(FmtMsg, FmtArgs), - gen_server:cast(?MODULE, Msg). + %gen_server:cast(?MODULE, Msg). + log(Msg). uptime() -> erlang:send(?MODULE, uptime). diff --git a/src/led.erl b/src/led.erl index 6c8c4fc..437fc0d 100644 --- a/src/led.erl +++ b/src/led.erl @@ -8,65 +8,74 @@ -include("app.hrl"). -module(led). --export([flash/0, flash/1, flash/2, flash/3]). -%-export([init/1, handle_call/3, handle_info/2, terminate/2]). --export([start_link/0, init/1, handle_call/3, terminate/2]). - -start_link() -> - io:format("Starting '~p' with ~p/~p...~n", [?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY]), - {ok, _Pid} = gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - %timer:sleep(1). - -init(_) -> - io:format("[~p:~p] Starting...~n", [?MODULE, ?FUNCTION_NAME]), - flash_led(?STATUS_LED, 50, 20), - util:set_output(?STATUS_LED, on), - {ok, []}. - -handle_call(init, _From, State) -> - {reply, ok, State}; -handle_call({flash, Pin, Interval, Times}, _From, State) -> - Pid = spawn(fun() -> flash_led(Pin, Interval, Times) end), - %{reply, ok, [Pid|State]}; - {reply, ok, State}; -handle_call(listproc, _From, State) -> - io:format("Processes: ~p ~n", [State]), - {reply, ok, State}; -handle_call(reset, _From, State) -> - io:format("Processes: ~p ~n", [State]), - kill_flashers(State), - {reply, ok, []}; -handle_call(Call, _From, State) -> - erlang:display(Call), - {reply, ok, State}. - -%handle_info({gpio_interrupt, 26}, SPI) -> -% handle_irq(SPI), -% {noreply, SPI}. - +%-behavior(gen_server). -terminate(_Reason, _State) -> - ok. - -kill_flashers(PidList) -> - case PidList of - [Pid|Remainder] -> exit(Pid, kill), - kill_flashers(Remainder); - [] -> ok; - _ -> ok - end. +-export([flash/0, flash/1, flash/2, flash/3, flash_led/3, set_status_led/1]). +%-export([init/1, handle_call/3, handle_info/2, terminate/2]). +%-export([start_link/0, init/1, handle_call/3, handle_cast/2, terminate/2]). +% +%start_link() -> +% io:format("Starting '~p' with ~p/~p...~n", [?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY]), +% {ok, _Pid} = gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +% %timer:sleep(1). +% +%init(_) -> +% io:format("[~p:~p] Starting...~n", [?MODULE, ?FUNCTION_NAME]), +% flash_led(?STATUS_LED, 50, 20), +% util:set_output(?STATUS_LED, on), +% {ok, []}. +% +%handle_call(init, _From, State) -> +% {reply, ok, State}; +%handle_call({flash, Pin, Interval, Times}, _From, State) -> +% _Pid = spawn(fun() -> flash_led(Pin, Interval, Times) end), +% %{reply, ok, [Pid|State]}; +% {reply, ok, State}; +%handle_call(listproc, _From, State) -> +% io:format("Processes: ~p ~n", [State]), +% {reply, ok, State}; +%handle_call(reset, _From, State) -> +% io:format("Processes: ~p ~n", [State]), +% kill_flashers(State), +% {reply, ok, []}; +%handle_call(Call, _From, State) -> +% erlang:display(Call), +% {reply, ok, State}. +% +%handle_cast(Msg, State) -> +% erlang:display(Msg), +% {noreply, State}. +% +%%handle_info({gpio_interrupt, 26}, SPI) -> +%% handle_irq(SPI), +%% {noreply, SPI}. +% +% +%terminate(_Reason, _State) -> +% ok. +% +%kill_flashers(PidList) -> +% case PidList of +% [Pid|Remainder] -> exit(Pid, kill), +% kill_flashers(Remainder); +% [] -> ok; +% _ -> ok +% end. flash() -> flash(?STATUS_LED, 1000, forever). flash(Pin) -> - gen_server:call(?MODULE, {flash, Pin, 1000, forever}). + flash_led(Pin, 1000, forever). +% gen_server:call(?MODULE, {flash, Pin, 1000, forever}). flash(Pin, Interval) -> io:format("Flashing Pin ~p FOREVER with interval ~p ms.~n", [Pin, Interval]), - gen_server:call(?MODULE, {flash, Pin, Interval, forever}). + flash_led(Pin, Interval, forever). +% gen_server:call(?MODULE, {flash, Pin, Interval, forever}). flash(Pin, Interval, Times) -> io:format("Flashing Pin ~p ~p times with interval ~p ms.~n", [Pin, Times, Interval]), - gen_server:call(?MODULE, {flash, Pin, Interval, Times}). + flash_led(Pin, Interval, Times). +% gen_server:call(?MODULE, {flash, Pin, Interval, Times}). flash_led(_Pin, _Interval, 0) -> ok; @@ -80,4 +89,28 @@ flash_led(Pin, Interval, Times) -> timer:sleep(Interval), flash_led(Pin, Interval, Times-1). +set_status_led(Mode) -> + led:flash_led(?STATUS_LED, 50, 3), + case Mode of + off -> + util:set_output(?STATUS_LED, off), + util:set_output(?STATUS_LED2, off); + on -> + util:set_output(?STATUS_LED, on), + util:set_output(?STATUS_LED2, off); + cool -> + util:set_output(?STATUS_LED, on), + util:set_output(?STATUS_LED2, off); + energy_saver -> + util:set_output(?STATUS_LED, on), + util:set_output(?STATUS_LED2, on); + fan_only -> + util:set_output(?STATUS_LED, off), + util:set_output(?STATUS_LED2, on); + _ -> + util:set_output(?STATUS_LED, off), + util:set_output(?STATUS_LED2, off) + end. + + diff --git a/src/mqtt.erl b/src/mqtt.erl index 0a036e5..676d6d5 100644 --- a/src/mqtt.erl +++ b/src/mqtt.erl @@ -31,8 +31,8 @@ start_mqtt_client() -> MQTTConfig = #{ url => maps:get(url, Config), client_id => ?DEVICENAME, - disconnected_handler => fun(_MQTT_CLIENT_PID) -> io:format("yoyo disconnected from mqtt!!!!!!~n"), mqtt_client:reconnect(whereis(mqtt_client)) end, - error_handler => fun(_MQTT_CLIENT_PID, Err) -> io:format("dah mqtt error_handler: ~p~n", [Err]) end, + disconnected_handler => fun(_MQTT_CLIENT_PID) -> io:format("DISCONNECTED from MQTT!!!!!!~n"), mqtt_client:reconnect(whereis(mqtt_client)) end, + error_handler => fun(_MQTT_CLIENT_PID, Err) -> io:format("[~p:~p] ~p~n", [?MODULE, ?FUNCTION_NAME, Err]) end, connected_handler => fun handle_connected/1 }, io:format("Starting 'mqtt_client'...~n"), @@ -117,7 +117,9 @@ handle_connected(MQTT) -> Config = mqtt_client:get_config(MQTT), debugger:format("[~p:~p] MQTT started and connected to ~p~n", [?MODULE, ?FUNCTION_NAME, maps:get(url, Config)]), gen_server:call(mqtt, {ready, MQTT}), - subscribe_to_topic_list(MQTT, get_topics()). + subscribe_to_topic_list(MQTT, get_topics()), + publish_message(?TOPIC_DEBUG, util:uptime()), + publish_message(?TOPIC_DEBUG, io_lib:format("[~p] '~p' running on [~p] started and connected!", [?MODULE, ?APPNAME, ?DEVICENAME])). subscribe_to_topic(MQTT, Topic, SubHandleFunc, DataHandleFunc) -> debugger:format("Subscribing to ~p...~n", [Topic]), @@ -180,10 +182,14 @@ handle_data(_MQTT, Topic, Data) -> <<"flash2">> -> spawn(fun() -> led:flash(?STATUS_LED2, 300, 10) end); <<"flash 10">> -> spawn(fun() -> led:flash(?STATUS_LED, 200, 10) end); <<"flash forever">> -> spawn(fun() -> led:flash(?STATUS_LED, 200, forever) end); - <<"listproc">> -> gen_server:call(led, listproc); - <<"reset_led">> -> gen_server:call(led, reset); + %<<"listproc">> -> gen_server:call(led, listproc); + %<<"reset_led">> -> gen_server:call(led, reset); <<"beep">> -> spawn(fun() -> util:beep(440, 1000) end); <<"beep ", F/binary>> -> spawn(fun() -> util:beep(util:make_int(F), 1000) end); + %<<"beep_pwm">> -> spawn(fun() -> util:beep_pwm(1000, 400) end); + %<<"beep_pwm ", L:2/binary, " ", F/binary>> -> spawn(fun() -> io:format("FREQ: ~p, DURATION: ~p",[F, L]), util:beep_pwm(util:make_int(F), util:make_int(L) * 100) end); + %<<"beep_pwm ", F/binary>> -> spawn(fun() -> util:beep_pwm(util:make_int(F), 60) end); + %<<"beep_pwm_long ", F/binary>> -> spawn(fun() -> util:beep_pwm(util:make_int(F), 1000) end); %%% <<"ac_on">> -> ac_on(); <<"all_off">> -> all_off(); @@ -192,11 +198,17 @@ handle_data(_MQTT, Topic, Data) -> %<<"coiltemp ", T/binary>> -> control:set_coil_templimit(util:make_int(T)); %%% <<"sysinfo">> -> system_info:start(); - <<"uptime">> -> publish_message(?TOPIC_DEBUG, util:uptime()); <<"debug">> -> debugger:enable(); %crashes after a few minutes <<"nodebug">> -> debugger:disable(); <<"debug_mqtt_only">> -> debugger:mqtt_only(); <<"debug_console_only">> -> debugger:console_only(); + <<"memory">> -> publish_message(?TOPIC_DEBUG, io_lib:format("Free memory: ~p. Min free: ~p", [ + erlang:system_info(esp32_free_heap_size), + erlang:system_info(esp32_minimum_free_size) + ])); + <<"uptime">> -> publish_message(?TOPIC_DEBUG, util:uptime()); + <<"reset">> -> all_off(), esp:restart(); + <<"reboot">> -> all_off(), esp:restart(); %%% <<"off">> -> control:set_mode(off); <<"on">> -> control:set_mode(cool); @@ -208,49 +220,22 @@ handle_data(_MQTT, Topic, Data) -> <<"fan ", F/binary>> -> control:set_fan(util:make_int(F)); <<"temp_source ", TempSrc/binary>> -> temperature:set_source(TempSrc); <<"safe">> -> control:set_safe(); - <<"reset">> -> all_off(), esp:restart(); - <<"reboot">> -> all_off(), esp:restart(); %%% Timer - <<"timer_h off ", T/binary>> -> mode_timer(off, T*3600); %hours timer - <<"timer_m off ", T/binary>> -> mode_timer(off, T*60); %minutes timer - <<"timer_s off ", T/binary>> -> mode_timer(off, T); %seconds timer - <<"timer off ", T/binary>> -> mode_timer(off, T); %default seconds timer - <<"timer_h cool ", T/binary>> -> mode_timer(cool, T*3600); %hours timer - <<"timer_m cool ", T/binary>> -> mode_timer(cool, T*60); %minutes timer - <<"timer_s cool ", T/binary>> -> mode_timer(cool, T); %seconds timer - <<"timer cool ", T/binary>> -> mode_timer(cool, T); %default seconds timer - <<"timer_h on ", T/binary>> -> mode_timer(cool, T*3600); %hours timer - <<"timer_m on ", T/binary>> -> mode_timer(cool, T*60); %minutes timer - <<"timer_s on ", T/binary>> -> mode_timer(cool, T); %seconds timer - <<"timer on ", T/binary>> -> mode_timer(cool, T); %default seconds timer - <<"timer_h energy_saver ", T/binary>> -> mode_timer(cool, T*3600); %hours timer - <<"timer_m energy_saver ", T/binary>> -> mode_timer(cool, T*60); %minutes timer - <<"timer_s energy_saver ", T/binary>> -> mode_timer(cool, T); %seconds timer - <<"timer energy_saver ", T/binary>> -> mode_timer(cool, T); %default seconds timer - <<"timer_h fan_only ", T/binary>> -> mode_timer(fan_only, T*3600); %hours timer - <<"timer_m fan_only ", T/binary>> -> mode_timer(fan_only, T*60); %minutes timer - <<"timer_s fan_only ", T/binary>> -> mode_timer(fan_only, T); %seconds timer - <<"timer fan_only ", T/binary>> -> mode_timer(fan_only, T); %default seconds timer -% <<"timer_h ", Mode/binary, " ", T/binary>> -> %hours timer -% TimeMs = util:make_int(T) * 3600 * 1000, -% erlang:send_after(TimeMs, control, {set_mode, Mode}); -% <<"timer_m ", Mode/binary, " ", T/binary>> -> %minutes timer -% TimeMs = util:make_int(T) * 60 * 1000, -% erlang:send_after(TimeMs, control, {set_mode, Mode}); -% <<"timer_s ", Mode/binary, " ", T/binary>> -> %seconds timer -% TimeMs = util:make_int(T) * 1000, -% erlang:send_after(TimeMs, control, {set_mode, Mode}); -% <<"timer ", Mode/binary, " ", T/binary>> -> %default seconds timer -% TimeMs = util:make_int(T) * 1000, -% erlang:send_after(TimeMs, control, {set_mode, Mode}); - %TimeMs = case binary:last(T) of - % 104 -> %h - % 72 -> %H - % 109 -> %m - % 77 -> %M - % 115 -> %s - % 83 -> %S - %end, + <<"timer off ", T/binary>> -> util:timer(mode, off, T); + <<"timer on ", T/binary>> -> util:timer(mode, cool, T); + <<"timer cool ", T/binary>> -> util:timer(mode, cool, T); + <<"timer energy_saver ", T/binary>> -> util:timer(mode, energy_saver, T); + <<"timer fan_only ", T/binary>> -> util:timer(mode, fan_only, T); + <<"timer ", Mode1:1/binary, " ", T/binary>> -> + Mode = case Mode1 of + <<"o">> -> off; + <<"0">> -> off; + <<"1">> -> cool; + <<"c">> -> cool; + <<"e">> -> energy_saver; + <<"f">> -> fan_only + end, + util:timer(mode, Mode, T); %%% _ -> util:set_output(?STATUS_LED, Data) end. @@ -271,34 +256,12 @@ handle_data_thermostat(_MQTT, Topic, Data) -> <<"load">> -> thermostat:load(); <<"default">> -> thermostat:default(); %%% Timer - <<"timer_h ", Temp:2/binary, " ", T/binary>> -> thermostat_timer(Temp, util:make_int(T) * 3600); %hours timer - <<"timer_m ", Temp:2/binary, " ", T/binary>> -> thermostat_timer(Temp, util:make_int(T) * 60); %minutes timer - <<"timer_s ", Temp:2/binary, " ", T/binary>> -> thermostat_timer(Temp, util:make_int(T)); %seconds timer - <<"timer ", Temp:2/binary, " ", T/binary>> -> thermostat_timer(Temp, util:make_int(T)); %seconds timer + <<"timer ", Temp:2/binary, " ", T/binary>> -> util:timer(temp, Temp, T); %seconds timer %%% _ -> thermostat:set_temp(Data), publish_message(?TOPIC_THERMOSTAT_SET, Data) end. -mode_timer(Mode, TimeSec) -> - T = util:make_int(TimeSec), - spawn(fun() -> - debugger:format("Setting mode to ~p via timer for ~p seconds from now...~n", [Mode, T]), - erlang:send_after(T * 1000, control, {set_mode, Mode}), - debugger:format("Changing mode to ~p via timer after ~p seconds!~n", [Mode, T]) - end). - - -thermostat_timer(Temp, TimeSec) -> - T = util:make_int(TimeSec), - spawn(fun() -> - debugger:format("Setting thermostat ~p timer for ~p seconds from now...~n", [Temp, T]), - erlang:send_after(T * 1000, thermostat, {set_temp, util:make_int(Temp)}), - debugger:format("Set thermostat to ~p with timer after ~p seconds!~n", [Temp, T]) - end). - - - handle_data_fan(_MQTT, Topic, Data) -> debugger:format("[~p:~p/~p] received data on topic ~p: ~p ~n", [?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY, Topic, Data]), case Data of @@ -309,17 +272,22 @@ handle_data_fan(_MQTT, Topic, Data) -> F -> control:set_fan(util:make_int(F)) end. -%handle_data_output(_MQTT, Topic = <<"barf/out", X/binary>>, Data) -> handle_data_output(_MQTT, Topic, Data) -> debugger:format("~p/~p received data on topic ~p: ~p ~n", [?FUNCTION_NAME, ?FUNCTION_ARITY, Topic, Data]), %binary:last gives the last byte of a binary as an integer (ASCII value in this case). Subtract 48 to get the output number. :) Output = binary:last(Topic)-48, + %OutputBin = list_to_binary([Output+48]), Pin = lists:nth(Output, ?GENERIC_OUTPUTS), %select the output pin from the ?GENERIC_OUTPUTS define list. SetOutFunc = case ?GENERIC_OUTPUT_ACTIVE_LOW of true -> fun util:set_output_activelow/2; _ -> fun util:set_output/2 end, - SetOutFunc(Pin, Data). + case Data of + <<"timer off ", T/binary>> -> util:timer({SetOutFunc, [Pin, off]}, nil, T); % <> + <<"timer on ", T/binary>> -> util:timer({SetOutFunc, [Pin, on]}, nil, T); % <<"timer on 15m">> + <<"timer ", Val:1/binary, " ", T/binary>> -> util:timer({SetOutFunc, [Pin, util:make_int(Val)]}, nil, T); % <<"timer 1 15m">> + _ -> SetOutFunc(Pin, Data) + end. publish_thermostat_loop(Interval) -> [{temp, Therm}|_] = thermostat:get_thermostat(), diff --git a/src/network_sup.erl b/src/network_sup.erl index 6761cf2..1bdecdd 100644 --- a/src/network_sup.erl +++ b/src/network_sup.erl @@ -19,7 +19,7 @@ start_link() -> init([]) -> io:format("Starting ~p supervisor with ~p/~p...~n", [?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY]), MaxRestarts = 3, % Kill everyone and die if more than MaxRestarts failures per MaxSecBetweenRestarts seconds - MaxSecBetweenRestarts = 10, + MaxSecBetweenRestarts = 60, WiFi = worker(wifi, start_link, []), MQTT_Controller = worker(mqtt, start_link, []), %MQTTClient = worker(mqtt_client, start_link, []), diff --git a/src/util.erl b/src/util.erl index 214930c..7edca00 100644 --- a/src/util.erl +++ b/src/util.erl @@ -9,7 +9,19 @@ -module(util). --export([ set_output/2, set_output_activelow/2, print_time/0, uptime/0, beep/2, make_int/1, make_float/1, convert_to_binary/1 ]). +-export([ + set_output/2 + ,set_output_activelow/2 + ,timer/3 + ,timer/4 + ,print_time/0 + ,uptime/0 + ,beep/2 +% ,beep_pwm/2 + ,make_int/1 + ,make_float/1 + ,convert_to_binary/1 + ]). set_output(Pin, State) -> Level = level(State), %validate the pin state. @@ -42,21 +54,123 @@ uptime() -> _ -> trunc(UptimeSec/3600) end, Mins = case (UptimeSec > 3600) of - true -> trunc((UptimeSec rem (Hours * 3600))/60); + true -> trunc((UptimeSec rem ((Days*86400) + (Hours*3600)))/60); _ -> trunc(UptimeSec/60) end, Secs = case (UptimeSec > 60) of - true -> UptimeSec rem (Mins * 60); + true -> (UptimeSec rem ((Days*86400) + (Hours*3600) + (Mins*60))); _ -> UptimeSec end, - {{Y,M,D}, {H,M,S}} = erlang:universaltime(), - Timestamp = io_lib:format("~p~2..0p~2..0p ~2..0p:~2..0p:~2..0p", [Y, M, D, H, M, S]), - UptimeMsg = io_lib:format("~p UPTIME: ~p days, ~p hours, ~p minutes, ~p seconds", [Timestamp, Days, Hours, Mins, Secs]), - io:format("~p~n", [UptimeMsg]), + {{Year, Month, Day}, {Hour, Minute, Second}} = erlang:universaltime(), + io:format("[~p~2..0p~2..0p ~2..0p:~2..0p:~2..0p] UPTIME [~p]: ~p days, ~p hours, ~p minutes, ~p seconds", [Year, Month, Day, Hour, Minute, Second, UptimeSec, Days, Hours, Mins, Secs]), + UptimeMsg = io_lib:format("[~p~2..0p~2..0p ~2..0p:~2..0p:~2..0p] UPTIME [~p]: ~p days, ~p hours, ~p minutes, ~p seconds", [Year, Month, Day, Hour, Minute, Second, UptimeSec, Days, Hours, Mins, Secs]), UptimeMsg. +%%% FUNCTION TIMER +timer(Param, Val, Time) when is_binary(Time) -> + {T, M} = parse_timer_time(Time), + io:format("PARSED TIME: ~p, ~p ", [T, M]), + timer(Param, Val, T, M); +timer(Param, Val, Time) -> + timer(Param, Val, Time, s), + io:format("PARSED TIME 22222 where time is not binary: ~p, ~p ", [x, x]). +timer(Param, Val, Time, Multiplier) -> + io:format("[~p:~p] processing timer...~n", [?MODULE, ?FUNCTION_NAME]), + T = make_int(Time), + M = multiplier_from_specifier(Multiplier), + MultP = parse_multiplier(Multiplier), + Delay = T * M * 1000, + {_, {Hour, Minute, Second}} = erlang:universaltime(), + {TimerType, ModOrFunc, Args} = case Param of + temp -> {message_send, thermostat, [set_temp, util:make_int(Val)]}; + set_temp -> {message_send, thermostat, [set_temp, util:make_int(Val)]}; + thermostat -> {message_send, thermostat, [set_temp, util:make_int(Val)]}; + span -> {message_send, thermostat, [set_span, util:make_int(Val)]}; + mode -> {message_send, control, [set_mode, control:validate_mode(Val)]}; + set_mode -> {message_send, control, [set_mode, control:validate_mode(Val)]}; + {Func, Argz} -> {function, Func, Argz} + end, + case TimerType of + function -> + [Arg1, Arg2] = Args, + Pid = spawn(fun() -> + timer:sleep(Delay), + debugger:format("Previously set [~2..0p:~2..0p:~2..0p] function timer [after ~p~p] running Func(~p, ~p)] now!", [Hour, Minute, Second, T, M, Arg1, Arg2]), + ModOrFunc(Arg1, Arg2), + mqtt:publish_and_forget(?TOPIC_DEBUG, io_lib:format("Previously set [~2..0p:~2..0p:~2..0p] function timer just ran function Func(~p, ~p) after [~p~p].", [Hour, Minute, Second, Arg1, Arg2, T, MultP])) + end), + debugger:format("[~2..0p:~2..0p:~2..0p] Setting function timer ~p for Func(~p, ~p)] in [~p~p] from now...", [Hour, Minute, Second, Pid, Arg1, Arg2, T, Multiplier]), + io:format("[~2..0p:~2..0p:~2..0p] Setting function timer ~p for Func(~p, ~p)] in [~p~p] from now...", [Hour, Minute, Second, Pid, Arg1, Arg2, T, Multiplier]), + mqtt:publish_and_forget(?TOPIC_DEBUG, io_lib:format("[~2..0p:~2..0p:~2..0p] Timer ~p set to run function Func(~p, ~p) in [~p~p] from now...", [Hour, Minute, Second, Pid, Arg1, Arg2, T, MultP])); + message_send -> + Mod = ModOrFunc, + [OpMsg, Value] = Args, + spawn(fun() -> + erlang:send_after(Delay, Mod, {OpMsg, Value}), + debugger:format("[~2..0p:~2..0p:~2..0p] Setting message_send timer [~p ! {~p, ~p}] in [~p~p] from now...", [Hour, Minute, Second, Mod, OpMsg, Value, T, MultP]), + {_, {Hour, Minute, Second}} = erlang:universaltime(), + mqtt:publish_and_forget(?TOPIC_DEBUG, io_lib:format("[~2..0p:~2..0p:~2..0p] Timer sending [~p ! {~p, ~p}] in [~p~p] from now...", [Hour, Minute, Second, Mod, OpMsg, Value, T, MultP])) + end) + end. + + +parse_timer_time(T) when is_integer(T) -> {T, s}; +parse_timer_time(T) when is_binary(T) -> + %io:format("[~p:~p] parsing timer time from binary...~n", [?MODULE, ?FUNCTION_NAME]), + S = byte_size(T), + {Time, MultiplierSuffix} = case binary:last(T) of + 100 -> binary_split(T, S-1); %d = days + 68 -> binary_split(T, S-1); %D = days + 104 -> binary_split(T, S-1); %h = hours + 72 -> binary_split(T, S-1); %H = hours + 109 -> binary_split(T, S-1); %m = minutes + 77 -> binary_split(T, S-1); %M = minutes + 115 -> binary_split(T, S-1); %s = seconds + 83 -> binary_split(T, S-1); %S = seconds + _ -> {T, <<"s">>} %empty or invalid multipler (just seconds) + end, + {make_int(Time), MultiplierSuffix}. + %make_int(Time) * multiplier_from_specifier(MultiplierSuffix). %total number of seconds + +binary_split(Bin, PrefixSize) -> + <> = Bin, + {Prefix, Suffix}. -%%% Crude beep/sound functionality +multiplier_from_specifier(Multiplier) -> + M = parse_multiplier(Multiplier), + case M of + d -> 86400; + h -> 3600; + m -> 60; + s -> 1; + _ -> 1 + end. + +parse_multiplier(Multiplier) -> + case Multiplier of + d -> d; + "d" -> d; + <<"d">> -> d; + <<"D">> -> d; + <<"day">> -> d; + <<"days">> -> d; + h -> h; + "h" -> h; + <<"h">> -> h; + <<"H">> -> h; + <<"hour">> -> h; + <<"hours">> -> h; + m -> m; + "m" -> m; + <<"m">> -> m; + <<"M">> -> m; + <<"min">> -> m; + <<"minute">> -> m; + <<"minutes">> -> m; + _ -> s + end. + +%%% Crude beep/sound functionality - this should be done with proper timers or LEDC PWM. beep(_Freq, _Duration) when (_Duration =< 1) -> ok; beep(Freq, DurationMs) -> @@ -67,6 +181,26 @@ beep(Freq, DurationMs) -> timer:sleep(trunc(CyclePeriod / 2)), beep(Freq, DurationMs - CyclePeriod). %not accurate length, but good enough +%beep_pwm(Freq, Duration) -> +% io:format("beeping at freq ~p for duration ~p ms.~n", [Freq, Duration]), +% Freq = Freq, +% {ok, Timer} = ledc_pwm:create_timer(Freq), +% io:format("Timer: ~p~n", [Timer]), +% +% {ok, Beeper} = ledc_pwm:create_channel(Timer, ?BEEPER), +% %{ok, Beeper} = ledc_pwm:create_channel(Timer, 25), +% io:format("Channel1: ~p~n", [Beeper]), +% +% %FadeupMs = make_int(Duration), +% FadeupMs = make_int(Duration/0.9), +% ok = ledc_pwm:fade(Beeper, 100, FadeupMs), +% timer:sleep(FadeupMs), +% +% FadeDownMs = make_int(Duration/10), +% ok = ledc_pwm:fade(Beeper, 0, FadeDownMs), +% timer:sleep(FadeDownMs). + + level(State) when is_list(State) -> level(list_to_binary(State)); level(State) -> @@ -175,7 +309,8 @@ make_float(X) -> %Give back a float best you can from whatever input true -> 0.0 end. -make_int(X) -> %Give back an int best you can from whatever input +make_int(X) -> make_integer(X). +make_integer(X) -> %Give back an int best you can from whatever input if is_integer(X) -> X; is_float(X) -> trunc(X); diff --git a/src/wifi.erl b/src/wifi.erl index cad876d..005d981 100644 --- a/src/wifi.erl +++ b/src/wifi.erl @@ -75,6 +75,7 @@ disconnected() -> sntp_synchronized({TVSec, TVUsec}) -> debugger:format("Synchronized time with SNTP server. TVSec=~p TVUsec=~p~n", [TVSec, TVUsec]), util:print_time(), - debugger:format("Updating system boot time..."), - debugger:set_boot_time(). + debugger:format("Updating system boot time...~n"), + debugger:set_boot_time(), + mqtt:publish_and_forget(?TOPIC_DEBUG, util:uptime()).