diff --git a/.gitignore b/.gitignore index 803584d..de9a008 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +dependencies.lock build/ sdkconfig.old @@ -7,5 +8,6 @@ main/spiffs/*.pem.key main/spiffs/*.pem.crt components/mruby_component/esp32_build_config.rb.lock +managed_components .vscode diff --git a/.gitmodules b/.gitmodules index 394fc6b..0de3cab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "mruby"] path = components/mruby_component/mruby url = https://github.com/mruby/mruby.git +[submodule "components/esp_littlefs"] + path = components/esp_littlefs + url = https://github.com/joltwallet/esp_littlefs.git diff --git a/README.md b/README.md index 5a966c6..e05e9cd 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,112 @@ -# Example of mruby on the ESP32 +# mruby on the ESP32 -Before you get started you will need to follow the setup documentation from -the [esp-idf](https://github.com/espressif/esp-idf/tree/master/docs) project -for your specific operating system. +This is an ESP-IDF project template running mruby on the ESP32 microcontroller. -I have only tested this on macOS and using a certain version of -[esp-idf](https://github.com/espressif/esp-idf/tree/release/v5.0). -You should try to use [more recent version](https://github.com/espressif/esp-idf#setting-up-esp-idf) if you have failed. +To get started, you need to install the ESP-IDF, by following the instructions +[here](https://docs.espressif.com/projects/esp-idf/en/release-v5.1/esp32/get-started/index.html), +for your operating system. -You will need to recursively clone this project with the recursive flag -because it includes mruby as a submodule: +This has been tested on macOS and Ubuntu Linux, using +[ESP-IDF 5.1](https://github.com/espressif/esp-idf/tree/release/v5.1). + +## Usage + +Recursively clone this repo to ensure the mruby (3.2.0) submodule gets downloaded: ``` git clone --recursive https://github.com/mruby-esp32/mruby-esp32.git ``` -The main ruby program can be found in the `main/simplest_mrb.rb` file. The -makefile configuration in `main/component.mk` and the main entry point source -file `mruby_main.c` will also be of interest if you want to change the name of -the ruby script. The examples included are very simple scripts that only print -to the ESP32's debug console. +The makefile configuration is in `main/component.mk`. The entry point source +file is `mruby_main.c`. Once that starts, it looks for `storage/main.rb` and runs it in mruby. +You can change the expected filename in `mruby_main.c`, or simply save your scripts as `main.rb` +inside the `storage` subfolder. -I'm assuming you have followed all the steps in the install documentation and -are at least somewhat familiar with the building steps. With that in mind you -can do something like the following and see the example running: +### First Build ``` -cp main/examples/$(YOU_WISH_TO_TRY_FILE) main/spiffs/main.rb idf.py build idf.py -p $(YOUR_SERIAL_PORT) flash monitor ``` +Your ESP32 should write 2 lines of numbers to the console. -The valiable `YOU_WISH_TO_TRY_FILE` can be replaced with one of the following: +### Examples - * _simplest_mrb.rb_ - Simply prints two strings - * _gpio.rb_ - An example of using GPIO - * _wifi_example_mrb.rb_ - An example of connecting to WiFi, you will need to - modify this file to include your SSID and password - * _mqtt_publish.rb_ - An sample of publishing to MQTT broker - * _system_mrb.rb_ - Examples of most of the system APIs +The folder `main/examples` includes simple scripts demonstrating functionality. -The clean command will clean both the ESP32 build and the mruby build: +Once you are familiar with the build process, try them with: ``` -idf.py fullclean +cp main/examples/$(EXAMPLE_FILENAME) main/storage/main.rb +idf.py build +idf.py -p $(YOUR_SERIAL_PORT) flash monitor ``` -There are multiple GEMS that can be turned on and off via the mruby +Replace `EXAMPLE_FILENAME` with one of the following: + + * `simplest.rb` - Prints two strings + * `system.rb` - Demonstrates most system APIs + * `gpio.rb` - GPIO blink example + * `wifi_connect.rb` - Connects to WiFi + * `wifi_socket.rb` - Connects to WiFi and makes a TCPSocket connection + * `mqtt_publish.rb` - Connects to WiFi and publishes to MQTT broker + * `filesystem.rb` - Write/append/read a file on the virtual filesystem + * `ledc_breathe.rb` - Gradually fades the brightness of an LED up and down + * `ledc_buzzer.rb` - Plays a melody on a piezo-electric buzzer + * `ledc_servo.rb` - Controls position of a 180 degree hobby servo motor + +**Note**: Edit GPIO numbers to match ones you are connected to, insert your WiFI credentials, customize MQTT settings etc. + +### Build Customization + +There are multiple gems that can be turned on and off via the mruby configuration file found in `components/mruby_component/esp32_build_config.rb`: -* _mruby-esp32-system_ - ESP32 system calls -* _mruby-esp32-wifi_ - ESP32 WiFi -* _mruby-esp32-mqtt_ - ESP32 MQTT library +* [_mruby-socket_](https://github.com/mruby-esp32/mruby-socket/tree/0.5) - ESP32 Socket library (modified from mruby) +* [_mruby-esp32-system_](https://github.com/mruby-esp32/mruby-esp32-system/tree/0.5) - ESP32 system calls +* [_mruby-esp32-wifi_](https://github.com/mruby-esp32/mruby-esp32-wifi/tree/0.5) - ESP32 WiFi +* [_mruby-esp32-mqtt_](https://github.com/mruby-esp32/mruby-esp32-mqtt/tree/0.5) - ESP32 MQTT library +* [_mruby-esp32-gpio_](https://github.com/mruby-esp32/mruby-esp32-gpio/tree/0.5) - ESP32 GPIO library +* [_mruby-esp32-ledc_](https://github.com/mruby-esp32/mruby-esp32-ledc/tree/0.5) - ESP32 LEDC (PWM) library + +To get gem changes to reflect in the build, `fullclean` the previous build, then build again: +``` +idf.py fullclean +idf.py build +``` + +All gems are enabled by default, so you can try out the examples, but it's a good idea to disable ones you don't need. + +## Hardware + +Everything works on: +- Original ESP32: `idf.py set-target esp32` + +Everything except gpio gem works on: +- ESP32-S2: `idf.py set-target esp32s2` +- ESP32-S3: `idf.py set-target esp32s3` + +If you followed the IDF installation instructions correctly for your chip, +you can switch the project target with the corresponding command above. + +You will probably not be able to build again, until the project partition table is reset to `partitions.csv`. To do this: + +``` +idf.py menuconfig +# Partition Table -> Partition Table (1st option) -> Custom partition Table CSV (Last options) +# Enter to select. Q to exit. Y to save +``` + +## Troubleshooting + +The following files and folders are safe to delete when trying to solve build issues: +- `build` +- `components/mruby_component/build` +- `components/mruby_component/esp32_build_config.rb.lock` +- `managed_components` +- `dependencies.lock` +This project uses [littlefs](https://github.com/littlefs-project/littlefs) through IDF component +manager. If you see errors about files on disk changing, try deleting the last 2 items on this list. diff --git a/components/esp_littlefs b/components/esp_littlefs new file mode 160000 index 0000000..8fb0290 --- /dev/null +++ b/components/esp_littlefs @@ -0,0 +1 @@ +Subproject commit 8fb0290e329ce84064e793755a88f48fd3ee4857 diff --git a/components/mruby_component/CMakeLists.txt b/components/mruby_component/CMakeLists.txt index e9a08d0..004ab68 100644 --- a/components/mruby_component/CMakeLists.txt +++ b/components/mruby_component/CMakeLists.txt @@ -4,7 +4,7 @@ set(MRUBY_CONFIG ${COMPONENT_DIR}/esp32_build_config.rb) idf_component_register( INCLUDE_DIRS mruby/include - REQUIRES esp_wifi esp_hw_support esp_rom mqtt driver esp_timer + REQUIRES esp_hw_support esp_rom esp_timer esp_wifi driver mqtt ) add_custom_command( @@ -17,7 +17,7 @@ add_custom_command( add_prebuilt_library( libmruby ${LIBMRUBY_FILE} - PRIV_REQUIRES esp_wifi esp_hw_support esp_rom mqtt driver + PRIV_REQUIRES esp_hw_support esp_rom esp_timer esp_wifi driver mqtt ) target_link_libraries(${COMPONENT_LIB} INTERFACE libmruby) diff --git a/components/mruby_component/esp32_build_config.rb b/components/mruby_component/esp32_build_config.rb index fd56fd7..64627eb 100644 --- a/components/mruby_component/esp32_build_config.rb +++ b/components/mruby_component/esp32_build_config.rb @@ -61,9 +61,14 @@ conf.gem :core => "mruby-print" conf.gem :core => "mruby-compiler" - conf.gem :github => "mruby-esp32/mruby-esp32-system" - conf.gem :github => "mruby-esp32/mruby-esp32-wifi" - conf.gem :github => "mruby-esp32/mruby-esp32-mqtt" - conf.gem :github => "mruby-esp32/mruby-io", :branch => 'esp32' - conf.gem :github => "mruby-esp32/mruby-esp32-gpio" + conf.gem :github => "mruby-esp32/mruby-io", :branch => '0.5' + conf.gem :github => "mruby-esp32/mruby-fileio", :branch => '0.5' + conf.gem :github => "mruby-esp32/mruby-socket", :branch => '0.5' + + conf.gem :github => "mruby-esp32/mruby-esp32-system", :branch => '0.5' + conf.gem :github => "mruby-esp32/mruby-esp32-wifi", :branch => '0.5' + conf.gem :github => "mruby-esp32/mruby-esp32-mqtt", :branch => '0.5' + + conf.gem :github => "mruby-esp32/mruby-esp32-gpio", :branch => '0.5' + conf.gem :github => "mruby-esp32/mruby-esp32-ledc", :branch => '0.5' end diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index e1476fe..c9df518 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -2,7 +2,7 @@ idf_component_register( SRCS mruby_main.c INCLUDE_DIRS . REQUIRES mruby_component - PRIV_REQUIRES nvs_flash spiffs + PRIV_REQUIRES nvs_flash esp_littlefs ) -spiffs_create_partition_image(storage ./spiffs FLASH_IN_PROJECT) +littlefs_create_partition_image(storage ./storage FLASH_IN_PROJECT) diff --git a/main/examples/filesystem.rb b/main/examples/filesystem.rb new file mode 100644 index 0000000..f16117f --- /dev/null +++ b/main/examples/filesystem.rb @@ -0,0 +1,42 @@ +# Get the number of previous boots from a file. +boot_count = nil +File.open('/storage/boot_count.txt', 'a+') { |f| boot_count = f.gets } + +# Increment it. +boot_count = 0 unless boot_count +boot_count = boot_count.to_i +boot_count += 1 + +# Write new count back to file. +File.open('/storage/boot_count.txt', 'w') { |f| f.puts(boot_count) } + +# Display it. +puts "Boot count: #{boot_count}" + +# PERSISTENCE DEMO +persistence_file = "/storage/test.txt" + +# If first boot, write then append to the test file. +if boot_count == 1 + string1 = "testing " + string2 = "1,2,3..." + + puts "Writing to #{persistence_file}: \"#{string1}#{string2}\"" + + File.open(persistence_file, 'w') { |f| f.write(string1) } + File.open(persistence_file, 'a') { |f| f.write(string2) } +end + +# Read the file back on every boot. +print "Read from #{persistence_file}: " +File.open(persistence_file, 'r') { |f| f.each_line { |l| puts l } } + +# OVERWRITE DEMO +overwrite_file = '/storage/overwrite.txt' + +File.open(overwrite_file, 'w') { |f| f.puts "12345678" } +File.open(overwrite_file, 'w') { |f| f.puts "1234" } + +puts "#{overwrite_file} should contain: 1234" +print "#{overwrite_file} contains: " +File.open(overwrite_file, 'r') { |f| f.each_line { |l| puts l } } diff --git a/main/examples/ledc_breathe.rb b/main/examples/ledc_breathe.rb new file mode 100644 index 0000000..b82ef57 --- /dev/null +++ b/main/examples/ledc_breathe.rb @@ -0,0 +1,26 @@ +group = ESP32::LEDC_LOW_SPEED_MODE +channel = ESP32::LEDC_CHANNEL_0 +timer = ESP32::LEDC_TIMER_0 +resolution = ESP32::LEDC_TIMER_8_BIT +frequency = 1000 +pin = ESP32::GPIO::GPIO_NUM_2 # Built in LED on original ESP32 devkit. + +ESP32::LEDC.timer_config(group, timer, resolution, frequency) +ESP32::LEDC.channel_config(pin, group, timer, channel) + +# Fade the LED up and down. +loop do + i = 0 + while (i < 256) do + ESP32::LEDC.set_duty(group, channel, i) + i += 1 + ESP32::System.delay(10) + end + + i=255 + while (i > -1) do + ESP32::LEDC.set_duty(group, channel, i) + i -= 1 + ESP32::System.delay(10) + end +end diff --git a/main/examples/ledc_buzzer.rb b/main/examples/ledc_buzzer.rb new file mode 100644 index 0000000..f709beb --- /dev/null +++ b/main/examples/ledc_buzzer.rb @@ -0,0 +1,39 @@ +group = ESP32::LEDC_LOW_SPEED_MODE +channel = ESP32::LEDC_CHANNEL_0 +timer = ESP32::LEDC_TIMER_0 +resolution = ESP32::LEDC_TIMER_8_BIT +pin = ESP32::GPIO::GPIO_NUM_4 + +# Configure the channel once. +ESP32::LEDC.channel_config(pin, group, timer, channel) + +# Note frequencies. +C4 = 262 +D4 = 294 +E4 = 330 + +# Melody to play. +notes = [ + [E4, 1], [D4, 1], [C4, 1], [D4, 1], [E4, 1], [E4, 1], [E4, 2], + [D4, 1], [D4, 1], [D4, 2], [E4, 1], [E4, 1], [E4, 2], + [E4, 1], [D4, 1], [C4, 1], [D4, 1], [E4, 1], [E4, 1], [E4, 1], [E4, 1], + [D4, 1], [D4, 1], [E4, 1], [D4, 1], [C4, 4], + ] + +# Calculate length of one beat in milliseconds. +bpm = 180 +beat_time = 60000.to_f / bpm + +# Play the melody. +notes.each do |note| + # Set timer frequency to 0th element of the note array. + ESP32::LEDC.timer_config(group, timer, resolution, note[0]) + # Duty cycle to 50% for square wave. + ESP32::LEDC.set_duty(group, channel, 128) + + # Wait for length of the note, 1st element. + ESP32::System.delay(note[1] * beat_time) + + # Duty cycle to 0 to stop note. + ESP32::LEDC.set_duty(group, channel, 0) +end diff --git a/main/examples/ledc_servo.rb b/main/examples/ledc_servo.rb new file mode 100644 index 0000000..fe339ba --- /dev/null +++ b/main/examples/ledc_servo.rb @@ -0,0 +1,37 @@ +group = ESP32::LEDC_LOW_SPEED_MODE +channel = ESP32::LEDC_CHANNEL_0 +timer = ESP32::LEDC_TIMER_0 +resolution = ESP32::LEDC_TIMER_14_BIT +pin = ESP32::GPIO::GPIO_NUM_4 +frequency = 50 + +# Configure the channel and timer. +ESP32::LEDC.channel_config(pin, group, timer, channel) +ESP32::LEDC.timer_config(group, timer, resolution, frequency) + +# Using 14-bit PWM @ 50Hz, 0-16383 maps from 0 to 20 milliseconds. +# Calculate how many microseconds each LSB of duty cycle represents. +US_PER_BIT = (1000000.0 / frequency) / (2 ** resolution) + +# Convert from microseconds to duty cycle. +def microseconds_to_duty(us) + (us / US_PER_BIT).round +end + +# This is for a 180 degree MG995 motor. Values will differ for other sweep angles, +# or continuous rotation servos / ESCs. Values may vary between individual motors. +# +# Make sure to connect your servo motor to a separate power source! +# +5.times do + # Send 500us pulses for 2 seconds. Should map to 0 degrees. + ESP32::LEDC.set_duty(group, channel, microseconds_to_duty(500)) + ESP32::System.delay(2000) + + # Send 2500us pulses for 2 seconds. Should map to 180 degrees. + ESP32::LEDC.set_duty(group, channel, microseconds_to_duty(2500)) + ESP32::System.delay(2000) +end + +# Turn it off. +ESP32::LEDC.set_duty(group, channel, 0) diff --git a/main/examples/simplest_mrb.rb b/main/examples/simplest.rb similarity index 100% rename from main/examples/simplest_mrb.rb rename to main/examples/simplest.rb diff --git a/main/examples/system_mrb.rb b/main/examples/system.rb similarity index 100% rename from main/examples/system_mrb.rb rename to main/examples/system.rb diff --git a/main/examples/wifi_example_mrb.rb b/main/examples/wifi_connect.rb similarity index 100% rename from main/examples/wifi_example_mrb.rb rename to main/examples/wifi_connect.rb diff --git a/main/examples/wifi_socket.rb b/main/examples/wifi_socket.rb new file mode 100644 index 0000000..6160676 --- /dev/null +++ b/main/examples/wifi_socket.rb @@ -0,0 +1,38 @@ +# Stack sizes may need to be increased. +# See: https://github.com/mruby-esp32/mruby-socket/blob/0.4/README.md +# +puts "Getting ready to start Wi-Fi" + +wifi = ESP32::WiFi.new + +wifi.on_connected do |ip| + puts "Wi-Fi Connected: #{ip} (#{Socket.gethostname})" + soc = TCPSocket.open("www.kame.net", 80) + msg = "HEAD / HTTP/1.1\r\nHost: www.kame.net\r\nConnection: close\r\n\r\n" + msg.split("\r\n").each do |e| + puts ">>> #{e}" + end + soc.send(msg, 0) + puts "--------------------------------------------------------------------------------" + loop do + buf = soc.recv(128, 0) + break if buf.length == 0 + print buf + end + puts "" + puts "--------------------------------------------------------------------------------" +end + +wifi.on_disconnected do + puts "Wi-Fi Disconnected" +end + +puts "Connecting to Wi-Fi" +wifi.connect('SSID', 'PASSWORD') + +# +# Loop forever otherwise the script ends +# +while true do + ESP32::System.delay(1000) +end diff --git a/main/mruby_main.c b/main/mruby_main.c index e129faa..ee97ba3 100644 --- a/main/mruby_main.c +++ b/main/mruby_main.c @@ -4,8 +4,8 @@ #include "freertos/task.h" #include "esp_system.h" #include "esp_log.h" -#include "esp_spiffs.h" #include "nvs_flash.h" +#include "esp_littlefs.h" #include "mruby.h" #include "mruby/irep.h" @@ -26,10 +26,10 @@ void mruby_task(void *pvParameter) ESP_LOGI(TAG, "%s", "Loading..."); mrb_load_func load = mrb_load_detect_file_cxt; - FILE *fp = fopen("/spiffs/main.rb", "r"); + FILE *fp = fopen("/storage/main.rb", "r"); if (fp == NULL) { load = mrb_load_irep_file_cxt; - fp = fopen("/spiffs/main.mrb", "r"); + fp = fopen("/storage/main.mrb", "r"); if (fp == NULL) { ESP_LOGI(TAG, "File is none."); goto exit; @@ -60,13 +60,12 @@ void app_main() { nvs_flash_init(); - esp_vfs_spiffs_conf_t conf = { - .base_path = "/spiffs", - .partition_label = NULL, - .max_files = 10, - .format_if_mount_failed = false + esp_vfs_littlefs_conf_t conf = { + .base_path = "/storage", + .partition_label = "storage", + .format_if_mount_failed = false, }; - ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); + ESP_ERROR_CHECK(esp_vfs_littlefs_register(&conf)); xTaskCreate(&mruby_task, "mruby_task", 16384, NULL, 5, NULL); } diff --git a/main/spiffs/main.rb b/main/storage/main.rb similarity index 100% rename from main/spiffs/main.rb rename to main/storage/main.rb diff --git a/partitions.csv b/partitions.csv index a31cca5..cdac626 100644 --- a/partitions.csv +++ b/partitions.csv @@ -2,5 +2,5 @@ # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1500K, -storage, data, spiffs, 0x190000,200K, \ No newline at end of file +factory, app, factory, 0x10000, 1728K, +storage, data, spiffs, , 256K, \ No newline at end of file
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: