About the Lua scripting language.
Samovar supports automation based on the scripting programming language
Lua. Lua support is enabled when building the firmware: in the configurator — the "Use Lua" («Использовать Lua») checkbox ("Hardware" («Оборудование») section), in PlatformIO — the
Samovar_luaenvironment, in a manual build — the line
#define USE_LUA in the user_config_override.h file. The script editor is opened with the "Editor" («Редактор») button on the Settings page (address http://samovar.local/edit). No login or password is requested.
The current Lua version is 5.4.4
When Samovar starts, the init.lua file runs. Its purpose is to enable/disable the one-second script execution loop and to initialize variables that will be used in other scripts. As shipped, init.lua disables the loop: setNumVariable("loop_lua_fl",0).
If running scripts in a loop is enabled, two scripts are run every second: first script.lua, then the script whose name depends on the operating mode: rectificat.lua (Rectification), dist.lua (Distillation), beer.lua (Beer), bk.lua (Wash column), nbk.lua (NBK), suvid.lua (Sous-vide), cheese.lua (Cheesemaking). In "Lua mode" («Lua-режим») (an item in the mode list in Settings, visible only in a build with Lua) there is no mode script — all the mode logic is written in script.lua. A pair of scripts can also be run once by going to http://samovar.local/lua, and a separate file at http://samovar.local/lua?script=file_name.
The scripts run one after another in a single task: while one is running, the other waits. A single script run cannot last longer than 20 seconds — after that it is aborted with the message "Lua: chunk execution aborted by timeout" («Lua: выполнение чанка прервано по таймауту»). If a mode script ends with an error 5 times in a row, the one-second loop stops (loop_lua_fl is reset to 0) and one summary message is printed to the console; script.lua stops being run after 5 errors in a row until the scripts are reloaded, without stopping the loop. Script errors are written to the browser console and to the port monitor.
You can also add buttons to the interface to run files. The file name: btn_<mode>_button<N>.lua, where mode is rect, dist, beer, bk, nbk, suvid or cheese, and N is the button number. For example, btn_rect_button1.lua, btn_beer_button2.lua. The buttons are shown on the "Additional" («Дополнительно») tab of the main page of the mode they were created for, in the Lua block; there is also the "Lua:" («Lua:») field with the "Run Lua" («Выполнить Lua») button for running one or two lines of code, and "Lua status" («Статус Lua») — a line that the script sets with the setLuaStatus function.
The button name is set in the first line of the file by a comment of the form --|Name^ (for example, --|Start^). If there is no such line, the buttons are named LUA1, LUA2, and so on, in the order of the files found.
As shipped, each mode has two buttons: "Start" («Начать») (btn_*_button1.lua — enables the one-second loop) and "Stop" («Остановить») (btn_*_button2.lua — disables it).
The main purpose of the buttons is to start running scripts in a loop (for example, by setting a variable value that can then be read in the script running in the loop), or to switch a control device (for example, a pump) on or off.
So the architecture of using the buttons looks as follows (as an example - for distillation mode):
The main controlled loop: once a second script.lua => dist.lua => script.lua => dist.lua =>... (controlled in the sense that it can be started/stopped programmatically from any script through a global variable)
The script.lua script is common to all modes; you can put here code that must run in any mode (rectification, beer, sous-vide, etc.)
The dist.lua script will be used only in distillation mode; you can put in it all the logic that must run once a second depending on parameters this script gets from the Samovar global variables, for example getNumVariable("TankTemp"), or from variables saved by any script in the common namespace, for example getObject("StartTankFilling"). That is, in the code of this script you can put, for example, the condition:
if (StartTankFilling == "true" and TankFillingPercent < 80) then StartPump() else StopPump() end
and every second the script will check whether there is a flag that the boiler needs to be filled, and if there is, whether it is already 80% full, and whether the previously started pump should now be stopped.
The button script btn_dist_button1.lua, which, when the button is pressed, sets the flag that the boiler needs to be filled via setObject("StartTankFilling", "true"). On the next cycle a second later, the dist.lua script will read this flag and switch the pump on (if the boiler is not yet filled). Thus all scripts share a common namespace, available through setObject/getObject, which lets them exchange data.
The button script btn_dist_button2.lua, which, when the button is pressed, can stop the main controlled loop via setNumVariable("loop_lua_fl", 0), for example if something goes wrong with the scripts.
A program line of type L (a Lua stage). In the programs of all modes except NBK (Rectification, Distillation, Wash column, Beer, Cheesemaking) there is a line of type L — a stage controlled by Lua. When this type is selected in the program editor, the "Lua stage" («Lua-этап») window opens, in which you set:
Lua file — any file with the .lua extension from the Samovar memory (the list is taken from the file editor). You can upload your own script through the web interface file editor.
Stage timeout, sec — from 1 to 65535 seconds. If the script has not called setNextProgram() within this time, Samovar shows the message "Lua did not finish the operation before the timeout" («Lua не завершила операцию до тайм-аута») and ends the mode.
Parameters — any number of strings passed to the script. In the script they are available in the arg table: arg[0] is the file name, arg[1], arg[2]… are the parameters in order. An empty parameter is written as "".
When the program reaches an L line, Samovar runs the selected file and waits for a setNextProgram() call — this is the move to the next line. A script error ("Lua finished with an error" («Lua завершилась с ошибкой»)) or a timeout stops the mode. The other fields of the line (temperature, rate, container, device) are not set for type L. In text form such a line is stored as the file and parameters separated by the ^ character, for example L;120;hops.lua^3^45;0;0;0 for rectification.
An example of the dose.lua script, which switches on the relay whose number is in the first parameter for the time in the second (in the program: file dose.lua, parameters 3 and 1500):
set_i2c_rele_state(tonumber(arg[1]), 1)
delay(tonumber(arg[2]))
set_i2c_rele_state(tonumber(arg[1]), 0)
setNextProgram()
If during operation you upload through the file editor a new version of the file that is currently being run by an L line, Samovar will re-read the script without restarting the program.
Samovar functions
From scripts you can access Samovar's internal variables (some of them cannot be changed, they are read-only) and call Samovar functions. While a mode switch is in progress, functions that change the Samovar state end with the error "mode switch blocks state changes".
Pin numbers depend on the board. Here and below, pass their numeric value:
ESP32 DEVKIT: RELE_CHANNEL1 – 2, RELE_CHANNEL2 – 15, RELE_CHANNEL3 – 14, RELE_CHANNEL4 – 13, WATER_PUMP_PIN – 4, LUA_PIN – 34, ALARM_BTN_PIN – 35.
ESP32-S3: RELE_CHANNEL1 – 2, RELE_CHANNEL2 – 42, RELE_CHANNEL3 – 41, RELE_CHANNEL4 – 40, WATER_PUMP_PIN – 11, LUA_PIN – 4, ALARM_BTN_PIN – 48.
LILYGO T-Relay: RELE_CHANNEL1 – 21, RELE_CHANNEL2 – 19, RELE_CHANNEL3 – 18, RELE_CHANNEL4 – 5, WATER_PUMP_PIN – 2, LUA_PIN – 12.
The constants INPUT, OUTPUT, LOW, HIGH and the command result codes ACTUATOR_COMMAND_ACCEPTED, ACTUATOR_COMMAND_PENDING, ACTUATOR_COMMAND_APPLIED, ACTUATOR_COMMAND_FAILED are defined in scripts.
pinMode(pin, mode) – like the Arduino function of the same name, sets the operating mode of the given input/output (pin) as an input or an output. Changing the mode is available only for the ports RELE_CHANNEL1–RELE_CHANNEL4 and LUA_PIN. That is, if you need to set the RELE_CHANNEL4 port of the DEVKIT board as an input, call pinMode(13, INPUT); as an output - pinMode(13, OUTPUT).
digitalWrite(pin, Value) – like the Arduino function of the same name, applies a HIGH or LOW value to a digital output (pin). The ports RELE_CHANNEL1–RELE_CHANNEL4, WATER_PUMP_PIN, LUA_PIN, ALARM_BTN_PIN and BTN_PIN are available, the others are ignored. That is, if you need to set a high value on the RELE_CHANNEL4 output, call digitalWrite(13, 1). For WATER_PUMP_PIN with a PWM pump, the value 1 switches the pump on at full speed, 0 stops it. If the emergency heating protection has triggered, writing to the heater channels (RELE_CHANNEL1, RELE_CHANNEL4) is not performed.
digitalRead(pin) – like the Arduino function of the same name, reads the value from the given input - HIGH or LOW. That is, if you need to read the value set on the RELE_CHANNEL4 input or output, call digitalRead(13)
analogRead() – like the Arduino function of the same name, reads the value from the analog input LUA_PIN (no parameters). The voltage applied to the analog input will be converted to a value from 0 to 4095. ATTENTION! Applying a voltage higher than 3.3 volts to the input can damage the port or the whole ESP32. In Cheesemaking mode LUA_PIN is occupied by the pH sensor: pinMode and digitalWrite for it end with an error.
exp_pinMode(pin, mode) – like pinMode(pin, mode), but for controlling the PCF8575 port expander; pin is the expander port number (0–15), mode is INPUT, OUTPUT or INPUT_PULLUP. All expander ports can be selected as either read or output
exp_digitalWrite(pin, Value) – like digitalWrite(pin, Value), but for controlling the PCF8575 port expander; pin is the expander port number (0–15), Value is 1 or 0. All expander ports are available for writing
exp_digitalRead(pin) – like digitalRead(pin), but for controlling the PCF8575 port expander; pin is the expander port number (0–15). All expander ports are available for reading
exp_analogWrite(Value) – like analogWrite(pin, Value), but for controlling the PCF8591 port expander, Value is a value from 0 to 255. The expander has one port available for output.
exp_analogRead(pin) – like analogRead(), but for controlling the PCF8591 port expander; pin is the expander input number (0–3), returns a value from 0 to 255.
The exp_* functions exist only in firmware built with USE_EXPANDER (PCF8575) and USE_ANALOG_EXPANDER (PCF8591).
delay(ms) – like the Arduino function of the same name, pauses program execution for the number of milliseconds given in the parameter (1000 milliseconds in 1 second). No more than 1000.
millis() – like the Arduino function of the same name, returns the number of milliseconds since Samovar started
setTimer(Num, Sec) – set timer number Num for Sec seconds (up to 65535). There are 10 timers, 1 to 10. That is, scripts can work with no more than 10 timers at the same time
getTimer(Num) – get the time remaining on timer number Num in seconds. If the timer is not set or the time has run out, the function returns 0
sendMsg(Msg, Level) – If Level = -1, the message Msg is printed to the COM port and to the browser console, convenient for debugging. If Level is 0, 1 or 2, the message is sent as a system message (0 – alarm, 1 – warning, 2 – notification): to the display, the web interface, the apps and the website.
setPower(Power) – switches Samovar on/off. setPower(0) switches it off, setPower(1) switches it on (the same as the heating button of the current mode)
setCurrentPower(Value) – set the value Value on the regulator. If a regulator with power control (SEM/AVR) is used, then Value is the power in watts to set, otherwise it is the voltage with 0.1 V accuracy. The function exists only in a build with a power regulator.
setBodyTemp() – set the hearts temperature during rectification. It works only in rectification mode. In other modes it prints a message to the console that the hearts temperature cannot be set
setMixer(Val) – switch the stirrer on/off. setMixer(0) – off, setMixer(1) – on
openValve(Val) – open/close the water valve. openValve(0) – close, openValve(1) – open
setPumpPwm(Val) – set the water pump speed, Val from 0 to 1023. Exists only in a build with a PWM pump (USE_WATER_PUMP).
setMixer, openValve, setCurrentPower and setPumpPwm return the command result code (the ACTUATOR_COMMAND_* constants).
setAlarm() – set the alarm mode. Samovar will switch off the power, close the water valve and switch off the water pump
setNextProgram() – go to the next program. Like pressing the "Next program" («Следующая программа») button in the interface. Works only when heating is on.
setPauseWithdrawal(Val) – pause/resume the take-off. setPauseWithdrawal(0) – resume, setPauseWithdrawal(1) – pause
setCapacity(Num) – turn the servo to container number Num (0 – drain, 1 and up – containers).
setServoAngle(Angle) – turn the servo to Angle degrees (0–180). Returns 1 if the firmware has a servo output (SERVO_PIN), otherwise 0. Used for homemade additive dispensers (see
Additive dispensers); the servo is shared with setCapacity, so after turning it in rectification the container must be set again.
getState() – get the Samovar status (a number), convenient for determining the state Samovar is in: 0 – idle; rectification: 10 – take-off in progress, 15 – automatic pause between program lines, 20 – program completed, 30 – take-off pump calibration, 50 – boost, 51 – stabilization, 52 – stabilization completed; 40 – manual pause (in any mode); 1000 – distillation in progress, 2000 – beer, 3000 – wash column, 4000 – NBK, 5000 – cheesemaking.
setNumVariable("Variable", Val) – set an internal numeric Samovar variable. Not all variables can have values set (list below). setNumVariable("loop_lua_fl", 1) will enable the one-second script loop. Attention! If you do not understand how Samovar works, it is better not to use this function, since you could potentially disrupt Samovar's operation
setStrVariable("Variable", Val) – set an internal string Samovar variable. Not all variables can have values set. setStrVariable("SamovarStatus", "Test Samovar status")
getNumVariable("Variable") – get an internal numeric Samovar variable. TankTemp = getNumVariable("TankTemp") will assign the boiler temperature value to the script variable TankTemp
getStrVariable("Variable") – get an internal string Samovar variable.
program_type = getStrVariable("program_type")– will assign the type of the program currently running to the script variable program_type.
Script variables exist only while the script is running. On the next run their values are not preserved. Sometimes you need to remember a variable's value in one script run and read it in another. There are two functions for this – setObject("Object",Val) and getObject("Object")/getObject("Object","NUMERIC"). The set variable values are kept until reboot. There can be no more than 32 different objects; if exceeded, setObject ends with an error.
setLuaStatus(Status) - to show the script status in the interface ("Lua status" («Статус Lua») on the "Additional" («Дополнительно») tab). For example, tracking script execution:
i = getObject("cnt", "NUMERIC")
i = i + 1
setLuaStatus("Counter cnt = "..i)
setObject("cnt", i)
setObject("Object",Val) – will remember the object Object with the value Val in the Samovar memory.
getObject("Object") – gets the previously saved value of the object Object. If no such object was saved, an empty string is returned. If you try to use an empty string as a number, it will take the value nil – undefined. To make working with numeric values easier and avoid checking for nil, you can call this function with the additional parameter "NUMERIC"; in this case, if the object has not been initialized yet, 0 is returned.
An example of working with setObject/getObject:
n = 156
setObject("MyObject", n)
print (n)
n = n + 5
print (n)
n = getObject("MyObject")
print (n)
If you run this script, it will print in the Arduino port monitor
156
161
156
http_request(Url) – perform a GET request to the address Url and return the response body (or the string "error"). For example, you can send readings to your own web service. You can also send a POST request by passing four parameters: the address, the method, the Content-Type header and the body:
http_request("http://test.com:80/post?foo1=bar1", "POST", "Content-Type: text/text; charset=utf-8", "body")
An example of a script that sends the boiler temperature to an external web service (replace the address and parameters with your own):
local char_to_hex = function(c)
return string.format("%%%02X", string.byte(c))
end
local function urlencode(url)
if url == nil then
return
end
url = url:gsub("\n", "\r\n")
url = url:gsub("([^%w ])", char_to_hex)
url = url:gsub(" ", "+")
return url
end
function SendToServer(text)
local key = "my_secret_key" -- your service access key
http_request("http://example.com/notify?key=" .. key .. "&text=" .. urlencode(text))
end
local text = "Current boiler temperature = ".. getNumVariable("TankTemp")
SendToServer(text)
The functions of the I2CStepper board (stirrer, dosing pump, relay) – set_stepper_by_time, set_stepper_target, get_stepper_status, i2cpump_start, i2cpump_stop, i2cpump_get_speed, i2cpump_get_target_ml, i2cpump_get_remaining_ml, i2cpump_get_running, set_mixer_pump_target, get_mixer_pump_status, set_i2c_rele_state, get_i2c_rele_state, check_I2C_device – are described on the page
I2CStepper.
Variables
The list of internal variables available in a script by default (their values are filled in before each script run, changing them by assignment is useless):
bme_pressure – the current atmospheric pressure value
capacity_num – the number of the container being taken off into
SamovarStatusInt – the Samovar operation status (the same as getState())
ProgramNum – the current program number
ProgramLen – the number of lines in the program
ActualVolumePerHour – the current take-off rate in liters
WthdrwlProgress - the progress of the current take-off
PowerOn – the heating on/off flag. 0 – off, 1 - on
PauseOn – the pause flag. 0 – no, 1 – yes
StepperMoving – the peristaltic pump running flag. 0 – not running, 1 – running
program_Pause – the flag that a pause program is running. 0 – no, 1 – yes
program_Wait – the flag that the program is paused. 0 – no, 1 – yes
program_Wait_Type – the reason for the pause – exceeded column section or vapor temperature. The values are the Russian strings "(пар)" (vapor) or "(царга)" (column section)
WFflowMilliLitres, WFtotalMilliLitres, WFflowRate – the volume for the last interval, the total volume (ml) and the cooling water flow rate from the flow sensor (only in a build with a flow sensor)
WthdrwTimeAll – the remaining take-off time
WthdrwTime – the take-off time of the current program line
WthdrwTimeAllS – the remaining take-off time as a string
WthdrwTimeS – the take-off time of the current program line as a string
pump_started – the water pump running status. 0 – no, 1 – yes
heater_state – the heating status during mashing. 0 – not heating, 1 – heating
mixer_status – the stirrer running status. 0 – not running, 1 – running
alarm_event – the flag that the alarm button has triggered. 0 – no, 1 – yes
acceleration_heater – the flag that the boost heating element is on. 0 – off, 1 – on
valve_status – the water supply valve status. 0 – closed, 1 – open
program_type – the type of the current program (from the description of rectification and beer programs)
program_volume – the take-off volume in ml
program_speed – the take-off rate in l/h
program_temp – the temperature at which this part of the distillate is taken off. 0 - determined automatically
program_power – the voltage/power at which this part of the distillate is taken off
program_time – the time needed for this program line to run
program_capacity_num – the container number for take-off
SamSetup_Mode – the Samovar operating mode: 0 – rectification, 1 – distillation, 2 – beer, 3 – wash column, 4 – NBK, 5 – sous-vide, 6 – Lua mode, 7 – cheesemaking
test_num_val – a numeric variable for debugging
test_str_val – a string variable for debugging
SteamTemp – the vapor sensor temperature
PipeTemp – the column section sensor temperature
WaterTemp – the water sensor temperature
TankTemp – the boiler sensor temperature
ACPTemp – the TCA sensor temperature
current_power_mode – the current regulator operating mode
target_power_volt – the set regulator operating voltage
wp_count
Variables available through getNumVariable (read-only): WFpulseCount, pump_started, valve_status, SamSetup_Mode, Samovar_Mode, Samovar_CR_Mode (the current and switchable mode, numbers as in SamSetup_Mode), SteamTemp, PipeTemp, WaterTemp, TankTemp, ACPTemp, WFtotalMilliLitres, WFflowRate, program_volume, program_speed, program_temp, program_power, program_time, program_capacity_num, capacity_num, target_power_volt, PowerOn, PauseOn, program_Wait, alcohol (the alcohol content in the boiler by temperature), alcohol_s (the alcohol content by vapor), water_pump_speed (the water pump speed 0–1023), pressure_value (the boiler pressure), YY, MM, DD, HH, MI, SS (year, month, day, hours, minutes, seconds).
Variables available through getNumVariable and setNumVariable: acceleration_temp, boil_temp (the remembered boiling temperature), wp_count, test_num_val, loop_lua_fl, SetScriptOff, show_lua_script. Only through setNumVariable: pmpKp, pmpKi, pmpKd – the PID coefficients of the water pump.
String variables (getStrVariable/setStrVariable): SamovarStatus – the status text on the main page (read and write), test_str_val (read and write), program_type (read only), Msg – when read, returns the next unread system message or an empty string, when written, adds a message.
Variables for controlling scripts:
loop_lua_fl – values 0 or 1. If 0 – do not run the every-second script loop, 1 – run it.
SetScriptOff – values 0 or 1. If 1 – stop the every-second loop (resets loop_lua_fl). Samovar sets it to 1 itself at the end of a program.
show_lua_script – values 0 or 1. If 0 – do not show the script being run in the console and the port monitor, 1 – show it. Can be used for debugging. It will also show the values of all variables set for this script.
For example, if you call these two functions in the init.lua script, two scripts (script.lua and the one determined by the current Samovar mode) will run every second, and each script will be printed to the port monitor and the browser console
setNumVariable("loop_lua_fl",1)
setNumVariable("show_lua_script",1)