!!! info "Berry Scripting is included in all tasmota32
builds. It is NOT supported on ESP82xx"
!!! tip "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
!!! tip "If you're new to Berry, have a look at Berry Introduction (in 20 minutes of less)"
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.
!!! tip "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 I^2^C drivers
- build complete Tasmota drivers
- integrate native libraries like
lvgl
see LVGL
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.
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.
Click on Configuration then Berry Scripting Console and enjoy the colorful Berry console, also called REPL (Read-Eval-Print-Loop).
!!! tip "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.
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 ++arrow-up++ and ++arrow-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
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.
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
.
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.
!!! example "2 relays and 1 light"
```berry
> 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]}
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 |
!!! example "Dimmer rule"
Define the function and add a rule to Tasmota where the function runs if Dimmer value is more than 50
```berry
> def dimmer_over_50()
print("The light is bright")
end
tasmota.add_rule("Dimmer>50", dimmer_over_50)
```
```berry
> 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 )
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 )
would trigger if 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 )
would trigger for simultaneous 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
The 3rd argument msg
remains unchanged.
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
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) |
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
!!! example "Sending a timer is as easy as tasmota.set_timer(<delay in ms>,<function>)
"
```berry
> def t() print("Booh!") end
> tasmota.set_timer(5000, t)
[5 seconds later]
Booh!
```
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.
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.
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.
!!! note "You don't need to prefix with /
. A leading /
will be added automatically if it is not present."
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")
.
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.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)
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
Logs a message to the Tasmota console. Optional second argument is log_level (0..4), default is 2
LOG_LEVEL_INFO
.
!!! example
```
> log("A")
A
```
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).
When loading a source file, the precompiled bytecode is saved to filesystem using the .bec
extension.
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()
.
A root level object called tasmota
is created and contains numerous functions to interact with Tasmota.
See examples in the Berry-Cookbook
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.
!!! warning "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 |
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(payloas_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 returnnil
or nothing, it is considered astrue
which is the usual behavior you want (i.e. not trigger a Tasmota command from random MQTT messages).
Module light
is automatically imported via a hidden import light
command.
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.
??? note "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 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
The function returns the closest voltage found. In this case it's 1255 for setting to 1250.
DAC can also be used via Esp8266Audio
through the ESP32 I2S -> DAC bridge.
??? example ```berry 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")
```
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.
Tasmota Function | Parameters and details |
---|---|
energy.read() | () -> map Returns all current values for the energy module. Some values may be unused by the current driver. |
List of energy
attributes that you can read or write:
Attribute | Type | Description |
---|---|---|
voltage voltage_2 voltage_3 |
float | Voltage (V) for main phase or 3 phases |
current current_2 current_3 |
float | Current (A) for main phase or 3 phases |
active_power active_power_2 active_power_3 |
float | Active Power (W) for main phase or 3 phases |
reactive_power reactive_power_2 reactive_power_3 |
float | Reactive Power (W) for main phase or 3 phases |
power_factor power_factor_2 power_factor_3 |
float | Power Factor (no unit) for main phase or 3 phases |
frequency frequency_2 frequency_3 |
float | Frequency (Hz) for main phase or 3 phases |
export_active export_active_2 export_active_3 |
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 data_valid_2 data_valid_3 |
uint8 | |
phase_count | uint8 | Number of phases (1,2 or 3) |
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 | |
Below if for Energy Margin Detection | ||
power_history_0 power_history_0_2 power_history_0_3 power_history_1 power_history_1_2 power_history_1_3 power_history_2 power_history_2_2 power_history_2_3 |
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 | |
Below if for Energy Power Limit | ||
mplh_counter | uint16 | |
mplw_counter | uint16 | |
mplr_counter | uint8 | |
max_energy_state | uint8 |
Berry Scripting provides 2 objects: wire1
and wire2
to communicate with both I^2^C 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 I^2^C bus.
!!! example "MPU6886 on bus 2"
```
> mpuwire = tasmota.wire_scan(0x68, 58)
> mpuwire
<instance: Wire()>
```
Low-level commands if you need finer control:
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
Easy way to persist simple values in Berry and read/write any attribute. Values are written in JSON format in _persist.json
file.
!!! example
> import persist
> persist.a = 1
> persist.b = "foobar"
> print(persist)
<instance: Persist({'a': 1, 'b': 'foobar'})>
> persist.save() # save to _persist.json
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
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
``` berry
> 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 of downloading a file from Github
``` berry
> 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
```
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 of following redirects
``` berry
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 of retrieving Location
``` berry
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() |
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.
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)
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)
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()
to accept the connection. It returns an instance oftcpclient
as above; 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
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).
> 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
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()`
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)
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"
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":""})
There is native support for addressable leds via NeoPixelBus, with support for animations. Currently supported: WS2812, SK6812.
Details are in Berry leds
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
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.
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. |
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
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
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.
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.
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
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)
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
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)
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_____"
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)
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)
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"))
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'))
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"))
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"))
Provides RSA core features, currently only JWT RS256 signing (RSASSA-PKCS1-v1_5 with SHA256) - requires #define USE_BERRY_CRYPTO_RSA
!!! example "Signing a full JWT token with RS256"
``` berry
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
```
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"))
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.
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) |
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 |
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.
First step is to use import zigbee
which returns an instance (monad) of zb_coord()
.
General methods | Parameters and details |
---|---|
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) -> instance of zb_device Returns the Zigbee device with short address shortaddr You can use the compact syntax zigbee[0xFAB6] |
abort | zigbee.abort() -> nil aborts the initialization of Zigbee MCU. To be used when initialization of Zigbee failed |
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.
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
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.
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
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}
# 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 |
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)
.
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
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. |
Find complete examples and use scenarios of Berry in the Berry Cookbook