Programming

There are two main programming methods supported and tested with the Smart Plant V1:
  • ESPHome

  • Arduino

In both scenarios, you will first need to enter the board into flashing mode. For that, press and hold the Flash pushbutton while you reset the board (pressing once the Reset pushbutton).

Important

For flashing new firmwares, if the OTA support is not available, you will need an external UST-to-TTL module (like this) connected to the Serial port (3.3, GND, Tx, Rx).

Caution

When flashing the board, make sure its only powered from one power source: through the Serial port (by removing the battery connector) or through the battery (but then do not connect the 3.3V pin on the Serial port).

ESPHome

ESPHome is a well known platform for programming ESP-based devices with a very little effort. It is configured via YAML files and supports a wide range of functionalities and sensors.

Important

For using ESPHome, and all its funcionalities, you need to have a Home Assistant instance running in the same network as your Smart Plant V1.

_images/captive_portal-ui.png

The Smart Plant V1 already comes with an embeded version of ESPHome, that would only require an OTA update to get it ready to work in your network:

  1. Power the board, and let it run for 1-2 minutes. When the board cannot connect to a WiFi network, it will create a fallback hotspot.

  2. Use a smartphone or tablet and go to the WiFi settings, connect to the recently created Smart-Plant hotspot with the password smartplant.

  3. Access to the captive portal and open the browser if doesn’t pop up automatically.

  4. Enter your network setttings and press Save.

Now, your ESPHome device is ready to be found by Home Assistant in your network. Add it from the ESPHome section to add and edit a customized configuration file.

As an example of such configuration setup (and the one flashed on the factory settings of the Smart Plant V1) with all the dependencies:

esphome
├── fonts
│ ├── Audiowide.ttf
│ └── materialdesignicons-webfont_5.9.55.ttf
├── libraries
│ └── icon-map.h
├── Lemon_tree_label_page_1.png
└── smart-plant.yaml


In the folder structure above:

  • Audiowide.ttf is just a fonts style, you can download any of your choice and paste it there

  • materialdesignicons-webfont_5.9.55.ttf is a file containing a set of MDI that you can download from here

  • icon-map.h is a mapping file that is used to associate a variable name with the icon ID from the previous file. It contains the following code:

 1 #include <map>
 2 std::map<int, std::string> battery_icon_map
 3 {
 4     {0, "\U000F10CD"},
 5     {1, "\U000F007A"},
 6     {2, "\U000F007B"},
 7     {3, "\U000F007C"},
 8     {4, "\U000F007D"},
 9     {5, "\U000F007E"},
10     {6, "\U000F007F"},
11     {7, "\U000F0080"},
12     {8, "\U000F0081"},
13     {9, "\U000F0082"},
14     {10, "\U000F0079"},
15 };
  • Lemon_tree_label_page_1.png is the background image that will be displayed on the e-paper. It has a resolution of 296x128 pixels.

_images/Lemon_tree_label_page_1.png
  • smart-plant.yaml is the YAML configuration file:

  1esphome:
  2  name: smart-plant
  3  # Initialize the IIC bus immediatelly after the powering the sensors
  4  on_boot:
  5    priority: 600
  6    then:
  7     - lambda: |-
  8          Wire.begin();
  9          delay(100);
 10
 11external_components:
 12  - source:
 13      type: git
 14      url: https://github.com/velaar/esphome
 15      ref: dev
 16    components: [ waveshare_epaper, display]
 17
 18esp32:
 19  board: esp32dev
 20  framework:
 21    type: arduino
 22
 23# Enable logging
 24logger:
 25
 26# Enable Home Assistant API
 27api:
 28
 29ota:
 30  password: ***********
 31
 32wifi:
 33  ssid: !secret wifi_ssid
 34  password: !secret wifi_password
 35
 36  # Enable fallback hotspot (captive portal) in case wifi connection fails
 37  ap:
 38    ssid: "Smart-Plant"
 39    password: "smartplant"
 40
 41captive_portal:
 42
 43
 44i2c:
 45  scl: GPIO22
 46  sda: GPIO21
 47  scan: true
 48  id: bus_a
 49
 50spi:
 51  clk_pin:  GPIO13
 52  mosi_pin: GPIO14
 53    
 54image:
 55  - file: "Lemon_tree_label_page_1.png"
 56    id: page_1_background
 57
 58font:
 59  - file: "fonts/Audiowide.ttf"
 60    id: font_title
 61    size: 20
 62  - file: "fonts/Audiowide.ttf"
 63    id: font_subtitle
 64    size: 15
 65  - file: "fonts/Audiowide.ttf"
 66    id: font_parameters
 67    size: 15
 68
 69time:
 70  - platform: homeassistant
 71    id: esptime
 72
 73switch:
 74  - platform: gpio
 75    pin: GPIO16
 76    id: exc
 77    name: "Excitation switch"
 78    icon: "mdi:power"
 79    restore_mode: ALWAYS_ON  
 80    
 81    
 82sensor:
 83  # Temperature and humidity sensor
 84  - platform: aht10
 85    temperature:
 86      name: "Temperature"
 87      id: temp
 88      icon: "mdi:thermometer"
 89    humidity:
 90      name: "Air Humidity"
 91      id: hum
 92      icon: "mdi:water-percent"
 93    update_interval: 1s
 94    
 95  # Light sensor
 96  - platform: adc
 97    pin: GPIO33
 98    id: illum
 99    name: "Light"
100    icon: "mdi:white-balance-sunny"
101    attenuation: 11db
102    unit_of_measurement: lux
103    update_interval: 1s
104    filters:
105    - lambda: |-
106        return (x / 10000.0) * 2000000.0;
107        
108  # Capacitive soil moisture sensor
109  - platform: adc
110    pin: GPIO32
111    name: "Soil Moisture"
112    id : soil
113    icon: "mdi:cup-water"
114    update_interval: 1s
115    unit_of_measurement: "%"
116    attenuation: 11db
117    filters:
118    - median:
119        window_size: 7
120        send_every: 3
121
122    - calibrate_linear:
123        - 1.25 -> 100.00
124        - 2.8 -> 0.00
125    - lambda: if (x < 1) return 0; else if (x > 100) return 100; return (x);
126    accuracy_decimals: 0
127    on_value:
128      then:
129        - component.update: my_display      
130
131display:
132  - platform: waveshare_epaper
133    dc_pin: GPIO27
134    cs_pin: GPIO15
135    busy_pin: GPIO25
136    reset_pin: GPIO26
137    rotation: 270
138    model: 2.90inv2
139    update_interval: never
140    id: my_display
141    pages:
142      - id: page1
143        lambda: |-
144          #define H_LEFT_MARGIN 4
145          #define H_RIGHT_MARGIN 280
146          #define H_CENTER 128 
147          #define V_WEATHER 0
148          #define V_CLOCK 1
149          #define V_WIFI 30
150          #define V_VOLTAGE 60
151          #define V_BATTERY  90
152          
153          it.image(0, 0, id(page_1_background));
154          
155          // Battery
156          float battery_perc = id(battery).state;
157          int battery_range = battery_perc / 10 ;
158          battery_range = (battery_range > 10) ? 10 : battery_range;
159          battery_range = (battery_range < 0)  ?  0 : battery_range;
160          
161          it.printf(278, 1, id(font_icon_battery), TextAlign::TOP_LEFT, battery_icon_map[battery_range].c_str()
162          );
163          it.printf(278, 1, id(font_subtitle), TextAlign::TOP_RIGHT, 
164          "%3.0f%%", battery_perc);
165 
166          
167          // Clock 
168          it.strftime(278, 18, id(font_subtitle), TextAlign::TOP_RIGHT, 
169          "%d/%m/%y", id(esptime).now());     
170          
171          // Parameters
172          // Drawing the marker over the gauge
173          float pi = 3.141592653589793;
174          float alpha = 4.71238898038469; // Defined as the gauge angle in radians (270deg)
175          float beta = 2*pi - alpha;
176          int radius = 22;              // Radius of the gauge in pixels
177          int thick = 7;                // Size of the marker 
178          
179          // *** Moisture ***
180          int min_range = 0; 
181          int max_range = 100;
182          int xc = 80;
183          int yc = 50;
184          
185          float measured = id(soil).state;
186          
187          if (measured < min_range) {
188            measured = min_range;
189          } 
190          if (measured > max_range) {
191            measured = max_range;
192          } 
193          
194          float val = (measured - min_range) / abs(max_range - min_range) * alpha;
195          
196          int x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
197          int y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
198          int x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
199          int y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
200          int x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
201          int y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
202          it.line(x0, y0, x1, y1);
203          it.line(x1, y1, x2, y2);
204          it.line(x2, y2, x0, y0);
205          
206          it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER, 
207          "%.0f%%", id(soil).state);  
208          
209          // *** Light ***
210          min_range = 0; 
211          max_range = 10000;
212          xc = 134;
213          yc = 70;
214          
215          measured = id(illum).state;
216          
217          if (measured < min_range) {
218            measured = min_range;
219          } 
220          if (measured > max_range) {
221            measured = max_range;
222          } 
223          
224          val = (measured - min_range) / abs(max_range - min_range) * alpha;        
225          x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
226          y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
227          x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
228          y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
229          x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
230          y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
231          it.line(x0, y0, x1, y1);
232          it.line(x1, y1, x2, y2);
233          it.line(x2, y2, x0, y0);
234          
235          it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER, 
236          "%.0flx", id(illum).state);  
237          
238          
239          // *** Temperature ***
240          min_range = -10; 
241          max_range = 50;
242          xc = 188;
243          yc = 50;
244          
245          measured = id(temp).state;
246          
247          if (measured < min_range) {
248            measured = min_range;
249          } 
250          if (measured > max_range) {
251            measured = max_range;
252          } 
253          
254          val = (measured - min_range) / abs(max_range - min_range) * alpha;        
255          x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
256          y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
257          x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
258          y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
259          x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
260          y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
261          it.line(x0, y0, x1, y1);
262          it.line(x1, y1, x2, y2);
263          it.line(x2, y2, x0, y0);
264          
265          it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER, 
266          "%.0f°C", id(temp).state);     
267        
268
269          // *** Humidity ***
270          min_range = 20; 
271          max_range = 80;
272          xc = 242;
273          yc = 70;
274          
275          measured = id(hum).state;
276          
277          if (measured < min_range) {
278            measured = min_range;
279          } 
280          if (measured > max_range) {
281            measured = max_range;
282          } 
283          
284          val = (measured - min_range) / abs(max_range - min_range) * alpha;        
285          x0 = static_cast<int>(xc + radius + radius * cos(pi / 2 + beta / 2 + val));
286          y0 = static_cast<int>(yc + radius + radius * sin(pi / 2 + beta / 2 + val));
287          x1 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val + 0.1));
288          y1 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val + 0.1));
289          x2 = static_cast<int>(xc + radius + (radius+thick) * cos(pi / 2 + beta / 2 + val - 0.1));
290          y2 = static_cast<int>(yc + radius + (radius+thick) * sin(pi / 2 + beta / 2 + val - 0.1));
291          it.line(x0, y0, x1, y1);
292          it.line(x1, y1, x2, y2);
293          it.line(x2, y2, x0, y0);
294          
295          it.printf(xc + radius, yc + 1.7*radius, id(font_parameters), TextAlign::TOP_CENTER, 
296          "%.0f%%", id(hum).state);     
297          
298
299deep_sleep:
300  run_duration: 10s
301  sleep_duration: 3600s

Arduino

If you are still interested in programming directly with the Arduino IDE, the procedure is no different than with any other ESP32 devices:

  1. Open the Arduino IDE and go to File -> Preferences option.

  2. Add to the Additional Boards Manager URSLs the url:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  1. Close the preferences and open in the menu Tools -> Board -> Boards Manager.

  2. Search for esp32 and install it. This might take some time.

  3. Now you can select the board ESP32 Dev Module as the target board. Leave the rest of parameters by default.

  4. Select the correct port and remember to enter the board into flashing mode before uploading the sketch.