Berry Scripting Language ~
Berry Scripting is included in all tasmota32
builds. It is NOT supported on ESP82xx
If you plan to code in Berry, you should enable #define USE_BERRY_DEBUG
which will give you much more details when coding
Useful resources:
- First time user of Berry: Berry Introduction (in 20 minutes of less)
- Language fast reference PDF (7 pages) Berry Short manual
- Full language documentation The Berry Script Language Reference Manual
- Tasmota extension of Berry, see below
- Full examples in the Berry Cookbook
If you're new to Berry, have a look at Berry Introduction (in 20 minutes of less)
Introduction to Berry~
Berry is the next generation scripting for Tasmota. It is based on the open-source Berry project, delivering an ultra-lightweight dynamically typed scripting language designed for lower-performance embedded devices.
Reference sheet
Download Berry Short Manual to get a list of basic functions and capabilities of Berry language
Berry Scripting allows simple and also advanced extensions of Tasmota, for example:
- simple scripting
- advanced rules, beyond what is possible with native rules
- advanced automations
Berry Scripting takes it one step further and allows to build dynamic extensions to Tasmota, that would previously require native code:
- build light animations
- build I2C drivers
- build complete Tasmota drivers
- integrate native libraries like
lvgl
see LVGL
About the Berry language~
Berry has the following advantages:
- Lightweight: A well-optimized interpreter with very little resources. Ideal for use in microprocessors.
- Fast: optimized one-pass bytecode compiler and register-based virtual machine.
- Powerful: supports imperative programming, object-oriented programming, functional programming.
- Flexible: Berry is a dynamic type script, and it's intended for embedding in applications. It can provide good dynamic scalability for the host system.
- Simple: simple and natural MicroPython-eque syntax, supports garbage collection and easy to use FFI (foreign function interface).
- RAM saving: With compile-time object construction, most of the constant objects are stored in read-only code data segments, so the RAM usage of the interpreter is very low when it starts.
Tasmota Port~
Berry Scripting in only supported on Tasmota32 for ESP32. The RAM usage starts at ~10kb and will be later optimized. Berry uses PSRAM on ESP32 if available (PSRAM is external RAM attached to ESP32 via SPI, it is slower but larger than internal RAM.
Quick Start~
Click on Configuration then Berry Scripting Console and enjoy the colorful Berry console, also called REPL (Read-Eval-Print-Loop).
Drag the bottom corner of each screen to change its size
The console is not designed for big coding tasks but it's recommended to use a code editor when dealing with many, many lines of code. An extension for Visual Studio Code exists to make writing Berry scripts even easier with colored syntax. Download the entire folder and copy to VSCode extensions folder.
REPL Console~
Try typing simple commands in the REPL. Since the input can be multi-lines, press Enter twice or click "Run" button to run the code. Use Up and Down to navigate through history of previous commands.
> 1+1
2
> 2.0/3
0.666667
> print('Hello Tasmota!')
Hello Tasmota!
Note: Berry's native print()
command displays text in the Berry Console and in the Tasmota logs. To log with finer control, you can also use the log()
function which will not display in the Berry Console.
> print('Hello Tasmota!')
log('Hello again')
Hello Tasmota!
Meanwhile the Tasmota log shows:
> tasmota.cmd("Dimmer 60")
{'POWER': 'ON', 'Dimmer': 60, 'Color': '996245', 'HSBColor': '21,55,60', 'Channel': [60, 38, 27]}
The light is bright
Save your Scripts~
Berry can autostart your scripts. See a short desciption in the Section about the Filesystem: https://tasmota.github.io/docs/UFS/#autoexecbe Your can use the Filemanager to edit or save files with your berry scripts.
Iterate without rebooting~
Since v13.0.0.1 you can restart the entire Berry VM with a click in the Berry console. This feature requires to compile with #define USE_BERRY_DEBUG
which is anyways highly recommended when coding in Berry. Be aware that restarting the Berry VM loses all context, and may generate negative side effects that we haven't yet identified. When restarting the VM, autoexec.be
is ran again.
Instead of using the Web UI, you can also use the BrRestart
command which does not require #define USE_BERRY_DEBUG
.
Lights and Relays~
Berry provides complete support for Relays and Lights.
You can control individual Relays or lights with tasmota.get_power()
and tasmota.set_power()
.
tasmota.get_power()
returns an array of booleans representing the state of each relays and light (light comes last).
tasmota.set_power(relay, onoff)
changes the state of a single relay/light.
2 relays and 1 light
> tasmota.get_power()
[false, true, false]
> tasmota.set_power(0, true)
true
> tasmota.get_power()
[true, true, false]
For light control, light.get()
and light.set
accept a structured object containing the following arguments:
Attributes | Details |
---|---|
power | boolean Turns the light off or on. Equivalent to tasmota.set_power() . When brightness is set to 0 , power is automatically set to off. On the contrary, you need to specify power:true to turn the light on. |
bri | int range 0..255 Set the overall brightness. Be aware that the range is 0..255 and not 0..100 as Dimmer. |
hue | int 0..360 Set the color Hue in degree, range 0..360 (0=red). |
sat | int 0..255 Set the color Saturation (0 is grey). |
ct | int 153..500 Set the white color temperature in mired, ranging from 153 (cold white) to 500 (warm white) |
rgb | string 6 hex digits Set the color as hex RRGGBB , changing color and brightness. |
channels | array of int, ranges 0..255 Set the value for each channel, as an array of numbers |
When setting attributes, they are evaluated in the following order, the latter overriding the previous: power
, ct
, hue
, sat
, rgb
, channels
, bri
.
# set to yellow, 25% brightness
> light.set({"power": true, "hue":60, "bri":64, "sat":255})
{'bri': 64, 'hue': 60, 'power': true, 'sat': 255, 'rgb': '404000', 'channels': [64, 64, 0]}
# set to RGB 000080 (blue 50%)
> light.set({"rgb": "000080"})
{'bri': 128, 'hue': 240, 'power': true, 'sat': 255, 'rgb': '000080', 'channels': [0, 0, 128]}
# set bri to zero, also powers off
> light.set({"bri": 0})
{'bri': 0, 'hue': 240, 'power': false, 'sat': 255, 'rgb': '000000', 'channels': [0, 0, 0]}
# changing bri doesn't automatically power
> light.set({"bri": 32, "power":true})
{'bri': 32, 'hue': 240, 'power': true, 'sat': 255, 'rgb': '000020', 'channels': [0, 0, 32]}
# set channels as numbers (purple 12%)
> light.set({"channels": [32,0,32]})
{'bri': 32, 'hue': 300, 'power': true, 'sat': 255, 'rgb': '200020', 'channels': [32, 0, 32]}
Rules~
The rule function have the general form below where parameters are optional:
def function_name(value, trigger, msg)
end
Parameter | Description |
---|---|
value | The value of the trigger. Similar to %value% in native rules. |
trigger | string of the trigger with all levels. Can be used if the same function is used with multiple triggers. |
msg | map Berry structured object of the message, decoded from JSON. If JSON was invalid, it contains the original string |
Dimmer rule
Define the function and add a rule to Tasmota where the function runs if Dimmer value is more than 50
> def dimmer_over_50()
print("The light is bright")
end
tasmota.add_rule("Dimmer>50", dimmer_over_50)
> tasmota.cmd("Dimmer 30")
{'POWER': 'ON', 'Dimmer': 30, 'Color': '4D3223', 'HSBColor': '21,55,30', 'Channel': [30, 20, 14]}
> tasmota.cmd("Dimmer 60")
{'POWER': 'ON', 'Dimmer': 60, 'Color': '996245', 'HSBColor': '21,55,60', 'Channel': [60, 38, 27]}
The light is bright
The same function can be used with multiple triggers.
If the function to process an ADC input should be triggered both by the tele/SENSOR
message and the result of a Status 10
command:
tasmota.add_rule("ANALOG#A1", rule_adc_1)
tasmota.add_rule("StatusSNS#ANALOG#A1", rule_adc_1)
Or if the same function is used to process similar triggers:
import string
def rule_adc(value, trigger)
var i=string.find(trigger,"#A")
var tr=string.split(trigger,i+2)
var adc=number(tr[1])
print("value of adc",adc," is ",value)
end
tasmota.add_rule("ANALOG#A1",rule_adc)
tasmota.add_rule("ANALOG#A2",rule_adc)
Another way to address the same using anonymous functions created dynamically
def rule_adc(adc, value)
print("value of adc",adc," is ",value)
end
tasmota.add_rule("ANALOG#A1", def (value) rule_adc(1,value) end )
tasmota.add_rule("ANALOG#A2", def (value) rule_adc(2,value) end )
Multiple triggers AND logic~
It is possible to combine multiple triggers in a AND logic as an array:
tasmota.add_rule(["ANALOG#A1>300","ANALOG#A1<500"], def (values) rule_adc_in_range(1,values) end )
300 < ANALOG#A1 < 500
Triggers can be of different types too:
tasmota.add_rule(["ANALOG#A1>300","BME280#Temperature>28.0"], def (values) rule_adc_and_temp(1,values) end )
ANALOG#A1>300
AND BME280#Temperature>28.0
In that case, the value and trigger arguments passed to the rule function are also lists:
def function_name(values:list_of_string, triggers:list_of_string, msg)
end
msg
remains unchanged. Teleperiod rules~
Teleperiod rules are supported with a different syntax from Tasmota rules. Instead of using Tele-
prefix, you must use Tele#
. For example Tele#ANALOG#Temperature1
instead of Tele-ANALOG#Temperature1
Rules operators~
Operator | Function |
---|---|
String Operators | |
= | equal to (used for string comparison) |
!== | not equal to (used for string comparison) |
$< | string starts with |
$> | string ends with |
$\| | string contains |
$! | string is not equal to |
$^ | string do not contains |
Numerical Operators | |
== | equal to (used for numerical comparison) |
> | greater than |
< | lesser than |
!= | number not equal to |
>= | greater than or equal to |
<= | lesser than or equal to |
\| | Modulo division to this number is 0 (remainder=0) |
Timers~
Berry code, when it is running, blocks the rest of Tasmota. This means that you should not block for too long, or you may encounter problems. As a rule of thumb, try to never block more than 50ms. If you need to wait longer before the next action, use timers. As you will see, timers are very easy to create thanks to Berry's functional nature.
All times are in milliseconds. You can know the current running time in milliseconds since the last boot:
> tasmota.millis()
9977038
Sending a timer is as easy as tasmota.set_timer(<delay in ms>,<function>)
> def t() print("Booh!") end
> tasmota.set_timer(5000, t)
[5 seconds later]
Booh!
A word on functions and closure~
Berry is a functional language, and includes the very powerful concept of a closure. In a nutshell, it means that when you create a function, it can capture the values of variables when the function was created. This roughly means that it does what intuitively you would expect it to do.
When using Rules or Timers, you always pass Berry functions.
cron
recurrent calls~
You can choose to run some function/closure at regular intervals specified as cron
style format with the first field representing seconds.
> def f() print("Hi") end
> tasmota.add_cron("*/15 * * * * *", f, "every_15_s")
Hi
Hi # added every 15 seconds
> tasmota.remove_cron("every_15_s") # cron stops
Like timers, you need to create a closure if you want to register a method of an instance. Example:
class A
var name
def init(name)
self.name = name
end
def p()
print("Hi,", self.name)
end
end
> bob = A("bob")
> bob.p()
Hi, bob
> tasmota.add_cron("*/15 * * * * *", /-> bob.p(), "hi_bob")
Hi, bob
Hi, bob
Hi, bob
> tasmota.remove_cron("hi_bob") # cron stops
You can get the timestamp for the next event by using tasmota.next_cron(id)
which returns an epoch in seconds.
Loading Filesystem~
Berry files can exist in 2 forms, either a source file (extension .be
) or a pre-compiled bytecode (extension .bec
). Pre-compiled are usually smaller and load slightly faster (although compilation is fast enough in most use cases). It's usually more flexible and simpler to use source code (.be
).
You can upload Berry code in the filesystem using the Consoles - Manage File system menu and load them at runtime. Make careful to use *.be
extension for those files.
To load a Berry file, use the load(filename)
function where filename
is the name of the file with .be
or .bec
extension; if the file has no extension '.be' is automatically appended.
You don't need to prefix with /
. A leading /
will be added automatically if it is not present.
Previous behavior before 13.4.0.3:
When loading a Berry script, the compiled bytecode is automatically saved to the filesystem, with the extension .bec
(this is similar to Python's .py
/.pyc
mechanism). The save(filename,closure)
function is used internally to save the bytecode.
If a precompiled bytecode (extension .bec
) is present of more recent than the Berry source file, the bytecode is directly loaded which is faster than compiling code. You can eventually remove the *.be
file and keep only *.bec
file (even with load("file.be")
.
The loading behavior is as follows:
load("hello")
andload("hello.be")
loadshello.be
and trieshello.bec
if the first does not exist. If bothhello.be
andhello.bec
exist,hello.bec
is deleted to avoid confusion between versions.load("hello.bec")
loads onlyhello.bec
and fails if onlyhello.be
is present
To compile to .bec
use tasmota.compile("hello.be")
. If all is good, it returns true
and creates hello.bec
. But beware that if you use load()
the .bec
file is deleted.
Note: tasmota.compile()
is different than native Berry compile()
Creating a Tasmota Driver~
You can easily create a complete Tasmota driver with Berry.
A Driver responds to messages from Tasmota. For each message type, the method with the same name is called. Actually you can register any class as a driver, it does not need to inherit from Driver
; the call mechanism is based on names of methods that must match the name of the event to be called.
Driver methods are called with the following parameters: f(cmd, idx, payload, raw)
. cmd
is a string, idx
an integer, payload
a Berry object representation of the JSON in payload
(if any) or nil
, raw
is a string. These parameters are meaningful to a small subset of events:
every_second()
: called every secondevery_50ms()
: called every 50ms (i.e. 20 times per second)every_100ms()
: called every 100ms (i.e. 10 times per second)every_250ms()
: called every 250ms (i.e. 4 times per second)web_sensor()
: display sensor information on the Web UIjson_append()
: display sensor information in JSON format for TelePeriod reportingweb_add_button()
: (deprecated) synonym ofweb_add_console_button()
web_add_main_button()
,web_add_management_button()
,web_add_console_button()
,web_add_config_button()
: add a button to Tasmotas Web UI on a specific pageweb_add_handler()
: called when Tasmota web server started, and the right time to callwebserver.on()
to add handlersbutton_pressed()
: called when a button is pressedsave_before_restart()
: called just before a restartmqtt_data(topic, idx, data, databytes)
: called for MQTT payloads matchingmqtt.subscribe
.idx
is zero, anddata
is normally unparsed JSON.set_power_handler(cmd, idx)
: called whenever a Power command is made.idx
is a combined index value, with one bit per relay or light currently on.cmd
can be ignored.any_key(cmd, idx)
: called when an interaction with Button or Switch occurs.idx
is encoded as follows:device_save << 24 | key << 16 | state << 8 | device
display()
: called by display driver with the following subtypes:init_driver
,model
,dim
,power
.
Then register the driver with tasmota.add_driver(<driver>)
.
There are basically two ways to respond to an event:
Example
Define a class and implement methods with the same name as the events you want to respond to.
class MyDriver
def every_second()
# do something
end
end
d1 = MyDriver()
tasmota.add_driver(d1)
Fast Loop~
Beyond the events above, a specific mechanism is available for near-real-time events or fast loops (200 times per second, or 5ms).
Special attention is made so that there is no or very little impact on performance. Until a first callback is registered, performance is not impacted and Berry is not called. This protects any current use from any performance impact.
Once a callback is registered, it is called separately from Berry drivers to ensure minimal overhead.
tasmota.add_fast_loop(cl:function) -> nil
registers a callback to be called in fast loop mode.
The callback is called without any parameter and does not need to return anything. The callback is called at each iteration of Tasmota event loop. The frequency is set to 200Hz or 5ms.
Note: since v13.1.0.2, the frequency of fast_loop
does not depend anymore on the value of the Sleep <x>
command.
tasmota.remove_fast_loop(cl:function) -> nil
removes a previously registered function or closure. You need to pass the exact same closure reference.
Warning, if you need to register a method from an instance, you need a closure:
class my_driver
def every_100ms()
# called every 100ms via normal way
end
def fast_loop()
# called at each iteration, and needs to be registered separately and explicitly
end
def init()
# register fast_loop method
tasmota.add_fast_loop(/-> self.fast_loop())
# variant:
# tasmota.add_fast_loop(def () self.fast_loop() end)
end
end
tasmota.add_driver(my_driver()) # register driver
tasmota.add_fast_loop(/-> my_driver.fast_loop()) # register a closure to capture the instance of the class as well as the method
Tasmota Only Extensions~
log(msg:string [, level:int = 3]) -> string
~
Logs a message to the Tasmota console. Optional second argument is log_level (0..4), default is 2
(matching build time LOG_LEVEL_INFO
).
Example
> log("A")
A
load(filename:string) -> bool
~
Loads a Berry script from the filesystem, and returns true if loaded successfully, false if file not found, or raises an exception in runtime. Filename does not need to start with /
, but needs to end with .be
(Berry source code) or .bec
(precompiled bytecode). If the .be
extension is missing, it is automatically added.
The behavior for .bec
files changed in v13.4: - when loading <file>.be
, the .be
file is loaded in priority and any .bec
file with same prefix is removed (to avoid inconsistencies). If no .be
file is present, it tried to load .bec
file. - when loading <file>.bec
, only .bec
files are loaded and .be
is ignored. - to create .bec
files, you need to use tasmota.compile
(see below)
tasmota.compile(filename:string) -> bool
~
Loads a .be
file, compiles it and saves a .bec
file containing the compiled bytecode.
Note: tasmota.compile
is different from Berry native compile
function.
save(filename:string, f:closure) -> nil
~
Internally used function to save bytecode. It's a wrapper to the Berry's internal API be_savecode()
. There is no check made on the filename.
There is generally no need to use this function, it is used internally by load()
.
tasmota
object~
A root level object called tasmota
is created and contains numerous functions to interact with Tasmota.
Functions used to retrieve Tasmota configuration~
Functions for time, timers or cron~
Functions to create custom Tasmota command~
Functions to add custom responses to JSON and Web UI to sensors~
See examples in the Berry-Cookbook
Functions to manage Relays and Lights~
Low-level access to Tasmota globals and settings.~
Use with care and only if you know what you are doing.
The construct is to use tasmota.global
or tasmota.settings
to read or write attributes.
You can do bad things with these features
Value | Details |
---|---|
tasmota.global.sleep | Current sleep value |
tasmota.global.devices_present | Number of Power channels, e.g. having virtual relays |
tasmota.settings.sleep | Sleep value stored in flash |
mqtt
module~
Use with import mqtt
.
Since v11.1.0.1, there is an easier way than registering a driver, and listening to mqtt_data
event. You can now just attach a function or closure to a MQTT topic, and it does the magic for you.
The function you attach to a topic pattern received only the matching MQTT messages, not all messages unlike mqtt_data()
would.
The function takes the same parameters as mqtt_data()
:
topic
: full topic received from the brokeridx
: not usedpayload_s
: payload as string, usually converted to JSON withimport json json.load(payload_s)
payload_b
: payload as a binary payload, bytes() array
the function should return true
if the event was parsed or if the event should not trigger a Tasmota command. If you return nil
or nothing, it is considered as true
which is the usual behavior you want (i.e. not trigger a Tasmota command from random MQTT messages).
light
object~
Module light
is automatically imported via a hidden import light
command.
gpio
module~
This module allows to retrieve the GPIO configuration set in the templates. You need to distinguish between logical gpio (like PWM, or I2C) and physical gpio which represent the GPIO number of the physical pin. gpio.pin()
transforms a logical GPIO to a physical GPIO, or -1
if the logical GPIO is not set.
Currently there is limited support for GPIO: you can only read/write in digital mode and set the GPIO mode.
Any internal error or using unsupported GPIO yields a Berry exception.
Possible values for Tasmota GPIOs:
gpio.NONE
, gpio.KEY1
, gpio.KEY1_NP
, gpio.KEY1_INV
, gpio.KEY1_INV_NP
, gpio.SWT1
, gpio.SWT1_NP
, gpio.REL1
, gpio.REL1_INV
, gpio.LED1
, gpio.LED1_INV
, gpio.CNTR1
, gpio.CNTR1_NP
, gpio.PWM1
, gpio.PWM1_INV
, gpio.BUZZER
, gpio.BUZZER_INV
, gpio.LEDLNK
, gpio.LEDLNK_INV
, gpio.I2C_SCL
, gpio.I2C_SDA
, gpio.SPI_MISO
, gpio.SPI_MOSI
, gpio.SPI_CLK
, gpio.SPI_CS
, gpio.SPI_DC
, gpio.SSPI_MISO
, gpio.SSPI_MOSI
, gpio.SSPI_SCLK
, gpio.SSPI_CS
, gpio.SSPI_DC
, gpio.BACKLIGHT
, gpio.OLED_RESET
, gpio.IRSEND
, gpio.IRRECV
, gpio.RFSEND
, gpio.RFRECV
, gpio.DHT11
, gpio.DHT22
, gpio.SI7021
, gpio.DHT11_OUT
, gpio.DSB
, gpio.DSB_OUT
, gpio.WS2812
, gpio.MHZ_TXD
, gpio.MHZ_RXD
, gpio.PZEM0XX_TX
, gpio.PZEM004_RX
, gpio.PZEM016_RX
, gpio.PZEM017_RX
, gpio.SAIR_TX
, gpio.SAIR_RX
, gpio.PMS5003_TX
, gpio.PMS5003_RX
, gpio.SDS0X1_TX
, gpio.SDS0X1_RX
, gpio.SBR_TX
, gpio.SBR_RX
, gpio.SR04_TRIG
, gpio.SR04_ECHO
, gpio.SDM120_TX
, gpio.SDM120_RX
, gpio.SDM630_TX
, gpio.SDM630_RX
, gpio.TM1638CLK
, gpio.TM1638DIO
, gpio.TM1638STB
, gpio.MP3_DFR562
, gpio.HX711_SCK
, gpio.HX711_DAT
, gpio.TX2X_TXD_BLACK
, gpio.TUYA_TX
, gpio.TUYA_RX
, gpio.MGC3130_XFER
, gpio.MGC3130_RESET
, gpio.RF_SENSOR
, gpio.AZ_TXD
, gpio.AZ_RXD
, gpio.MAX31855CS
, gpio.MAX31855CLK
, gpio.MAX31855DO
, gpio.NRG_SEL
, gpio.NRG_SEL_INV
, gpio.NRG_CF1
, gpio.HLW_CF
, gpio.HJL_CF
, gpio.MCP39F5_TX
, gpio.MCP39F5_RX
, gpio.MCP39F5_RST
, gpio.PN532_TXD
, gpio.PN532_RXD
, gpio.SM16716_CLK
, gpio.SM16716_DAT
, gpio.SM16716_SEL
, gpio.DI
, gpio.DCKI
, gpio.CSE7766_TX
, gpio.CSE7766_RX
, gpio.ARIRFRCV
, gpio.ARIRFSEL
, gpio.TXD
, gpio.RXD
, gpio.ROT1A
, gpio.ROT1B
, gpio.ADC_JOY
, gpio.SSPI_MAX31865_CS1
, gpio.HRE_CLOCK
, gpio.HRE_DATA
, gpio.ADE7953_IRQ
, gpio.SOLAXX1_TX
, gpio.SOLAXX1_RX
, gpio.ZIGBEE_TX
, gpio.ZIGBEE_RX
, gpio.RDM6300_RX
, gpio.IBEACON_TX
, gpio.IBEACON_RX
, gpio.A4988_DIR
, gpio.A4988_STP
, gpio.A4988_ENA
, gpio.A4988_MS1
, gpio.OUTPUT_HI
, gpio.OUTPUT_LO
, gpio.DDS2382_TX
, gpio.DDS2382_RX
, gpio.DDSU666_TX
, gpio.DDSU666_RX
, gpio.SM2135_CLK
, gpio.SM2135_DAT
, gpio.DEEPSLEEP
, gpio.EXS_ENABLE
, gpio.TASMOTACLIENT_TXD
, gpio.TASMOTACLIENT_RXD
, gpio.TASMOTACLIENT_RST
, gpio.TASMOTACLIENT_RST_INV
, gpio.HPMA_RX
, gpio.HPMA_TX
, gpio.GPS_RX
, gpio.GPS_TX
, gpio.HM10_RX
, gpio.HM10_TX
, gpio.LE01MR_RX
, gpio.LE01MR_TX
, gpio.CC1101_GDO0
, gpio.CC1101_GDO2
, gpio.HRXL_RX
, gpio.ELECTRIQ_MOODL_TX
, gpio.AS3935
, gpio.ADC_INPUT
, gpio.ADC_TEMP
, gpio.ADC_LIGHT
, gpio.ADC_BUTTON
, gpio.ADC_BUTTON_INV
, gpio.ADC_RANGE
, gpio.ADC_CT_POWER
, gpio.WEBCAM_PWDN
, gpio.WEBCAM_RESET
, gpio.WEBCAM_XCLK
, gpio.WEBCAM_SIOD
, gpio.WEBCAM_SIOC
, gpio.WEBCAM_DATA
, gpio.WEBCAM_VSYNC
, gpio.WEBCAM_HREF
, gpio.WEBCAM_PCLK
, gpio.WEBCAM_PSCLK
, gpio.WEBCAM_HSD
, gpio.WEBCAM_PSRCS
, gpio.BOILER_OT_RX
, gpio.BOILER_OT_TX
, gpio.WINDMETER_SPEED
, gpio.KEY1_TC
, gpio.BL0940_RX
, gpio.TCP_TX
, gpio.TCP_RX
, gpio.ETH_PHY_POWER
, gpio.ETH_PHY_MDC
, gpio.ETH_PHY_MDIO
, gpio.TELEINFO_RX
, gpio.TELEINFO_ENABLE
, gpio.LMT01
, gpio.IEM3000_TX
, gpio.IEM3000_RX
, gpio.ZIGBEE_RST
, gpio.DYP_RX
, gpio.MIEL_HVAC_TX
, gpio.MIEL_HVAC_RX
, gpio.WE517_TX
, gpio.WE517_RX
, gpio.AS608_TX
, gpio.AS608_RX
, gpio.SHELLY_DIMMER_BOOT0
, gpio.SHELLY_DIMMER_RST_INV
, gpio.RC522_RST
, gpio.P9813_CLK
, gpio.P9813_DAT
, gpio.OPTION_A
, gpio.FTC532
, gpio.RC522_CS
, gpio.NRF24_CS
, gpio.NRF24_DC
, gpio.ILI9341_CS
, gpio.ILI9341_DC
, gpio.ILI9488_CS
, gpio.EPAPER29_CS
, gpio.EPAPER42_CS
, gpio.SSD1351_CS
, gpio.RA8876_CS
, gpio.ST7789_CS
, gpio.ST7789_DC
, gpio.SSD1331_CS
, gpio.SSD1331_DC
, gpio.SDCARD_CS
, gpio.ROT1A_NP
, gpio.ROT1B_NP
, gpio.ADC_PH
, gpio.BS814_CLK
, gpio.BS814_DAT
, gpio.WIEGAND_D0
, gpio.WIEGAND_D1
, gpio.NEOPOOL_TX
, gpio.NEOPOOL_RX
, gpio.SDM72_TX
, gpio.SDM72_RX
, gpio.TM1637CLK
, gpio.TM1637DIO
, gpio.PROJECTOR_CTRL_TX
, gpio.PROJECTOR_CTRL_RX
, gpio.SSD1351_DC
, gpio.XPT2046_CS
, gpio.CSE7761_TX
, gpio.CSE7761_RX
, gpio.VL53LXX_XSHUT1
, gpio.MAX7219CLK
, gpio.MAX7219DIN
, gpio.MAX7219CS
, gpio.TFMINIPLUS_TX
, gpio.TFMINIPLUS_RX
, gpio.ZEROCROSS
, gpio.HALLEFFECT
, gpio.EPD_DATA
, gpio.INPUT
, gpio.KEY1_PD
, gpio.KEY1_INV_PD
, gpio.SWT1_PD
, gpio.I2S_OUT_DATA
, gpio.I2S_OUT_CLK
, gpio.I2S_OUT_SLCT
, gpio.I2S_IN_DATA
, gpio.I2S_IN_CLK
, gpio.I2S_IN_SLCT
, gpio.INTERRUPT
, gpio.MCP2515_CS
, gpio.HRG15_TX
, gpio.VINDRIKTNING_RX
, gpio.BL0939_RX
, gpio.BL0942_RX
, gpio.HM330X_SET
, gpio.HEARTBEAT
, gpio.HEARTBEAT_INV
, gpio.SHIFT595_SRCLK
, gpio.SHIFT595_RCLK
, gpio.SHIFT595_OE
, gpio.SHIFT595_SER
, gpio.SOLAXX1_RTS
, gpio.OPTION_E
, gpio.SDM230_TX
, gpio.SDM230_RX
, gpio.ADC_MQ
, gpio.CM11_TXD
, gpio.CM11_RXD
, gpio.BL6523_TX
, gpio.BL6523_RX
, gpio.ADE7880_IRQ
, gpio.RESET
, gpio.MS01
, gpio.SDIO_CMD
, gpio.SDIO_CLK
, gpio.SDIO_D0
, gpio.SDIO_D1
, gpio.SDIO_D2
, gpio.SDIO_D3
, gpio.FLOWRATEMETER_SIGNAL
, gpio.SENSOR_END
An H-bridge is an electronic circuit that switches the polarity of a voltage applied to a load. These circuits are often used in robotics and other applications to allow DC motors to run forwards or backwards.
See the Berry cookbook for H-bridge control
DAC GPIOs~
DAC is limited to specific GPIOs:
- ESP32: only GPIO 25-26
- ESP32-S2: only GPIO 17-18
- ESP32-C3: not supported
Example
> gpio.pin_mode(25, gpio.DAC) # sets GPIO25 to a DAC pin
> gpio.dac_voltage(25, 1250) # set voltage to 1250mV
1255
I2S~
DAC can also be used via Esp8266Audio
through the ESP32 I2S -> DAC bridge.
Example
class MP3_Player : Driver
var audio_output, audio_mp3, fast_loop_closure
def init()
self.audio_output = AudioOutputI2S()
self.audio_mp3 = AudioGeneratorMP3()
self.fast_loop_closure = def () self.fast_loop() end
tasmota.add_fast_loop(self.fast_loop_closure)
end
def play(mp3_fname)
if self.audio_mp3.isrunning()
self.audio_mp3.stop()
end
var audio_file = AudioFileSourceFS(mp3_fname)
self.audio_mp3.begin(audio_file, self.audio_output)
self.audio_mp3.loop() #- start playing now -#
end
def fast_loop()
if self.audio_mp3.isrunning()
if !self.audio_mp3.loop()
self.audio_mp3.stop()
tasmota.remove_fast_loop(self.fast_loop_closure)
end
end
end
end
mp3_player = MP3_Player()
mp3_player.play("/pno-cs.mp3")
energy
module~
The energy
module provides ways to read current energy counters and values (if you're creating your own automation) or updating the energy counters (if you're writing a driver).
It relies on a new Berry feature that provides a direct mapping between the internal C
structure called struct Energy
and the energy
module in Berry.
For example, if you want to read or update an energy value:
> energy.active_power
0
> energy.active_power = 460
> energy.active_power
460
# internally it updates the C value `Energy.active_power[0]` (float)
You don't need to do import energy
since Tasmota does it for you at boot.
The special energy.read()
function dumps all current values to a single map
. Be aware that the object is very long. Prefer accessing individual attributes instead.
List of energy
attributes that you can read or write:
Attribute | Type | Description |
---|---|---|
voltage | float | Voltage (V) for main phase |
voltage_phases | array of float | Voltage (V) as an array of phases |
current | float | Current (A) for main phase |
current_phases | array of float | Current (A) as an array of phases |
active_power | float | Active Power (W) for main phase |
active_power_phases | array of float | Active Power (W) as an array of phases |
reactive_power | float | Reactive Power (W) for main phase |
reactive_power_phases | array of float | Reactive Power (W) as an array of phases |
power_factor | float | Power Factor (no unit) for main phase |
power_factor_phases | array of float | Power Factor (no unit) as an array of phases |
frequency | float | Frequency (Hz) for main phase |
frequency_phases | array of float | Frequency (Hz) as an array of phases |
export_active | float | (kWh) |
export_active_phases | array of float | (kWh) |
start_energy | float | Total previous energy (kWh) |
daily | float | Daily energy (kWh) |
total | float | Total energy (kWh) |
today_delta_kwh | uint32 | (deca milli Watt hours) 5764 = 0.05764 kWh = 0.058 kWh |
today_offset_kwh | uint32 | (deca milli Watt hours) |
today_kwh | uint32 | (deca milli Watt hours) |
period | uint32 | (deca milli Watt hours) |
fifth_second | uint8 | |
command_code | uint8 | |
data_valid | uint8 | 0 if data is valid for main phase |
data_valid_phases | array of uint8 | 0 if data is valid as an array of phases |
phase_count | uint8 | Number of phases (1..8) |
voltage_common | bool | Use single voltage |
frequency_common | bool | Use single frequency |
use_overtemp | bool | Use global temperature as overtemp trigger on internal energy monitor hardware |
today_offset_init_kwh | bool | |
voltage_available | bool | Enable if voltage is measured |
current_available | bool | Enable if current is measured |
type_dc | bool | |
power_on | bool | |
power_history_0 power_history_1 power_history_2 | uint16 | |
power_steady_counter | uint8 | Allow for power on stabilization |
min_power_flag | bool | |
max_power_flag | bool | |
min_voltage_flag | bool | |
max_voltage_flag | bool | |
min_current_flag | bool | |
max_current_flag | bool | |
mplh_counter | uint16 | |
mplw_counter | uint16 | |
mplr_counter | uint8 | |
max_energy_state | uint8 |
Energy driver in Berry~
Since v14.2.0, it is possible to implement an Energy driver in pure Berry. The Berry driver is enabled when an OPTION_A 9
GPIO is configured:
- by default, the energy driver has zero consumption.
- the berry code can is
energy.driver_enabled()
to check if the virtual Berry Energy driver is active (i.e.OPTION_A 9
is configured) - the following values need to be configured:
energy.phase_count
(default1
),energy.voltage
,energy.current
,energy.power_factor
(typically1.0
or less),energy.frequency
(defaultnan
) - the most important value is
energy.active_power
(in Watt) which is added to the daily power consumption
Example test code in autoexec.be
:
if energy.driver_enabled()
energy.phase_count = 1
energy.voltage = 240
energy.power_factor = 1.0
energy.current = 1.5
energy.frequency = 50
energy.active_power = 360
end
wire
object for I2C~
Berry Scripting provides 2 objects: wire1
and wire2
to communicate with both I2C buses.
Use wire1.scan()
and wire2.scan()
to scan both buses:
> wire1.scan()
[]
> wire2.scan()
[140]
You generally use tasmota.wire_scan()
to find a device and the corresponding I2C bus.
MPU6886 on bus 2
> mpuwire = tasmota.wire_scan(0x68, 58)
> mpuwire
<instance: Wire()>
Low-level commands if you need finer control:
path
module~
A simplified version of os.path
module of standard Berry which is disabled in Tasmota because we don't have a full OS.
The default file-system is the ESP32 internal flash. If you have a SD card mounted, it is mapped to the /sd/
subdirectory.
Example:
import path
print(path.listdir("/sd/"))
# outputs a list of filenames at the root dir of the SD card
persist
module~
Easy way to persist simple values in Berry and read/write any attribute. Values are written in JSON format in _persist.json
file. Be aware that persist
cannot detect any change in sub-objects like lists or maps; in such case you can call persist.dirty()
to indicate that data needs to be saved.
Example
> import persist
> persist.a = 1
> persist.b = "foobar"
> print(persist)
<instance: Persist({'a': 1, 'b': 'foobar'})>
> persist.save() # save to _persist.json
introspect
module~
Allows to do introspection on instances and modules, to programmatically list attributes, set and get them.
> class A var a,b def f() return 1 end end
> ins=A()
> ins.a = "foo"
> import introspect
> introspect.members(ins)
['b', 'a', 'f']
> introspect.get(ins, "a")
foo
> introspect.set(ins, "a", "bar")
bar
> ins.a
bar
webclient
class~
Class webclient
provides an implementation of an HTTP/HTTPS web client and make requests on the LAN or over the Internet.
Features:
- Support HTTP and HTTPS requests to IPv4 addresses and domain names, to arbitrary ports, via a full URL.
- Support for HTTPS and TLS via BearSSL (which is much lighter than default mbedTLS)
- HTTPS (TLS) only supports cipher ECDHE_RSA_WITH_AES_128_GCM_SHA256 which is both secure and widely supported
- Support for URL redirections
- Ability to set custom User-Agent
- Ability to set custom headers
- Ability to set Authentication header
- Support for Chunked encoding response (so works well with Tasmota devices)
- Support for
GET
,POST
,PUT
,PATCH
,DELETE
methods
The current implementation is based on a fork of Arduino's HttpClient customized to use BearSSL
Current limitations (if you need extra features please open a feature request on GitHub):
- Payload sent to server (
POST
) can include either text or binary - Only supports text responses (html, json...) but not binary content yet (no NULL char allowed). However you can download binary content to the file-system with
write_file
- Maximum response size is 32KB, requests are dropped if larger
- HTTPS (TLS) is in 'insecure' mode and does not check the server's certificate; it is subject to Man-in-the-Middle attack
- No support for compressed response - this should not be a problem since the client does not advertize support for compressed responses
Example
> cl = webclient()
> cl.begin("http://ota.tasmota.com/tasmota32/release/")
<instance: webclient()>
> r = cl.GET()
> print(r)
200
> s = cl.get_string()
> print(s)
<pre>
<b></b>Alternative firmware for ESP32 based devices with web UI,
[.../...]
Example
> cl = webclient()
> cl.begin("https://raw.githubusercontent.com/tasmota/autoconf/main/esp32/M5Stack_Fire_autoconf.zip")
<instance: webclient()>
> r = cl.GET()
> print(r)
200
> cl.write_file("M5Stack_Fire_autoconf.zip")
950
Managing redirects~
HTTP redirects (301/302) are not followed by default. You can use wc.set_follow_redirects(true)
to have redirects automatically followed for HEAD and GET. There is a default limit of 10 successive redirects, this prevents from infinite loops.
For the examples, we use http://ota.tasmota.com/tasmota32
which is redirected to http://ota.tasmota.com/tasmota32/
Example
cl = webclient()
cl.set_follow_redirects(true)
cl.begin("http://ota.tasmota.com/tasmota32")
r = cl.GET()
print(r)
s = cl.get_string()
print(s)
Alternatively, you can manage yourself redirects and retrieve the Location
header
Example
cl = webclient()
cl.set_follow_redirects(false)
cl.collect_headers("Location")
cl.begin("http://ota.tasmota.com/tasmota32")
r = cl.GET()
print(r)
if r == 301 || r == 302
print("Location:", cl.get_header("Location"))
elif r == 200
s = cl.get_string()
print(s)
end
cl.close()
Main functions:
Request customization:
Static utility methods:
webclient static method | Parameters and details |
---|---|
url_encode | (url:string) -> string Encodes a string according to URL escape rules. Use before you use begin() |
webserver
module~
Module webserver
provides functions to enrich Tasmota's Web UI. It is tightly linked to Tasmota page layout.
Functions used to add UI elements like buttons to Tasmota pages, and analyze the current request. See above Driver
to add buttons to Tasmota UI.
Low-level functions if you want to display custom pages and content:
Module webserver
also defines the following constants:
- Tasmota's web server states:
webserver.HTTP_OFF
,webserver.HTTP_USER
,webserver.HTTP_ADMIN
,webserver.HTTP_MANAGER
,webserver.HTTP_MANAGER_RESET_ONLY
- Tasmota's pages:
webserver.BUTTON_CONFIGURATION
,webserver.BUTTON_INFORMATION
,webserver.BUTTON_MAIN
,webserver.BUTTON_MANAGEMENT
,webserver.BUTTON_MODULE
- Methods received by handler:
webserver.HTTP_ANY
,webserver.HTTP_GET
,webserver.HTTP_OPTIONS
,webserver.HTTP_POST
See the Berry Cookbook for examples.
tcpclient
class~
Simple tcp client supporting string and binary transfers:
- create an instance of the client with
var tcp = tcpclient()
- connect to the server
tcp.connect(address:string, port:int [, timeout_ms:int]) -> bool
Address can be numerical IPv4 or domain name. Returnstrue
if the connection succeeded. Optionaltimeout
in milliseconds. The default timeout isUSE_BERRY_WEBCLIENT_TIMEOUT
(2 seconds). - check if the socket is connected with
tcp.connected()
- send content with
tcp.write(content:string or bytes) -> int
. Accepts either a string or a bytes buffer, returns the number of bytes sent. It's your responsibility to resend the missing bytes - check if bytes are available for reading
tcp.available() -> int
. Returns0
if nothing was received. This is the call you should make in loops for polling. - read incoming content as string
tcp.read() -> string
or as bytestcp.readbytes() -> bytes
. It is best to calltcp.available()
first to avoid creating empty response objects when not needed - close the socket with
tcp.close()
Full example:
tcp = tcpclient()
tcp.connect("192.168.2.204", 80)
print("connected:", tcp.connected())
s= "GET / HTTP/1.0\r\n\r\n"
tcp.write(s)
print("available1:", tcp.available())
tasmota.delay(100)
print("available1:", tcp.available())
r = tcp.read()
tcp.close()
print(r)
tcpclientasync
class~
Variant of tcpclient
using only non-blocking calls in full asynchronous mode. This allows to have multiple concurrent connections with fine-grained control over timeouts and no blocking of Tasmota. This is especially useful for Matter Border Router for ESP8266 Tasmota based devices via HTTP.
All calls return immediately, so you need to poll the API periodically to send/receive data, and manage timeouts yourself.
Typical equence:
- create an instance of the client with
var tcp = tcpclientasync()
- connect to the server
tcp.connect(address:string, port:int) -> bool
. Address should be numerical IPv4 or IPv6 if you want the call to return immediately (i.e. do DNS resolution ahead of time), otherwise a DNS resolution might take some time and fail. If DNS failed, this call returnsfalse
. - regularly call
connected()
waiting fortrue
to detect when the connection is established. Whileconnected()
returnsnil
then connection is in-progress. Ifconnected()
changes tofalse
then the connection was refused by the host. - if the connection is not established after a definite amount of time, you should declare 'timeout' and call
close()
- to send data: first call
listening()
to ensure that the socket is ready to send data. Note: the socket is always listening when the connection was just established. Then callwrite()
to send you data (string or bytes), this call returns the actual amount of data sent; if it is lower than your content, you need to handle yourself re-sending the remaining data. Note: ensuring that you send less than the MTU should keep you from happening (~1430 bytes max). - to receive data: first call
available()
to check if some data is ready to be received. Then callread()
orreadbytes()
to get the buffer as string or bytes. You can limit the amount of data received, but in such case, the extra data is discarded and lost. - regularly call
connected()
to check if the connection is still up - finally call
close()
to close the connection on your side and free resources. It is implicitly called if the connection was closed from the peer.
Full example:
def try_connect(addr, port)
import string
var tcp = tcpclientasync()
var now = tasmota.millis()
var r = tcp.connect(addr, port)
print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected())))
print(tcp.info())
tasmota.delay(50)
print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected())))
print(tcp.info())
tasmota.delay(150)
print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected())))
print(tcp.info())
tasmota.delay(500)
print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected())))
print(tcp.info())
return tcp
end
tcp = try_connect("192.168.1.19", 80)
tcpserver
class~
Simple tcp server (socket) listening for incoming connection on any port.
- create an instance of the
tcpserver
on a specific port withs = tcpserver(8888)
- periodically call
s.hasclient()
to know if a new client has connected - if the previous returned
true
, callvar c = s.accept()
orvar c = s.acceptasync()
to accept the connection. It returns an instance oftcpclient
ortcpclientasync
; it responds to the same APIs as outgoing TCP connection and allows text and binary transfers. - you can call
c.close()
to close the connection, or callc.connected()
to know if it's still connected (i.e. the client hasn't closed the connection on their side) - close the server with
s.close()
. This will prevent the server from receinving any new connection, but existing connections are kept alive.
Full example:
> s = tcpserver(8888) # listen on port 8888
> s.hasclient()
false
# in parallel connect on this port with `nc <ip_address> 8888`
> s.hasclient()
true # we have an incoming connection
> c = s.accept()
> c
<instance: tcpclient()>
# send 'foobar' from the client
> c.read()
foobar
# send 'foobar2' again from the client
> c.readbytes()
bytes('666F6F626172320A')
> c.close()
# this closes the connection
udp
class~
Class udp
provides ability to send and received UDP packets, including multicast addresses.
You need to create an object of class udp
. Such object can send packets and listen to local ports. If you don't specify a local port, the client will take a random source port. Otherwise the local port is used as source port.
When creating a local port, you need to use udp->begin(<ip>, <port)>
. If <ip>
is empty string ""
then the port is open on all interfaces (wifi and ethernet).
Sending udp packets~
> u = udp()
> u.begin("", 2000) # listen on all interfaces, port 2000
true
> u.send("192.168.1.10", 2000, bytes("414243")) # send 'ABC' to 192.168.1.10:2000, source port is 2000
true
Receive udp packets~
You need to do polling on udp->read()
. If no packet was received, the call immediately returns nil
.
> u = udp()
> u.begin("", 2000) # listen on all interfaces, port 2000
true
> u.read() # if no packet received, returns `nil`
>
> u.read() # if no packet received, returns `nil`
bytes("414243") # received packet as `bytes()`
Simple UDP server printing received packets~
class udp_listener
var u
def init(ip, port)
self.u = udp()
print(self.u.begin_multicast(ip, port))
tasmota.add_driver(self)
end
def every_50ms()
import string
var packet = self.u.read()
while packet != nil
tasmota.log(string.format(">>> Received packet ([%s]:%i): %s", self.u.remote_ip, self.u.remote_port, packet.tohex()), 2)
packet = self.u.read()
end
end
end
# listen on port 2000 for all interfaces
# udp_listener("", 2000)
Send and receive multicast~
IPv4 example, using the udp_listener
listener above.
On receiver side:
udp_listener("224.3.0.1", 2000)
On sender side:
u = udp()
u.begin_multicast("224.3.0.1", 2000)
u.send_multicast(bytes().fromstring("hello"))
# alternatively
u = udp()
u.begin("", 0) # send on all interfaces, choose random port number
u.send("224.3.0.1", 2000, bytes().fromstring("world"))
The receiver will show:
>>> Received packet ([192.168.x.x]:2000): 68656C6C6F
>>> Received packet ([192.168.x.x]:64882): 776F726C64
This works the same with IPv6 using an address like "FF35:0040:FD00::AABB"
mdns
module~
Module import mdns
support for mdns (Multicast DNS, aka Bonjour protocol) announces. This is needed for Matter Wifi support.
This feature requires #define USE_DISCOVERY
compile option (not included in standard builds).
Example (announce of a Matter Wifi device):
import mdns
mdns.start()
mdns.add_service("_matterc","_udp", 5540, {"VP":"65521+32768", "SII":5000, "SAI":300, "T":1, "D":3840, "CM":1, "PH":33, "PI":""})
Addressable leds (WS2812, SK6812)~
There is native support for addressable leds via NeoPixelBus, with support for animations. Currently supported: WS2812, SK6812.
Details are in Berry leds
serial
class~
The serial
class provides a low-level interface to hardware UART. The serial GPIOs don't need to be configured in the template.
Example
# gpio_rx:4 gpio_tx:5
ser = serial(4, 5, 9600, serial.SERIAL_7E1)
ser.write(bytes(203132)) # send binary 203132
ser.write(bytes().fromstring("Hello)) # send string "Hello"
msg = ser.read() # read bytes from serial as bytes
print(msg.asstring()) # print the message as string
Supported serial message formats: SERIAL_5N1
, SERIAL_6N1
, SERIAL_7N1
, SERIAL_8N1
, SERIAL_5N2
, SERIAL_6N2
, SERIAL_7N2
, SERIAL_8N2
, SERIAL_5E1
, SERIAL_6E1
, SERIAL_7E1
, SERIAL_8E1
, SERIAL_5E2
, SERIAL_6E2
, SERIAL_7E2
, SERIAL_8E2
, SERIAL_5O1
, SERIAL_6O1
, SERIAL_7O1
, SERIAL_8O1
, SERIAL_5O2
, SERIAL_6O2
, SERIAL_7O2
, SERIAL_8O2
display
module~
The display
module provides a simple API to initialize the Universal Display Driver with data provided as a string. It is used by autoconf
mechanism.
uuid
module~
The uuid
module allows to generate uuid4 random ids.
> import uuid
> uuid.uuid4()
1a8b7f78-59d8-4868-96a7-b7ff3477d43f
Tasmota Function | Parameters and details |
---|---|
uuid4 | uuid.uuid4() -> string Generates a uuid4 random id as string. |
crc
module~
The crc
module allows to compute crc32/16/8 from bytes() arrays.
> import crc
> crc.crc32(0xFFFFFFFF, bytes("AABBCC"))
-1091314015
> crc.crc16(0xFFFF, bytes("AABBCC"))
20980
> crc.crc8(0xFF, bytes("AABBCC"))
139
tasmota_log_reader
class~
The tasmota_log_reader
class allows you to read and potentially parse the Tasmota logs. It keeps track of what logs were already read in the past and feeds you with new log lines if some are available. It is for example used by the LVGL tasmota_log
widget to display logs on a display.
Note: calling tasmota_log_reader
can be expensive in string allocations, and adds pressure on the garbage collector. Use wisely.
Example:
var lr = tasmota_log_reader()
# do this regularly
var ret = lr.get_log(2) # read at log level 2
if ret != nil
var lines = r.split('\n') # extract as a list of lines
# do whatever you need
end
ULP
module~
The ULP
module exposes the third computing unit of the ESP32, which is a simple finite state machine (FSM) that is designed to perform measurements using the ADC, temperature sensor and even external I2C sensors. This small ultra low power coprocessor can run in parallel to the main cores and in deep sleep mode, where it is capable to wake up the system, i.e. in reaction to sensor measurements. The binding to Berry consists of some lightweight wrapper functions and the communication with the main cores works by accessing the RTC_SLOW_MEM from both sides, which is the same way as in any other ESP32 ULP project.
# simple LED blink example
import ULP
ULP.wake_period(0,500000) # off time
ULP.wake_period(1,200000) # on time
c = bytes("756c70000c006c00000000001000008000000000000000000000000010008072010000d0e5af2c72340040802705cc190005681d10008072e1af8c720100006821008072040000d0120080720800207004000068010005825c0000800405681d00000092680000800505681d0100009268000080000000b0")
ULP.load(c)
ULP.run()
More infos (including suggestions for a toolchain) on the ULP page.
re
regex module~
Use with import re
.
There are two ways to use regex, first is to call directly the module which triggers a compilation of the regex at each call. The second one is to pre-compile the regex once into an object which is much more efficient if you need to use the regex multiple times. Any error in the compilation of the regex pattern yields an exception.
> import re
# first series are all-in-one, patterns are compiled on the fly
> re.search("a.*?b(z+)", "zaaaabbbccbbzzzee")
['aaaabbbccbbzzz', 'zzz']
> re.match("a.*?b(z+)", "aaaabbbccbbzzzee")
['aaaabbbccbbzzz', 'zzz']
> re.split('/', "foo/bar//baz")
['foo', 'bar', '', 'baz']
> re.searchall('<([a-zA-Z]+)>', '<abc> yeah <xyz>')
[['<abc>', 'abc'], ['<xyz>', 'xyz']]
# below are pre-compiled patterns, which is much faster if you use the
# pattern multiple times
> rr = re.compile('<([a-zA-Z]+)>')
> rr.searchall('<abc> yeah <xyz>')
[['<abc>', 'abc'], ['<xyz>', 'xyz']]
> rr = re.compile("/")
> rr
<instance: re_pattern()>
> rr.split("foo/bar//baz")
['foo', 'bar', '', 'baz']
> rr.split("/b")
['', 'b']
Note: for match
and search
, the first element in the list contains the global match of the pattern. Additional elements correspond to the sub-groups (in parenthesis).
The regex engine is based on re1.5 also used in Micropython.
crypto
module~
Module import crypto
support for common cryptographic algorithms.
Currently supported algorithms:
- AES CTR 256 bits - requires
#define USE_BERRY_CRYPTO_AES_CTR
- AES GCM 256 bits
- AES CCM 128 or 256 bits
- AES CBC 128 bits
- Elliptic Curve C25519 - requires
#define USE_BERRY_CRYPTO_EC_C25519
- Elliptic Curve P256 (secp256r1) - requires
#define USE_BERRY_CRYPTO_EC_P256
- HKDF key derivation with HMAC SHA256 - requires
#define USE_BERRY_CRYPTO_HKDF_SHA256
- HMAC SHA256
- MD5
- PKKDF2 with HMAC SHA256 key derivation - requires
#define USE_BERRY_CRYPTO_PBKDF2_HMAC_SHA256
- SHA256
- JWT RS256 (RSASSA-PKCS1-v1_5 with SHA256) - requires
#define USE_BERRY_CRYPTO_RSA
crypto.AES_CTR
class~
Encrypt and decrypt, using AES CTR (Counter mode) with 256 bits keys.
Test vectors from https://datatracker.ietf.org/doc/html/rfc4231
# Test case from https://www.ietf.org/rfc/rfc3686.txt
import crypto
key = bytes("F6D66D6BD52D59BB0796365879EFF886C66DD51A5B6A99744B50590C87A23884")
iv = bytes("00FAAC24C1585EF15A43D875")
cc = 0x000001
aes = crypto.AES_CTR(key)
plain = bytes("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F")
cipher = aes.encrypt(plain, iv, cc)
assert(cipher == bytes("F05E231B3894612C49EE000B804EB2A9B8306B508F839D6A5530831D9344AF1C"))
plain2 = aes.decrypt(cipher, iv, cc)
assert(plain == plain2)
crypto.AES_GCM
class~
Encrypt, decrypt and verify, using AES GCM (Gallois Counter Mode) with 256 bits keys.
Example taken from https://wizardforcel.gitbooks.io/practical-cryptography-for-developers-book/content/symmetric-key-ciphers/aes-encrypt-decrypt-examples.html
import crypto
key = bytes('233f8ce4ac6aa125927ccd98af5750d08c9c61d98a3f5d43cbf096b4caaebe80')
ciphertext = bytes('1334cd5d487f7f47924187c94424a2079656838e063e5521e7779e441aa513de268550a89917fbfb0492fc')
iv = bytes('2f3849399c60cb04b923bd33265b81c7')
authTag = bytes('af453a410d142bc6f926c0f3bc776390')
# decrypt ciphertext with key and iv
aes = crypto.AES_GCM(key, iv)
plaintext = aes.decrypt(ciphertext)
print(plaintext.asstring())
# 'Message for AES-256-GCM + Scrypt encryption'
tag = aes.tag()
print(tag == authTag)
# true
crypto.AES_CCM
class~
Encrypt and decrypt, using AES CCM with 256 bits keys.
Example from Matter:
# raw_in is the received frame
raw_in = bytes("00A0DE009A5E3D0F3E85246C0EB1AA630A99042B82EC903483E26A4148C8AC909B12EF8CDB6B144493ABD6278EDBA8859C9B2C")
payload_idx = 8 # unencrypted header is 8 bytes
tag_len = 16 # MIC is 16 bytes
p = raw[payload_idx .. -tag_len - 1] # payload
mic = raw[-tag_len .. ] # MIC
a = raw[0 .. payload_idx - 1] # AAD
i2r = bytes("92027B9F0DBC82491D4C3B3AFA5F2DEB") # key
# p = bytes("3E85246C0EB1AA630A99042B82EC903483E26A4148C8AC909B12EF")
# a = bytes("00A0DE009A5E3D0F")
n = bytes("009A5E3D0F0000000000000000") # nonce / IV
# mic = bytes("8CDB6B144493ABD6278EDBA8859C9B2C")
# expected cleartext
clr = bytes("05024FF601001536001724020024031D2404031818290324FF0118")
# method 1 - with distinct calls
import crypto
aes = crypto.AES_CCM(i2r, n, a, size(p), 16)
cleartext = aes.decrypt(p)
tag = aes.tag()
assert(cleartext == clr)
assert(tag == mic)
# method 2 - single call
raw = raw_in.copy() # copy first if we want to keep the encrypted version
var ret = crypto.AES_CCM.decrypt1(i2r, n, 0, size(n), raw, 0, payload_idx, raw, payload_idx, size(raw) - payload_idx - tag_len, raw, size(raw) - tag_len, tag_len)
assert(ret)
assert(raw[payload_idx .. -tag_len - 1] == clr)
crypto.AES_CBC
class~
Encrypt and decrypt, using AES CBC with 128 bits keys.
Example:
var b = bytes().fromstring("hello world_____") # 16-byte aligned
var key = bytes().fromstring("1122334455667788") # 16 bytes
var iv = bytes().fromstring("8877665544332211") # 16 bytes
print("data:",b.asstring()) # "hello world_____"
import crypto
aes = crypto.AES_CBC()
aes.encrypt1(key, iv, b)
print("cipher:",b)
iv = bytes().fromstring("8877665544332211")
aes.decrypt1(key, iv, b)
print("decrypted data:",b.asstring()) # "hello world_____"
crypto.EC_C25519
class~
Provides Elliptic Curve C25519 Diffie-Hellman key agreement. Requires #define USE_BERRY_CRYPTO_EC_C25519
Example from test vectors https://www.rfc-editor.org/rfc/rfc7748:
import crypto
# alice side
alice_priv_key = bytes("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
alice_pub_key = bytes("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
assert(crypto.EC_C25519().public_key(alice_priv_key) == alice_pub_key)
# bob side
bob_priv_key = bytes("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb")
bob_pub_key = bytes("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f")
assert(crypto.EC_C25519().public_key(bob_priv_key) == bob_pub_key)
# shared key computed by alice
ref_shared_key = bytes("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742")
alice_shared_key = crypto.EC_C25519().shared_key(alice_priv_key, bob_pub_key)
bob_shared_key = crypto.EC_C25519().shared_key(bob_priv_key, alice_pub_key)
assert(alice_shared_key == ref_shared_key)
assert(bob_shared_key == ref_shared_key)
crypto.EC_P256
class~
Provides Elliptic Curve Prime256 (secp256r1) Diffie-Hellman key agreement and various functions on P256 curve. Requires #define USE_BERRY_CRYPTO_EC_P256
Example:
import crypto
priv = bytes("f502fb911d746b77f4438c674e1c43650b68285dfcc0583c49cd6ed88f0fbb58")
p = crypto.EC_P256()
pub = p.public_key(priv)
assert(pub == bytes("04F94C20D682DA29B7E99985D8DBA6ABEA9051D16508742899835098B1113D3D749466644C47B559DB184556C1733C33E5788AE250B8FB45F29D4CF48FF752C1ED"))
import crypto
priv = bytes("4E832960415F2B5FA2B1FDA75C1A8F3C84BAEB189EDC47211EF6D27A21FC0ED8")
p = crypto.EC_P256()
pub = p.public_key(priv)
assert(pub == bytes("042166AE4F89981472B7589B8D79B8F1244E2EEE6E0A737FFBFED2981DA3E193D6643317E054D2A924F2F56F1BF4BECA13192B27D8566AF379FBBF8615A223D899"))
print("x=",pub[1..32])
print("y=",pub[33..65])
import crypto
p = crypto.EC_P256()
priv_A = bytes("f502fb911d746b77f4438c674e1c43650b68285dfcc0583c49cd6ed88f0fbb58")
pub_A = bytes("04F94C20D682DA29B7E99985D8DBA6ABEA9051D16508742899835098B1113D3D749466644C47B559DB184556C1733C33E5788AE250B8FB45F29D4CF48FF752C1ED")
priv_B = bytes("4E832960415F2B5FA2B1FDA75C1A8F3C84BAEB189EDC47211EF6D27A21FC0ED8")
pub_B = bytes("042166AE4F89981472B7589B8D79B8F1244E2EEE6E0A737FFBFED2981DA3E193D6643317E054D2A924F2F56F1BF4BECA13192B27D8566AF379FBBF8615A223D899")
shared_1 = p.shared_key(priv_A, pub_B)
shared_2 = p.shared_key(priv_B, pub_A)
assert(shared_1 == shared_2)
crypto.HKDF_SHA256
class~
Provides HKDF using HMAC SHA256 key derivation. Turns 'ikm' (input keying material) of low entropy and creates a pseudo random key. Requires #define USE_BERRY_CRYPTO_HKDF_SHA256
Test vectors from https://www.rfc-editor.org/rfc/rfc5869
import crypto
# Test Case 1
hk = crypto.HKDF_SHA256()
ikm = bytes("0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B")
salt = bytes("000102030405060708090A0B0C")
info = bytes("F0F1F2F3F4F5F6F7F8F9")
k = hk.derive(ikm, salt, info, 42)
assert(k == bytes("3CB25F25FAACD57A90434F64D0362F2A2D2D0A90CF1A5A4C5DB02D56ECC4C5BF34007208D5B887185865"))
# Test Case 2
hk = crypto.HKDF_SHA256()
ikm = bytes("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f")
salt = bytes("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf")
info = bytes("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff")
k = hk.derive(ikm, salt, info, 82)
assert(k == bytes("b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87"))
# Test Case 3
hk = crypto.HKDF_SHA256()
ikm = bytes("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
salt = bytes()
info = bytes()
k = hk.derive(ikm, salt, info, 42)
assert(k == bytes("8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"))
crypto.PBKDF2_HMAC_SHA256
class~
Provides PBKDF2 using HMAC SHA256 key derivation. Turns a password into a hash.
Test vectors from https://github.com/brycx/Test-Vector-Generation/blob/master/PBKDF2/pbkdf2-hmac-sha2-test-vectors.md
import crypto
pb = crypto.PBKDF2_HMAC_SHA256()
assert(pb.derive("password", "salt", 1, 20) == bytes('120fb6cffcf8b32c43e7225256c4f837a86548c9'))
assert(pb.derive("password", "salt", 2, 20) == bytes('ae4d0c95af6b46d32d0adff928f06dd02a303f8e'))
assert(pb.derive("password", "salt", 3, 20) == bytes('ad35240ac683febfaf3cd49d845473fbbbaa2437'))
assert(pb.derive("password", "salt", 4096, 20) == bytes('c5e478d59288c841aa530db6845c4c8d962893a0'))
assert(pb.derive("passwd", "salt", 1, 128) == bytes('55AC046E56E3089FEC1691C22544B605F94185216DDE0465E68B9D57C20DACBC49CA9CCCF179B645991664B39D77EF317C71B845B1E30BD509112041D3A19783C294E850150390E1160C34D62E9665D659AE49D314510FC98274CC79681968104B8F89237E69B2D549111868658BE62F59BD715CAC44A1147ED5317C9BAE6B2A'))
crypto.SHA256
class~
Provides SHA256 hashing function
Example test vectors from https://www.dlitz.net/crypto/shad256-test-vectors/
import crypto
h = crypto.SHA256()
# SHA256 of empty message
assert(h.out() == bytes("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
# (first 16 bytes of RC4 keystream where the key = 0)
h.update(bytes("de188941a3375d3a8a061e67576e926d"))
assert(h.out() == bytes("067c531269735ca7f541fdaca8f0dc76305d3cada140f89372a410fe5eff6e4d"))
crypto.HMAC_SHA256
class~
Provides HMAC SHA256 hashing function
Test case from https://datatracker.ietf.org/doc/html/rfc4231:
import crypto
key = bytes("4a656665")
msg = bytes("7768617420646f2079612077616e7420666f72206e6f7468696e673f")
h = crypto.HMAC_SHA256(key)
h.update(msg)
hmac = h.out()
assert(hmac == bytes("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"))
crypto.RSA
class~
Provides RSA core features, currently only JWT RS256 signing (RSASSA-PKCS1-v1_5 with SHA256) - requires #define USE_BERRY_CRYPTO_RSA
Signing a full JWT token with RS256
import string
import crypto
# JWT requires base64url and not raw base64
# see https://base64.guru/standards/base64url
# input: string or bytes
def base64url(v)
import string
if type(v) == 'string' v = bytes().fromstring(v) end
var b64 = v.tob64()
# remove trailing padding
b64 = string.tr(b64, '=', '')
b64 = string.tr(b64, '+', '-')
b64 = string.tr(b64, '/', '_')
return b64
end
# JWT header and claim
var header = '{"alg":"RS256","typ":"JWT"}'
var claim = '{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}'
var b64header = base64url(header)
var b64claim = base64url(claim)
assert(b64header == 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9')
assert(b64claim == 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0')
# `body` is the payload to sign with RS256
var body = b64header + '.' + b64claim
assert(body == 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0')
var private_key =
'-----BEGIN PRIVATE KEY-----\n'+
'MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\n'+
'MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\n'+
'NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\n'+
'qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\n'+
'p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\n'+
'ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\n'+
'VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\n'+
'laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\n'+
'sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\n'+
'mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\n'+
'dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\n'+
'ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\n'+
'DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\n'+
'N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n'+
'0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\n'+
't8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\n'+
'AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n'+
'48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\n'+
'DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\n'+
'xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\n'+
'mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n'+
'2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\n'+
'et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\n'+
'VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\n'+
'TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\n'+
'dn/RsYEONbwQSjIfMPkvxF+8HQ==\n'+
'-----END PRIVATE KEY-----\n'
# public_key for reference but not actually used here
var public_key =
'-----BEGIN PUBLIC KEY-----\n'+
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n'+
'4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n'+
'+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\n'+
'kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n'+
'0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\n'+
'cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\n'+
'mwIDAQAB\n'+
'-----END PUBLIC KEY-----\n'
# read private_key as DER binary
while (private_key[-1] == '\n') private_key = private_key[0..-2] end
var private_key_DER = bytes().fromb64(string.split(private_key, '\n')[1..-2].concat())
# comparison with what was expected
assert(private_key_DER.tob64() == 'MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ==')
# sign body
var body_b64 = bytes().fromstring(body)
var sign = crypto.RSA.rs256(private_key_DER, body_b64)
var b64sign = base64url(sign)
# check output
assert(b64sign == 'NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ')
# Final token:
var jwt_token = payload + '.' + b64sign
crypto.MD5
class~
Provides MD5 hashing function.
Test vector:
import crypto
h = crypto.MD5()
t = bytes().fromstring("The quick brown fox jumps over the lazy dog")
h.update(t)
m = h.finish()
assert(m == bytes("9e107d9d372bb6826bd81d3542a419d6"))
flash
module~
Warning: this is a low-level module used to read and write flash memory. You normally shouldn't need to use it. It is used internally by partition_core
. Use with import flash
.
img
class~
Thin wrapper for image data, that allows format conversions and is able to reduce memory reallocations in certain scenarios.
Supports following image types, which integer values are equal to the enum pixformat_t
of Espressif's webcam driver:
- img.RGB565
= 0
- img.RGB888
= 5
- img.JPEG
= 4
- img.GRAYSCALE
= 3
Create an instance of an image with var i = img()
.
Memory will be released automatically by Berrys garbage collector after deletion of the instance.
img Function | Parameters and details |
---|---|
from_jpeg | img.from_jpeg(jpeg_buffer:bytes[, type:img.type]) -> nil Copy JPEG image as byte buffer to the buffer of an img instance. If optional image type is provided, this will be converted on the fly. This will not reallocate the image buffer, if the size and format does not change. |
from_buffer | img.from_buffer(image_data:bytes,width:int:height:int,type:img.type) -> nil Construct image from raw image data for the types RGB565 , RGB888 and GRAYSCALE . |
get_buffer | img.get_buffer([descriptor:bytes]) -> image_data:bytes Returns the raw image data for any supported type. For RGB565 , RGB888 and GRAYSCALE a descriptor can be provided to get a ROI (region of interest). |
convert_to | img.convert_to(type:img.type) -> nil Internal conversion of the image format. |
info | img.info() -> map Returns a map with some infos about the current image. |
The optional ROI descriptor is a representation of an affine matrix, which can be constructed in Berry:
# Describe ROI using an affine matrix (https://en.wikipedia.org/wiki/Affine_transformation#Image_transformation)
# | scale_x shear_x translation_x |
# | shear_y scale_y translation_y |
# | 0 0 1 | - these are constants in this scope
def roi_dsc(m)
var d = bytes(-24)
d.setfloat(0,m["scaleX"])
d.setfloat(4,m["shearX"])
d.setfloat(8,m["shearY"])
d.setfloat(12,m["scaleY"])
d.seti(16,m["transX"],2)
d.seti(18,m["transY"],2)
d.seti(20,m["width"],2)
d.seti(22,m["height"],2)
return d
end
Example:
# load jpg file into img
var i = img()
var f = open("j.jpg","r")
i.from_jpg(f.readbytes(),img.RGB565) # i now holds image data with type RGB565
f.close()
cam
module~
Very small module to access a connected camera module with the purpose to have as much heap memory available as possible in comparison to the fully fledged webcam drivers for machine learning, but there are more possible applications. It is not intended to be a general replacement for the webcam drivers.
Tasmota Function | Parameters and details |
---|---|
cam.setup | (mode:int) -> bool Init camera hardware with the resolution (same value as command wcresolution ). |
cam.get_image | ([image:img[,type:img.type]]) -> bytes or nil Takes a picture - without an additional option this is just a JPEG buffer. If an image instance is provided, the image data will go there. If an additional type is given, a conversion will happen on the fly. This will not lead to a memory reallocation, if there is no change for size and type of the image. |
cam.info | () -> map Shows info map with last current resolution and camera mode |
Example:
# Simple "video player" for boards with a camera and a display
scr = lv.scr_act()
scr.set_style_bg_color(lv.color(lv.COLOR_BLUE), lv.PART_MAIN | lv.STATE_DEFAULT)
# create a lv_img object as image view
cam_view = lv.img(scr)
cam_view.center()
i = img()
import cam
cam.setup(4) # 240 x 240
cam.get_image(i,i.RGB565)
def lv_img_dsc(image)
var i = image.info()
var dsc = bytes(24)
dsc..0x19 # magic
dsc..0x12 # cf RGB565
dsc.add(0,2) # flags
dsc.add(i["width"],2) # width
dsc.add(i["height"],2) # height
dsc.add(i["width"] * 2,2) # stride
dsc.add(0,2) # reserved
dsc.add(i["size"],4) # size
dsc.add(i["buf_addr"],4) # data
dsc.add(0,4) # reserved
print(dsc)
return dsc
end
descriptor = lv_img_dsc(i)
cam_view.set_src(descriptor) # bind cam_view to buffer of img
def video()
cam.get_image(i,i.RGB565) # this will just update the buffer, no reallocation
cam_view.invalidate()
tasmota.set_timer(20,/->video()) # aim for 50 Hz
end
video()
Philips Hue emulation for Alexa~
Berry extends the native Hue/Alexa emulation and makes it possible to handle any number of virtual lights. You can easily define "virtual" lights in Berry, respond to commands from Alexa and send light status.
It is up to you to define the final behavoir. For example you could control some fancy devices, light strips or whatever takes on/off, dimmer or RGB commands. Your imagination is the limit.
Hue emulation requires both #define USE_EMULATION
and #define USE_EMULATION_HUE
. Emulation must also be enabled with Emulation 2
command.
light_state
class~
The core class is light_state
which represents a virtual light.
light_state
general methods:
light_state
getters:
Attributes | Parameters and details |
---|---|
power | (bool) on/off state |
reachable | (bool) light is reachable |
type | (int) number of channels of the light |
bri | (int) brightness of the light (0..255) |
ct | (int) white temperature of the light (153..500) |
sat | (int) saturation of the light (0..255) |
hue | (int) hue of the light (0..360) |
hue16 | (int) hue as 16 bits (0..65535) |
r g b | (int) Red Green Blue channels (0..255) |
x y | (float) x/y color as floats (0.0 .. 1.0) |
mode_ct mode_rgb | (bool) light is in RGB or CT mode |
get | get() -> map returns the complete state of the light as a mapExemple: {'rgb': '1E285A', 'hue': 230, 'type': 5, 'power': false, 'bri': 90, 'mode_rgb': true, 'sat': 170, 'mode_ct': false, 'channels': [30, 40, 90, 0, 0]} |
light_state
setters:
Methods | Parameters and details |
---|---|
set_power | set_power(bool) -> nil sets on/off state |
set_reachable | set_reachable(bool) -> nil sets the reachable state |
set_bri | set_bri(int) -> nil sets the brightness (0..255) |
set_ct | set_ct(int) -> nil sets the white temperature (153..500) |
set_sat | set_sat(int) -> nil sets the saturation (0..255) |
set_huesat | set_huesat(hue:int, sat:int) -> nil sets hue and saturation (0..360, 0..255) |
set_hue16sat | set_hue16sat(hue16:int, sat:int) -> nil sets hue16 and saturation (0..65535, 0..255) |
set_rgb | set_rgb(r:int, g:int, b=int) -> nil sets red/green/blue channels (0..255 x 3) |
set_xy | set_xy(x:float, y:float) -> nil sets color as x/y (0.0 .. 1.0 x 2) |
light_state
static helper functions:
Methods | Parameters and details |
---|---|
gamma8 | gamma8(int) -> nil applies gamma correction to 8 bits value (0..255) |
gamma10 | gamma10(int) -> nil applies gamma correction to 10 bits value (0..1023) |
reverse_gamma10 | reverse_gamma10(int) -> nil applies reverse gamma correction to 10 bits value (0..1023) |
hue_bridge
module~
Use import hue_bridge
and declare all the virtual lights. Example:
# put this in `autoexec.be`
import hue_bridge
l1 = light_state(light_state.DIMMER)
hue_bridge.add_light(11, l1, "Synthetic Dimmer", "V1", "Tasmota Factory")
l2 = light_state(light_state.CT)
hue_bridge.add_light(12, l2, "Synthetic CT", "V1", "Tasmota Factory")
l5 = light_state(light_state.RGBCT)
hue_bridge.add_light(15, l5, "Synthetic RGBCT")
When you start the Hue pairing, all virtual lights are advertized. You need to make sure that virtual lights are defined at each restart (in autoexec.be
for example).
hue_bridge
functions:
Methods | Parameters and details |
---|---|
add_light | add_light(id:int, light:instance of light_state, name:string [, model:string, manuf:strin]) -> light Adds an virtual light to the Hue bridge. id = numerical identifier of the Hue light. Using low numbers avoids conflict with real lights from Tasmotalight = instance of light_state handling the state and behavior of the lightname = name of the light as displayed in the Alexa app (can be overriden in the app)model (opt) = name of the manufacturer model, defaults to "Unkwnon"manuf (opt) = name of the manufacturer, defaults to "Tasmota" |
remove_light | remove_light(id:int) -> nil Removes a light from the Hue bridge by hue id. |
light_to_id | light_to_id(light:instance) -> int converts a registered light_instance instance to its Hue id |
Zigbee~
For Zigbee coordinators, there is a Berry mapping that allows explore Zigbee configurations and devices. It also allows to intercept incoming message (low and high level) and transform messages before they reach the Tasmota layer. This is useful for non-standard Zigbee devices for which Zigbee plug-ins are not sufficient.
Note: the following are only available when compiling with #define USE_ZIGBEE
Internally, the Tasmota Zigbee engine calls callBerryZigbeeDispatcher()
at key points to allow your Berry code to take over and change messages on-the-fly.
import zigbee
~
First step is to use import zigbee
which returns an instance (monad) of zb_coord()
.
General methods | Parameters and details |
---|---|
started | zigbee.started() -> bool or nil Returns true if zigbee sucessfully started, then all other zigbee methods are available. This state is final and does not change.Returns false if zigbee is still in initialization process. This state eventually changes to true or nil .Returns nil if zigbee is not configured (no GPIO) or if initialization failes. This state is final and indicates a fatal error. |
info | zigbee.info() -> map returns a map with general configuration of the Zigbee coordinator.Format is identical to ZbConfig Example: {'ext_pan_id': '0xCCCCCCCCA11A2233', 'tx_radio': 20, 'shortaddr': 0, 'longaddr': '0x00124B0026BAABBC', 'channel': 11, 'pan_id': 837, 'pan_id_hex': '0x0345', 'shortaddr_hex': '0x0000'} |
size | zigbee.size() -> int returns the number of devices knwon by the coordinator |
iter | zigbee.iter() -> iterator Returns an iterator on all zigbee devices Use compact implicit form: for ze: zigbee print(ze) end |
item [] | zigbee.item(shortaddr:int | friendlyname:str) -> instance of zb_device Returns the Zigbee device corresponding to short address shortaddr or to friendlyname friendlyname .Returns an index_error exception if not found.You can use the compact syntax zigbee[0xFAB6] |
find | zigbee.find(shortaddr:int | friendlyname:str) -> instance of zb_device Returns the Zigbee device corresponding to short address shortaddr or to friendlyname friendlyname .Contrary to the above, returns nil if not found (no exception). |
abort | zigbee.abort() -> nil aborts the initialization of Zigbee MCU. To be used when initialization of Zigbee failed |
zb_device
class~
The class zb_device
contains all known information about a paired Zigbee device (end-device or router). You can't create a zb_device
from scratch, they most be retrieved from zigbee
object.
General methods | Parameters and details |
---|---|
info | info() -> attribute_list or nil Returns the last known state for this device as an attribute_list This is equivalent of running `ZbInfo |
zb_device
instances can only be read, you can't change directly any attribute.
Instance Variables | Parameters and details |
---|---|
shortaddr | shortaddr -> int returns the 16 bits short address |
longaddr | longaddr -> bytes returns the long 64 bits address as 8 bytes (or all zeroes if unknown) |
name | name -> string returns the friendlyname of the device or 0x.... hex name if no friendlyname was defined using ZbName command |
reachable | reachable -> bool is the device reachable, i.e. did it respond last time we tried to contact them |
hidden | hidden -> bool is the device declared as hidden, i.e. not announced in Hue emulation |
router | router -> bool is the device known to be a router |
model | model -> string model of the device |
manufacturer | manufacturer -> string manufacturer name of the device |
lastseen | lastseen -> int timestamp (epoch) when the device was last seen |
lqi | lqi -> int radion strength and quality when the device was last seen |
battery | battery -> int percentage of battery, or -1 if unknwon of no battery |
battery_lastseen | battery_lastseen -> int timestamp (epoch) when the battery was last reported, or -1 |
Example:
import zigbee
# show all devices
for device: zigbee
print(device)
end
#
# outputs:
# <instance: zb_device(0x868E, 0x00124B001F841E41, name:'Bedroom', model:'TH01', manufacturer:'eWeLink')>
# ... more devices
# read one device by short address
var device = zigbee[0x868E]
print(device.longaddr)
# bytes('411E841F004B1200')
print(device.reachable)
# false - because it's a sleep device
print(device.router)
# false - it's a sleepy device so not a router
print(device.manufacturer, device.model)
# eWeLink TH013000_g5xawfcq')>
# example with a plug
device = zigbee[0xC1BC]
print(device.longaddr, device.reachable, device.router)
# bytes('859F4E001044EF54') true false
print(device.manufacturer, device.model)
# LUMI lumi.plug.maeu01
Changing Zigbee values on-the-fly~
Whenever a Zigbee message is received (typically values of attributes), the Tasmota Zigbee engines generates events at key points which allow custom Berry code to intercept and change messages on-the-fly.
Messages are sent in the following order:
frame_received
: (low-level) the raw zigbee message is passed asbytes
and attributes are not yet decoded. Thebytes
buffer can be modified and passed back to the Tasmota Zigbee engine.attributes_raw
: (mid-level) Zigbee attributes are decoded but no transformation is applied yet. Attributes are only available in cluser/attribute format, names are not decoded and plug-ins are not yet applied.
This is the perfect moment to change non-standard attributes and map them to standard ones.attributes_refined
: (high-level) Attributes are mapped to their names (when possible) and all transformations are applied. This is the last chance to change values.attributes_final
: (high-level) consolidatedattributes_refined
. It is triggered just before final and consolidated attributes are sent to MQTT. Zigbee typically waits for 350ms before sending attributes, so it can consolidate multiple sensors (like temperature + humidity + pressure) in a single MQTT message
The format of methods are the following: def <zigbee event>(event_type, frame, attr_list, idx)
Argument | Description |
---|---|
event_type | (string) can take values: frame_received , attributes_raw or attributes_refined |
frame | (instance of zcl_frame ) low-level ZCL frameAlways present |
attr_list | (instance of XXX ) list of attributes.This attribute is nil for frame_received , contains raw attributes in attributes_raw and refined attributes in attributes_refined |
idx | (int 16 bits unsigned) contains the Zigbee short address |
Example, if you want to dump all the traffic passed:
import zigbee
class my_zb_handler
def frame_received(event_type, frame, attr_list, idx)
print(f"shortaddr=Ox{idx:04X} {event_type=} {frame=}")
end
def attributes_raw(event_type, frame, attr_list, idx)
print(f"shortaddr=Ox{idx:04X} {event_type=} {attr_list=}")
end
def attributes_refined(event_type, frame, attr_list, idx)
print(f"shortaddr=Ox{idx:04X} {event_type=} {attr_list=}")
end
def attributes_final(event_type, frame, attr_list, idx)
print(f"shortaddr=Ox{idx:04X} {event_type=} {attr_list=}")
end
end
var my_handler = my_zb_handler()
zigbee.add_handler(my_handler)
# example of reading for a plug
#
# shortaddr=OxC1BC event_type=frame_received frame={'srcendpoint': 21, 'transactseq_set': 0, 'shortaddr': 49596, 'dstendpoint': 1, 'payload': bytes('5500003956CE8243'), 'shortaddr_hex': '0xC1BC', 'manuf': 0, 'payload_ptr': <ptr: 0x3ffccb5c>, 'need_response': 0, 'transactseq': 25, 'cmd': 1, 'direct': 0, 'cluster': 12, 'cluster_specific': 0, 'groupaddr': 0}
# shortaddr=OxC1BC event_type=attributes_raw attr_list={"000C/0055":261.612,"Endpoint":21,"LinkQuality":21}
# shortaddr=OxC1BC event_type=attributes_refined attr_list={"ActivePower":261.612,"(ActivePower)":"0B04/050B","Endpoint":21,"LinkQuality":21}
# shortaddr=OxC1BC event_type=attributes_final attr_list={"ActivePower":261.612,"(ActivePower)":"0B04/050B","Endpoint":21,"LinkQuality":21}
# to remove handler:
# zigbee.remove_handler(my_handler)
The attr_list
is of class zcl_attribute_list
and can be accessed with zigbee.zcl_attribute_list
.
Methods | Parameters and details |
---|---|
size | size() -> int Number of attributes in the list |
remove | remove(index:int) -> nil Remove the item at index |
item [x] | item(index:int) -> instance or [index:int] -> instance Retrieve attribute at index , or nil if none.Note: contrary to native list it does not throw an exception if the index if off bounds. |
new_head | new_head(attribute:instance of zigbee.zcl_attribute_list) -> self Adds a new attribute at the beginning (head) of the list |
new_tail | new_tail(attribute:instance of zigbee.zcl_attribute_list) -> self Adds a new attribute at the end (tail) of the list |
Variables of zcl_attribute_list
for the entire list and common to all attributes:
Attributes (read or write) | Details |
---|---|
groupaddr | uint16 group address if the message was multicast, or nil |
src_ep | uint8 source endpoint of the message |
lqi | uint8 lqi for the message received (link quality) |
The zcl_attribute_list
contains a list of zcl_attribute
instance.
Attributes (read or write) | Details |
---|---|
cluster | uint16 ZCL cluster number |
attr_id | uint16 ZCL attribute id |
cmd | uint8 ZCL command number |
direction | 0 or 1 ZCL direction of the message (to or from the coordinator) |
cmd_general | 0 or 1 ZCL flag indicating a general command vs a cluster specific command |
key | string or nil attribute name (if any) or nil |
val | any ZCL value of the attribute, can be int/float/string/bytes... |
key_suffix | uint8 key suffix in case a same attribute is repeatedLike Power1 , Power2 ... |
manuf | uint16 ZCL manufacturer specific code or 0 if noneThis is typically indicating a proprietary attribute |
attr_multiplier | int multiplier to be applied or 1 |
attr_divider | int divider to be applied or 1 |
attr_base | int offset to be applied or 0 |
attr_type | uint8 ZCL type byte for the received attribute |
zcl_attribute_list
methods:
Methods | Parameters and details |
---|---|
tomap | tomap() -> map Transforms main attributes as map (read-only): cluster , attr_id , cmd , direction , key , val |
Changing attributes received~
For events attributes_raw
and attributes_refined
, you receive an instance of attr_list
which represents all the attributes received. This list can be modified according to specificities of devices, hence giving full liberty on decoding exotic protocols or manufacturers.
The decoding is done in 2 steps:
-
attributes_raw
contains individual attributes with their native raw values. Names are not yet matched, nor scale factors applied. This is where you want to decode non-standard protocols Example:{"000C/0055":261.612,"Endpoint":21,"LinkQuality":21}
represents raw value from a plug; the value was decoded as float. -
attributes_refined
contains a similar list with additional decoding handled, any scale factor applied (like transforming integer temperature in 1/100 of Celsius to afloat
), and human readable names attached. Example:{"ActivePower":261.612,"(ActivePower)":"0B04/050B","Endpoint":21,"LinkQuality":21}
In this example, the attribute is0B04/050B
is rename asActivePower
, but the original0B04/050B
attribute cluster/id is still readable. We can see that the generic000C/0055 (AnalogValue)
fromlumi.plug.maeu01
is replaced with0B04/050B (ActivePower)
.
Changing zigbee frame, zcl_frame
class~
The zcl_frame
represents a low-level ZCL (Zigbee Cluster Library) structure before any decoding or specific processing. You generally prefer to modify a frame later on when attributes or commands are decoded.
class zcl_frame
:
Attributes (read or write) | Details |
---|---|
srcendpoint | uint8 source endpoint |
dtsendpoint | uint8 destination endpoint |
shortaddr | uint16 destination short address |
groupadddr | uint16 destination multicast group address (if shortaddr is 0xFFFE) |
cluster | uint16 cluster number |
cmd | uint8 ZCL command number |
cluster_specific | flag 0/1 is the command general or cluster specific |
manuf | uint16 manufacturer specific number (or 0x0000) |
needs_response | flag 0/1 does this frame needs a response |
payload | bytes() bytes of the actual data (use with caution, can be read and changed) |
The following are rarely used flags | |
direct | flag 0/1 is the frame to be sent directly only (not routed) |
transactseq | uint8 transaction number (read only) |
transactseq_set | uint8 transaction number (write only - if you need to change it) |
Example:
frame_received frame_received {'srcendpoint': 21, 'transactseq_set': 0, 'shortaddr': 49596, 'dstendpoint': 1, 'payload': bytes('550039D5787B43'), 'shortaddr_hex': '0xC1BC', 'manuf': 4447, 'payload_ptr': <ptr: 0x3ffd4d04>, 'need_response': 0, 'transactseq': 60, 'cmd': 10, 'direct': 0, 'cluster': 12, 'cluster_specific': 0, 'groupaddr': 0} nil 49596
Compiling Berry~
Berry is included if the following is defined in user_config_override.h
:
#define USE_BERRY
Other options that can be changed:
Option | Description |
---|---|
#define USE_BERRY_PSRAM | Use PSRAM to allocate memory instead of main RAM. If no PSRAM is connected, this option has no effect. Enabled by default |
#define USE_BERRY_DEBUG | Provide additional information in case of a Berry exception, adding line number in the call chain. This feature adds ~8% of memory consumption to Berry compiled code. Disabled by default |
#define USE_WEBCLIENT | Enable the webclient module allowing to do HTTP requests.Enabled by default |
#define USE_WEBCLIENT_HTTPS | Adds support for HTTPS to webclient . This feature adds ~45KB of Flash space for TLS support.Disabled by default |
#define USE_BERRY_WEBCLIENT_USERAGENT "TasmotaClient" | Specifies the default User-Agent field sent by webclient . Can be changed on a per request basis. |
#define USE_BERRY_WEBCLIENT_TIMEOUT 5000 | Specifies the default timeout in millisecond for webclient . Can be changed on a per request basis. |
Berry Cookbook~
Find complete examples and use scenarios of Berry in the Berry Cookbook