Browse Source

Polish, fixes, etc.

- Mode validation
- Fix and improve action timers
- Removed unnecessary gen_server for LED control
- Show memory usage and uptime over console and MQTT
master
Ruel Tmeizeh - RuhNet 2 years ago
parent
commit
097f5c2d6e
11 changed files with 392 additions and 195 deletions
  1. +52
    -10
      README.md
  2. +1
    -0
      rebar.config
  3. +6
    -4
      src/airconditioner.erl
  4. +3
    -3
      src/app.hrl
  5. +46
    -41
      src/control.erl
  6. +13
    -3
      src/debugger.erl
  7. +82
    -49
      src/led.erl
  8. +41
    -73
      src/mqtt.erl
  9. +1
    -1
      src/network_sup.erl
  10. +144
    -9
      src/util.erl
  11. +3
    -2
      src/wifi.erl

+ 52
- 10
README.md View File

@ -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

+ 1
- 0
rebar.config View File

@ -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"}}}
]}.


+ 6
- 4
src/airconditioner.erl View File

@ -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


+ 3
- 3
src/app.hrl View File

@ -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).


+ 46
- 41
src/control.erl View File

@ -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, "\""


+ 13
- 3
src/debugger.erl View File

@ -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).


+ 82
- 49
src/led.erl View File

@ -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.

+ 41
- 73
src/mqtt.erl View File

@ -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 off 15m">>
<<"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(),


+ 1
- 1
src/network_sup.erl View File

@ -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, []),


+ 144
- 9
src/util.erl View File

@ -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) ->
<<Prefix:PrefixSize/binary, Suffix/binary>> = 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);


+ 3
- 2
src/wifi.erl View File

@ -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()).

Loading…
Cancel
Save