├── .gitignore ├── PROTOCOL.md ├── README.md ├── data ├── ajax-loader.gif ├── chart.min.js.gz ├── chartjs-annotation.min.js.gz ├── docstrings.js ├── gauge.min.js.gz ├── gauges.html ├── gauges.js ├── icon-check-circle.png ├── icon-trash.png ├── icon-x-square.png ├── index.html ├── index.js ├── inverter.js ├── jquery.core.min.js.gz ├── jquery.knob.min.js.gz ├── log.html ├── log.js ├── logo.png ├── modal.js ├── plot.js ├── refresh.png ├── remote.html ├── rtc.html ├── sdcard.html ├── style.css ├── syncofs.html ├── ui.js ├── wifi-updated.html ├── wifi.html └── wifi.js ├── doc ├── ARDUINO_IDE_setup.md ├── ARDUINO_IDE_usage.md ├── PLATFORMIO_setup.md └── PLATFORMIO_usage.md ├── esp32-web-interface.cbp ├── esp32-web-interface.ino ├── platformio-local-override.ini.example ├── platformio.ini └── upload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .pio/ 3 | platformio-local-override.ini 4 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # Openinverter Web Interface Protocol 2 | 3 | This document describes the protocol used on the serial interface between this ESP8266 module, and 4 | an inverter or VCU. 5 | 6 | ## General 7 | 8 | Commands are sent by the ESP8266. Each command consists of a single line consisting of a command word 9 | followed optionally by parameters and terminates with a newline character. Commands must be echoed back 10 | to the ESP8266. 11 | 12 | Following the echo, the esp8266 will receive an unlimited quantity of response data, terminated by a 13 | 100ms timeout. 14 | 15 | Except where otherwise noted, the responses are free text and should generally contain a single human 16 | readable line indicating success or faulure of the operation. 17 | 18 | ## Parameters and other data 19 | 20 | Openinverter makes available two types of data - parameters and non-parameters. 21 | 22 | Parameters are user configurable values, generally used for configuration. They can be stored in 23 | nonvolatile memory and should not change except in response to a user request. 24 | 25 | Other values are made available that are not configurable, but are instead indicative of the immediate 26 | state of the inverter. These are useful for monitoring and debugging. 27 | 28 | ## Commands 29 | 30 | | Command | Description| 31 | |---------|------------| 32 | |`save`|save current parameters to nonvolatile memory| 33 | |`load`|load parameters from nonvolatile memory| 34 | |`fastuart`|increases the baud rate to 921600Response must begin "OK" in success case| 35 | |`set [parameter] [value]`|set the decimal value of a named parameter| 36 | |`can [direction] [name] [canid] [offset] [length] [gain]`|map values to CAN messages| 37 | |`can clear`|clear all can mappings| 38 | |`start [opmode]`|start the inverter in a specified modemode 2 is manual run| 39 | |`stop`|stop the inverter| 40 | |`get [parameter]`|get the value of a parameter| 41 | |`stream [repetitions] [val1,val2,val3]`| repeatedly read and return one or more values| 42 | |`json [hidden]`| return an JSON encoded mapping of all parameters and values - see JSON format below| 43 | |`errors`|print information about all currently active error states, or indicate that everything is okay| 44 | |`reset`|reboot the device| 45 | |`defaults`|restore all parameters to default values| 46 | 47 | Note: This is not an exhaustive list of commands supported by openinverter devices, but does include 48 | all commands currently used by the openinverter web intrface. 49 | 50 | ## JSON Mapping 51 | 52 | The json command requests a dump of the full schema and values of both the configurable parameters 53 | and other available data. The optional "hidden" flag requests data that would not normally be 54 | displayed to the user. 55 | 56 | The following example shows a non-parameter value, a parameter value, and a value that has been 57 | mapped to CAN. 58 | 59 | ```json 60 | { 61 | "udc": {"unit":"V", "value": 400.0, "isparam": false}, 62 | "fweak": {"unit":"Hz", "value": 67.0, "isparam": true, "minimum": 0.0, "maximum": 400.0, "default": 67.0, "category": "Motor (sine)", "i": 8}, 63 | "speed": {"unit":"rpm", "value": 1000.0, "isparam": false, "canid": 123, "canoffset":0, "canlength":32, "cangain":5, "isrx": false} 64 | } 65 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | esp32-web-interface 2 | ===================== 3 | Web interface for Huebner inverter 4 | 5 | # Table of Contents 6 |
7 | Click to open TOC 8 | 9 | 10 | - [About](#about) 11 | - [Usage](#usage) 12 | - [Wifi network](#wifi-network) 13 | - [Reaching the board](#reaching-the-board) 14 | - [Hardware](#hardware) 15 | - [Firmware](#firmware) 16 | - [Flashing / Upgrading](#flashing--upgrading) 17 | - [Wirelessly](#wirelessly) 18 | - [Wired](#wired) 19 | - [Documentations](#documentations) 20 | - [Development](#development) 21 | - [Arduino](#arduino) 22 | - [PlatformIO](#platformio) 23 | 24 | 25 |
26 | 27 | # About 28 | This repository hosts the source code for the Web Interface for the Huebner inverter, and derivated projects: 29 | * [OpenInverter Sine (and FOC) firmware](https://github.com/jsphuebner/stm32-sine) 30 | * [Vehicle Control Unit for Electric Vehicle Conversion Projects](https://github.com/damienmaguire/Stm32-vcu) 31 | * [OpenInverter buck or boost mode charger firmware](https://github.com/jsphuebner/stm32-charger) 32 | * [OpenInverter non-grid connected inverter](https://github.com/jsphuebner/stm32-island) 33 | * [BMS project firmware](https://github.com/jsphuebner/bms-software) 34 | * ... 35 | 36 | It is written with the Arduino development environment and libraries. 37 | 38 | # Usage 39 | To use the web interface 2 things are needed : 40 | * You need to have a computer on the same WiFi network as the board, 41 | * You need to 'browse' the web interface page. 42 | 43 | ## Wifi network 44 | There are 2 possibilities: 45 | * Either you connect to an Access Point generated by the board. The default name for this access point is 'ESP-xxxxx' but can be customized. In that case, the board will have a fixed IP address of `192.168.4.1` (and will be reachable on http://192.168.4.1/) 46 | * Or you can configure the board to join your own WiFi network ; and in that case you may need to tweak your network configuration to provide a fixed address to the board (not necessary). 47 | 48 | ## Reaching the board 49 | The board announces itself to the world using mDNS protocol (aka Bonjour, or Rendezvous, or Zeroconf), so you may be able to reach the board using a local name of `inverter.local`. 50 | So first try to reach it on http://inverter.local/ 51 | 52 | # Hardware 53 | The web interface has been initially designed to run on ESP32-WROOM-32E boards. 54 | 55 | A SD card running in SDIO mode can be connected (CLK to pin14, CMD to pin15, D0 to Pin2, D1 to Pin4, D2 to Pin12, D3 to Pin13). 56 | 57 | A RTC can be connected. As standard a PCF8523 is suported but any clock supported by RTClib can be used with a sketch change. (SCLK to Pin22, SDA to Pin21). 58 | 59 | The connection to the inverter are on Pin16 (Rx line connect to inverter Tx line) and Pin17 (Tx line connect to inverter Rx line). 60 | 61 | # Firmware 62 | Tompile it follow the [instructions below](#development). 63 | 64 | # Flashing / Upgrading 65 | ## Wirelessly 66 | TBA 67 | 68 | ## Wired 69 | If your board is new and unprogrammed, or if you want to fully re-program it, you'll need to have a wired connection between your computer and the board. 70 | You'll either need a ESP32 board with an on board USB to serial converter or a 3.3v capable USB / Serial adapter 71 | * the following connections: 72 | 73 | Pin# | ESP32 Board Function | USB / Serial adapter 74 | ----- | ---------------------- | -------------------- 75 | 1 | +3.3v input | (Some adapters provide a +3.3v output, you can use it) 76 | 2 | GND | GND 77 | 3 | RXD input | TXD output 78 | 4 | TXD output | RXD input 79 | 80 | Then you would use any of the the [development tool below](#development) ; or the `esptool.py` tool to upload either a binary firmware file, or a binary filesystem file. 81 | 82 | Various openinverter boards (SDU, LDU, Leaf) use a different wiring scheme for initial programming. It is important to flash the ESP32 chip BEFORE flashing the STM32 chip because otherwise you will get a bus collision on the UART pins. If you've already flashed the STM32 either erase the flash or hold it in reset somehow while flashing the ESP32. 83 | 84 | Pin# | ESP32 Board Function | USB / Serial adapter 85 | ----- | ---------------------- | -------------------- 86 | 1 | TXD output | RXD input 87 | 2 | RXD input | TXD output 88 | 3 | +5v input | (Some adapters provide a +5v output, you can use it) 89 | 4 | GND | GND 90 | 5 | GND | GND 91 | 6 | GPIO0 | Connect this to pin 5 (GND) to put the ESP32 into programming mode. Then power up 92 | 93 | Flash subsequent updates via OTA. 94 | 95 | # Documentations 96 | * [Openinverter Web Interface Protocol](PROTOCOL.md) 97 | 98 | # Development 99 | You can choose between the following tools: 100 | 101 | ## Arduino 102 | [Arduino IDE](https://www.arduino.cc/en/software) is an easy-to-use desktop IDE, which provides a quick and integrated way to develop and update your board. 103 | * [Initial setup](doc/ARDUINO_IDE_setup.md) 104 | * [Day to day usage](doc/ARDUINO_IDE_usage.md) 105 | 106 | ## PlatformIO 107 | [PlatformIO](https://platformio.org/) is a set of tools, among which [PlatformIO Core (CLI)](https://docs.platformio.org/en/latest/core/index.html) is a command line interface that can be used to build many kind of projects. In particular Arduino-based projects like this one. 108 | (Note: even if PlatformIO provides an IDE, these instructions only target the CLI.) 109 | * [Initial setup](doc/PLATFORMIO_setup.md) 110 | * [Day to day usage](doc/PLATFORMIO_usage.md) 111 | -------------------------------------------------------------------------------- /data/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/ajax-loader.gif -------------------------------------------------------------------------------- /data/chart.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/chart.min.js.gz -------------------------------------------------------------------------------- /data/chartjs-annotation.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/chartjs-annotation.min.js.gz -------------------------------------------------------------------------------- /data/docstrings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | var docstrings = { 22 | data: { 23 | /* spot values */ 24 | version: "Firmware version.", 25 | hwver: "Hardware version", 26 | opmode: "Operating mode. 0=Off, 1=Run, 2=Manual_run, 3=Boost, 4=Buck, 5=Sine, 6=2 Phase sine", 27 | lasterr: "Last error message", 28 | status: "", 29 | udc: "Voltage on the DC side of the inverter. a.k.a, battery voltage.", 30 | idc: "Current passing through the DC side of the inverter (calculated).", 31 | il1: "Current passing through the first current sensor on the AC side.", 32 | il2: "Current passing through the second current sensor on the AC side.", 33 | id: "", 34 | iq: "", 35 | ud: "", 36 | uq: "", 37 | heatcur: "", 38 | fstat: "Stator frequency", 39 | speed: "The speed (rpm) of the motor.", 40 | cruisespeed: "", 41 | turns: "Number of turns the motor has completed since startup.", 42 | amp: "Sine amplitude, 37813=max", 43 | angle: "Motor rotor angle, 0-360°. When using the SINE software, the slip is added to the rotor position.", 44 | pot: "Pot value, 4095=max", 45 | pot2: "Regen Pot value, 4095=max", 46 | potnom: "Scaled pot value, 0 accel", 47 | dir: "Rotation direction. -1=REV, 0=Neutral, 1=FWD", 48 | tmphs: "Inverter heatsink temperature", 49 | tmpm: "Motor temperature", 50 | uaux: "Auxiliary voltage (i.e. 12V system). Measured on pin 11 (mprot)", 51 | pwmio: "Raw state of PWM outputs at power up", 52 | canio: "Digital IO bits received via CAN", 53 | din_cruise: "Cruise Control. This pin activates the cruise control with the current speed. Pressing again updates the speed set point.", 54 | din_start: "State of digital input \"start\". This pin starts inverter operation", 55 | din_brake: "State of digital input \"brake\". This pin sets maximum regen torque (brknompedal). Cruise control is disabled.", 56 | din_mprot: "State of digital input \"motor protection switch\". Shuts down the inverter when = 0", 57 | din_forward: "Direction forward.", 58 | din_reverse: "Direction backward.", 59 | din_emcystop: "State of digital input \"emergency stop\". Shuts down the inverter when = 0", 60 | din_ocur: "Over current detected.", 61 | din_desat: "", 62 | din_bms: "BMS over voltage/under voltage.", 63 | cpuload: "CPU load for everything except communication", 64 | /* parameters */ 65 | curkp: "Current controller proportional gain", 66 | curki: "Current controller integral gain", 67 | curkifrqgain: "Current controllers integral gain frequency coefficient", 68 | fwkp: "Cross comparison field weakening controller gain", 69 | dmargin: "Margin for residual torque producing current (so field weakening current doesn't use up the entire amplitude)", 70 | syncadv: "Shifts \"syncofs\" downwards/upwards with frequency", 71 | boost: "0 Hz Boost in digit. 1000 digit ~ 2.5%", 72 | fweak: "Frequency where V/Hz reaches its peak", 73 | fconst: "Frequency where slip frequency is derated to form a constant power region. Only has an effect when < fweak", 74 | udcnom: "Nominal voltage for fweak and boost. fweak and boost are scaled to the actual dc voltage. 0=don't scale", 75 | fslipmin: "Slip frequency at minimum throttle", 76 | fslipmax: "Slip frequency at maximum throttle", 77 | fslipconstmax: "Slip frequency at maximum throttle and fconst", 78 | fmin: "Below this frequency no voltage is generated", 79 | polepairs: "Pole pairs of motor (e.g. 4-pole motor: 2 pole pairs)", 80 | respolepairs: "Pole pairs of resolver (normally same as polepairs of motor, but sometimes 1)", 81 | encflt: "Filter constant between pulse encoder and speed calculation. Makes up for slightly uneven pulse distribution", 82 | encmode: "0=single channel encoder, 1=quadrature encoder, 2=quadrature /w index pulse, 3=SPI (deprecated), 4=Resolver, 5=sin/cos chip", 83 | fmax: "At this frequency rev limiting kicks in", 84 | numimp: "Pulse encoder pulses per turn", 85 | dirchrpm: "Motor speed at which direction change is allowed", 86 | dirmode: "0=button (momentary pulse selects forward/reverse), 1=switch (forward or reverse signal must be constantly high)", 87 | syncofs: "Phase shift of sine wave after receiving index pulse", 88 | snsm: "Motor temperature sensor. 12=KTY83, 13=KTY84, 14=Leaf, 15=KTY81", 89 | pwmfrq: "PWM frequency. 0=17.6kHz, 1=8.8kHz, 2=4.4kHz, 3=2.2kHz. Needs PWM restart", 90 | pwmpol: "PWM polarity. 0=active high, 1=active low. DO NOT PLAY WITH THIS! Needs PWM restart", 91 | deadtime: "Deadtime between highside and lowside pulse. 28=800ns, 56=1.5µs. Not always linear, consult STM32 manual. Needs PWM restart", 92 | ocurlim: "Hardware over current limit. RMS-current times sqrt(2) + some slack. Set negative if il1gain and il2gain are negative.", 93 | minpulse: "Narrowest or widest pulse, all other mapped to full off or full on, respectively", 94 | il1gain: "Digits per A of current sensor L1", 95 | il2gain: "Digits per A of current sensor L2", 96 | udcgain: "Digits per V of DC link", 97 | udcofs: "DC link 0V offset", 98 | udclim: "High voltage at which the PWM is shut down", 99 | snshs: "Heatsink temperature sensor. 0=JCurve, 1=Semikron, 2=MBB600, 3=KTY81, 4=PT1000, 5=NTCK45+2k2, 6=Leaf", 100 | pinswap: "Swap pins (only \"FOC\" software). Multiple bits can be set. 1=Swap Current Inputs, 2=Swap Resolver sin/cos, 4=Swap PWM output 1/3\n001 = 1 Swap Currents only\n010 = 2 Swap Resolver only\n011 = 3 Swap Resolver and Currents\n100 = 4 Swap PWM only\n101 = 5 Swap PWM and Currents\n110 = 6 Swap PWM and Resolve\n111 = 7 Swap PWM and Resolver and Currents", 101 | bmslimhigh: "Positive throttle limit on BMS under voltage", 102 | bmslimlow: "Regen limit on BMS over voltage", 103 | udcmin: "Minimum battery voltage", 104 | udcmax: "Maximum battery voltage", 105 | iacmax: "Maximum peak AC current", 106 | idcmax: "Maximum DC input current", 107 | idcmin: "Maximum DC output current (regen)", 108 | throtmax: "Throttle limit", 109 | throtmin: "Throttle regen limit", 110 | ifltrise: "Controls how quickly slip and amplitude recover. The greater the value, the slower", 111 | ifltfall: "Controls how quickly slip and amplitude are reduced on over current. The greater the value, the slower", 112 | chargemode: "0=Off, 3=Boost, 4=Buck", 113 | chargecur: "Charge current setpoint. Boost mode: charger INPUT current. Buck mode: charger output current", 114 | chargekp: "Charge controller gain. Lower if you have oscillation, raise if current set point is not met", 115 | chargeflt: "Charge current filtering. Raise if you have oscillations", 116 | chargemax: "Charge mode duty cycle limit. Especially in boost mode this makes sure you don't overvolt you IGBTs if there is no battery connected.", 117 | potmin: "Value of \"pot\" when pot isn't pressed at all", 118 | potmax: "Value of \"pot\" when pot is pushed all the way in", 119 | pot2min: "Value of \"pot2\" when regen pot is in 0 position", 120 | pot2max: "Value of \"pot2\" when regen pot is in full on position", 121 | potmode: "0=Pot 1 is throttle and pot 2 is regen strength preset. 1=Pot 2 is proportional to pot 1 (redundancy) 2=Throttle controlled via CAN", 122 | throtramp: "Max positive throttle slew rate", 123 | throtramprpm: "No throttle ramping above this speed", 124 | ampmin: "Minimum relative sine amplitude (only \"sine\" software)", 125 | slipstart: "% positive throttle travel at which slip is increased (only \"sine\" software)", 126 | throtcur: "Motor current per % of throttle travel (only \"FOC\" software)", 127 | brknompedal: "Foot on brake pedal regen torque", 128 | brkpedalramp: "Ramp speed when entering regen. E.g. when you set brkmax to 20% and brknompedal to -60% and brkpedalramp to 1, it will take 400ms to arrive at brake force of -60%", 129 | brknom: "Range of throttle pedal travel allocated to regen", 130 | brkmax: "Foot-off throttle regen torque", 131 | brkrampstr: "Below this frequency the regen torque is reduced linearly with the frequency", 132 | brkout: "Activate brake light output at this amount of braking force", 133 | idlespeed: "Motor idle speed. Set to -100 to disable idle function. When idle speed controller is enabled, brake pedal must be pressed on start.", 134 | idlethrotlim: "Throttle limit of idle speed controller", 135 | idlemode: "Motor idle speed mode. 0=always run idle speed controller, 1=only run it when brake pedal is released, 2=like 1 but only when cruise switch is on", 136 | speedkp: "Speed controller gain (Cruise and idle speed). Decrease if speed oscillates. Increase for faster load regulation", 137 | speedflt: "Filter before cruise controller", 138 | cruisemode: "0=button (set when button pressed, reset with brake pedal), 1=switch (set when switched on, reset when switched off or brake pedal)", 139 | udcsw: "Voltage at which the DC contactor is allowed to close", 140 | udcswbuck: "Voltage at which the DC contactor is allowed to close in buck charge mode", 141 | tripmode: "What to do with relays at a shutdown event. 0=All off, 1=Keep DC switch closed, 2=close precharge relay", 142 | pwmfunc: "Quantity that controls the PWM output. 0=tmpm, 1=tmphs, 2=speed", 143 | pwmgain: "Gain of PWM output", 144 | pwmofs: "Offset of PWM output, 4096=full on", 145 | canspeed: "Baud rate of CAN interface 0=250k, 1=500k, 2=800k, 3=1M", 146 | canperiod: "0=send configured CAN messages every 100ms, 1=every 10ms", 147 | fslipspnt: "Slip setpoint in mode 2. Written by software in mode 1", 148 | ampnom: "Nominal amplitude in mode 2. Written by software in mode 1", 149 | }, 150 | 151 | get: function(item) 152 | { 153 | if ( item in docstrings.data ) 154 | { 155 | return docstrings.data[item]; 156 | } 157 | return ""; 158 | } 159 | 160 | }; 161 | -------------------------------------------------------------------------------- /data/gauge.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/gauge.min.js.gz -------------------------------------------------------------------------------- /data/gauges.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | Huebner Inverter Management Console 26 | 62 | 63 | 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /data/gauges.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | var gauges = {}; 22 | var items = new Array(); 23 | 24 | function onLoad() 25 | { 26 | createGauges(); 27 | acquire(); 28 | } 29 | 30 | /** 31 | * @brief Creates gauges for all canvases found on the page 32 | */ 33 | function createGauges() 34 | { 35 | var div = document.getElementById("gauges"); 36 | var paramPart = document.location.href.split("items="); 37 | items = paramPart[1].split(","); 38 | 39 | for (var i = 0; i < items.length; i++) 40 | { 41 | var name = items[i]; 42 | var canvas = document.createElement("CANVAS"); 43 | canvas.setAttribute("id", name); 44 | div.appendChild(canvas); 45 | 46 | var gauge = new RadialGauge( 47 | { 48 | renderTo: name, 49 | title: name, 50 | width: 300, 51 | height: 300, 52 | minValue: 0, 53 | maxValue: 1, 54 | majorTicks: [0, 1] 55 | }); 56 | 57 | gauge.draw(); 58 | gauges[name] = gauge; 59 | } 60 | } 61 | 62 | function calcTicks(min, max) 63 | { 64 | var N = 6; 65 | var ticks = [ min ]; 66 | var dist = (max - min) / N; 67 | var tick = min; 68 | 69 | for (var i = 0; i < N; i++) 70 | { 71 | tick += dist; 72 | ticks.push(Math.round(tick)); 73 | } 74 | return ticks; 75 | } 76 | 77 | function acquire() 78 | { 79 | if (!items.length) return; 80 | 81 | inverter.getValues(items, 1, 82 | function(values) 83 | { 84 | for (var name in values) 85 | { 86 | var val = values[name][0]; 87 | gauges[name].options.minValue = Math.min(gauges[name].options.minValue, Math.floor(val * 0.7)); 88 | gauges[name].options.maxValue = Math.max(gauges[name].options.maxValue, Math.ceil(val * 1.5)); 89 | gauges[name].options.majorTicks = calcTicks(gauges[name].options.minValue, gauges[name].options.maxValue); 90 | gauges[name].value = val; 91 | gauges[name].update(); 92 | } 93 | acquire(); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /data/icon-check-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/icon-check-circle.png -------------------------------------------------------------------------------- /data/icon-trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/icon-trash.png -------------------------------------------------------------------------------- /data/icon-x-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/icon-x-square.png -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | Huebner Inverter Management Console 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 53 | 54 | 55 | 60 | 61 |
62 |

Communication problem between ESP and STM

63 |
64 | 65 | 66 | 114 | 115 | 175 | 176 |
177 |
178 | 179 | 180 |
181 | 182 |
183 |

Actions

184 | 187 | 190 | 191 |
192 | 193 |
194 |

Dashboard

195 | 196 |
197 |
198 | 199 |
200 |
201 |

Inverter messages

202 |
203 |
204 |
205 | 206 |
207 |
208 |

Command

209 |
210 | »  211 |
212 |
213 |
214 |
215 |
216 | 217 | 218 |
219 | 220 |
221 | 222 |

Firmware

223 | 224 | 225 |
226 | 227 | 230 |
231 | 232 | 233 | 236 | 237 |

Web Interface

238 | 239 | 240 |
241 | 242 | 243 | 244 |
245 | 246 |
247 | 248 |
249 |

Update

250 |

On this page you can apply software updates to your OpenInverter system. There are several distinct pieces of software which go to make up an OpenInverter system and each has a separate update mechanism.

251 | 252 |

OpenInverter Board Bootloader

253 |

Use the stm32_loader.bin file to update the bootloader on your OpenInverter board. You can find download links in the forum here. 254 | 255 |

OpenInverter Board Firmware

256 |

To install a new firmware on your OpenInverter board click on the 'Install firmware from file' button on the right. Use the stm32_sine.bin file to install the sine firmware or use the stm32_foc.bin file to install the FOC firmware. You can find information about the latest available releases, including download links, on the OpenInverter forum here.

257 | 258 |

Web Interface Firmware

259 |

The ESP8266 firmware can be upgraded with platformio via OTA i.e. directly via wifi without programming cable, see forum.
260 | You can also program with a cable, see here for more details.

261 | 262 |

Web Interface Application

263 |

You can apply updates to this web interface by uploading individual files. More information about updating the web interface can be found in its github repository here.

264 |
265 | 266 |
267 | 268 | 269 |
270 | 271 |
272 |

Save & Load

273 | 274 | 275 | 278 | 279 | 280 | 283 | 284 | 285 | 288 | 289 | 290 |
291 | 292 | 293 | 294 |
295 | 296 |

Parameter Database

297 | 298 | 299 |
300 | 301 | 302 | 305 |
306 | 307 | 308 | 311 | 312 | 315 | 316 |

Misc

317 | 318 | 319 | 320 | 321 | 322 | 325 | 326 | 327 | 330 | 331 |

332 |

333 |

334 | 335 |
336 |

Parameters

337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 |
INameValueUnitMinMaxDefault
352 |
353 | 354 |
355 | 356 | 357 |
358 | 359 |
360 |
361 | 362 |
363 |

Spot Values

364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 |
NameValueUnit
376 |
377 | 378 |
379 | 380 | 381 |
382 | 383 |
384 |

Actions

385 | 386 | 387 | 390 | 391 | 392 | 395 | 396 | 397 | 400 | 401 | 402 | 405 | 406 |

Configure Plot

407 | Data points :
408 | Burst length : 409 |
410 | 413 |
414 |
415 | 416 |
417 |

Plot

418 | 419 |

420 |

Copyright 2018 Johannes Huebner dev@johanneshuebner.com

421 |

Charting by chart.js

422 |

Gauges by Mykhailo Stadnyk

423 |
424 |
425 | 426 | 427 | 428 |
429 | 430 |
431 |

Actions

432 | 433 | 434 | 437 | 438 | 439 | 442 | 443 | 444 | 447 | 448 |

Configure Logger

449 | 450 |
451 | 452 |
453 |
454 | 457 |
458 |
459 | 460 |
461 |

Data Logger

462 | 464 |
465 | 466 |
467 | 468 | 469 | 470 |
471 | 472 |
473 |

Actions

474 | 477 | 480 | 483 |
484 | 485 |
486 |

CAN Mapping

487 |

On this page you can configure the CAN mapping settings for your OpenInverter board. CAN mapping allows you to send and receive data via CAN bus. You can specify spot values that you would like to transmit on the CAN bus. Additionally you can specify spot values that you would like to set based on data received on the CAN bus.

488 |

A maximum of 8 items per CAN message can be mapped.

489 | 490 |

Existing CAN Mappings

491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 |
Spot ValueTransmit or ReceiveIDOffsetLengthGainDelete Mapping
502 |
503 |
504 | 505 | 506 | 507 | 508 |
509 | 510 | 511 |
512 |
513 |

Actions

514 | 515 |
516 | 517 | 520 |
521 |
522 |
523 |

Files

524 | 525 | 526 | 527 | 528 | 529 |
FilenameDelete
530 |
531 |
532 | 533 | 534 | 535 |
536 | 542 |
543 |

Support

544 |

You can get support from the community on the OpenInverter Forum.

545 |

Paid support is also available. See details here. 546 |

547 |
548 | 549 | 550 |
551 |
552 | 553 |
554 | 555 | 556 | 557 | 558 | -------------------------------------------------------------------------------- /data/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | var chart; 22 | var items = {}; 23 | var stop; 24 | var imgid = 0; 25 | var subscription; 26 | 27 | function sleep(ms) { 28 | return new Promise(resolve => setTimeout(resolve, ms)); 29 | } 30 | 31 | 32 | 33 | /** @brief uploads file to web server, Flash using Serial-Wire-Debug. Start address bootloader = 0x08000000, firmware = 0x08001000*/ 34 | function uploadSWDFile() 35 | { 36 | var xmlhttp = new XMLHttpRequest(); 37 | var form = document.getElementById('swdform'); 38 | 39 | if (form.getFormData) 40 | var fd = form.getFormData(); 41 | else 42 | var fd = new FormData(form); 43 | var file = document.getElementById('swdfile').files[0]; 44 | 45 | xmlhttp.onload = function() 46 | { 47 | var xhr = new XMLHttpRequest(); 48 | xhr.seenBytes = 0; 49 | xhr.seenTotalPages = 0; 50 | xhr.onreadystatechange = function() { 51 | if(xhr.readyState == 3) { 52 | var data = xhr.response.substr(xhr.seenBytes); 53 | 54 | if(data.indexOf("Error") != -1) { 55 | document.getElementById("swdbar").style.width = "100%"; 56 | document.getElementById("swdbar").innerHTML = "

" + data + "

"; 57 | }else{ 58 | var s = data.split('\n'); 59 | xhr.seenTotalPages += (s.length - 1) * 16; 60 | //console.log("pages: " + s.length + " Size: " + ((s.length -1) * 16)); 61 | 62 | var progress = Math.round(100 * xhr.seenTotalPages / file.size); 63 | document.getElementById("swdbar").style.width = progress + "%"; 64 | document.getElementById("swdbar").innerHTML = "

" + progress + "%

"; 65 | 66 | xhr.seenBytes = xhr.responseText.length; 67 | } 68 | } 69 | }; 70 | if (file.name.endsWith('loader.bin')) 71 | { 72 | xhr.open('GET', '/swd/mem/flash?bootloader&file=' + file.name, true); 73 | }else{ 74 | xhr.open('GET', '/swd/mem/flash?flash&file=' + file.name, true); 75 | } 76 | xhr.send(); 77 | } 78 | xmlhttp.open("POST", "/edit"); 79 | xmlhttp.send(fd); 80 | } 81 | 82 | -------------------------------------------------------------------------------- /data/inverter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | 22 | /** @brief this is a little cache to store the current params/spot values. This 23 | * is here so that different functions can do look-ups without making a full 24 | * HTTP call to the inverter each time. */ 25 | 26 | var paramsCache = { 27 | 28 | data: undefined, 29 | dataById: {}, 30 | failedFetchCount: 0, 31 | 32 | get: function(name) { 33 | if ( paramsCache.data !== undefined ) 34 | { 35 | if ( name in paramsCache.data ) { 36 | if ( paramsCache.data[name].enums ) { 37 | if (paramsCache.data[name].enums[paramsCache.data[name].value]) 38 | { 39 | return paramsCache.data[name].enums[paramsCache.data[name].value]; 40 | } 41 | else 42 | { 43 | var active = []; 44 | for (var key in paramsCache.data[name].enums) 45 | { 46 | if (paramsCache.data[name].value & key) 47 | active.push(paramsCache.data[name].enums[key]); 48 | } 49 | return active.join('|'); 50 | } 51 | } else { 52 | return paramsCache.data[name].value; 53 | } 54 | } 55 | } 56 | return null; 57 | }, 58 | 59 | getEntry: function(name) { 60 | return paramsCache.data[name]; 61 | }, 62 | 63 | getData: function() { return paramsCache.data; }, 64 | 65 | setData: function(data) { 66 | paramsCache.data = data; 67 | 68 | for (var key in data) { 69 | paramsCache.dataById[data[key].id] = data[key]; 70 | paramsCache.dataById[data[key].id]['name'] = key; 71 | } 72 | }, 73 | 74 | getJson: function() { return JSON.stringify(paramsCache.data); }, 75 | 76 | getById: function(id) { 77 | return paramsCache.dataById[id]; 78 | } 79 | } 80 | 81 | var inverter = { 82 | 83 | firmwareVersion: 0, 84 | 85 | /** @brief send a command to the inverter */ 86 | sendCmd: function(cmd, replyFunc, repeat) 87 | { 88 | var xmlhttp=new XMLHttpRequest(); 89 | var req = "/cmd?cmd=" + cmd; 90 | 91 | xmlhttp.onload = function() 92 | { 93 | if (replyFunc) replyFunc(this.responseText); 94 | } 95 | 96 | if (repeat) 97 | req += "&repeat=" + repeat; 98 | 99 | xmlhttp.open("GET", req, true); 100 | xmlhttp.send(); 101 | }, 102 | 103 | /** @brief get the params from the inverter */ 104 | getParamList: function(replyFunc, includeHidden) 105 | { 106 | var cmd = includeHidden ? "json hidden" : "json"; 107 | 108 | inverter.sendCmd(cmd, function(reply) { 109 | var params = {}; 110 | try 111 | { 112 | params = JSON.parse(reply); 113 | 114 | for (var name in params) 115 | { 116 | var param = params[name]; 117 | param.enums = inverter.parseEnum(param.unit); 118 | 119 | if (name == "version") 120 | inverter.firmwareVersion = parseFloat(param.value); 121 | } 122 | paramsCache.failedFetchCount = 0; 123 | } 124 | catch(ex) 125 | { 126 | paramsCache.failedFetchCount += 1; 127 | if ( paramsCache.failedFetchCount >= 2 ){ 128 | ui.showCommunicationErrorBar(); 129 | } 130 | } 131 | if ( paramsCache.failedFetchCount < 2 ) 132 | { 133 | ui.hideCommunicationErrorBar(); 134 | } 135 | paramsCache.setData(params); 136 | if (replyFunc) replyFunc(params); 137 | }); 138 | }, 139 | 140 | getValues: function(items, repeat, replyFunc) 141 | { 142 | var process = function(reply) 143 | { 144 | var expr = /(\-{0,1}[0-9]+\.[0-9]*)/mg; 145 | var signalIdx = 0; 146 | var values = {}; 147 | 148 | for (var res = expr.exec(reply); res; res = expr.exec(reply)) 149 | { 150 | var val = parseFloat(res[1]); 151 | 152 | if (!values[items[signalIdx]]) 153 | values[items[signalIdx]] = new Array() 154 | values[items[signalIdx]].push(val); 155 | signalIdx = (signalIdx + 1) % items.length; 156 | } 157 | replyFunc(values); 158 | }; 159 | 160 | if (inverter.firmwareVersion < 3.53 || items.length > 10) 161 | inverter.sendCmd("get " + items.join(','), process, repeat); 162 | else 163 | inverter.sendCmd("stream " + repeat + " " + items.join(','), process); 164 | }, 165 | 166 | 167 | /** @brief given the 'unit' string provided by the inverter api, parse out 168 | * the key value pairs and return them in an array. 169 | * @param unit, e.g. "0=None, 1=UdcLow, 2=UdcHigh, 4=UdcBelowUdcSw" 170 | * Example return : ['None', 'UdcLow', 'UdcHigh',,'udcBelowUdcSw']. Note, 171 | * the extra comma is intentional. The position in the array is determined 172 | * by the index on the left hand side of the equals in the 'unit' string. 173 | */ 174 | parseEnum: function(unit) 175 | { 176 | var expr = /(\-{0,1}[0-9]+)=([a-zA-Z0-9_\-\.]+)[,\s]{0,2}|([a-zA-Z0-9_\-\.]+)[,\s]{1,2}/g; 177 | var enums = new Array(); 178 | var res = expr.exec(unit); 179 | 180 | if (res) 181 | { 182 | do 183 | { 184 | enums[res[1]] = res[2]; 185 | } while (res = expr.exec(unit)) 186 | //console.log('enums : ' + enums); 187 | return enums; 188 | } 189 | return false; 190 | }, 191 | 192 | /** @brief helper function, from a list of parameters send parameter with given index to inverter 193 | * @param params map of parameters (name -> value) 194 | * @param index numerical index which parameter to set */ 195 | setParam: function(params, index) 196 | { 197 | var keys = Object.keys(params); 198 | 199 | if (index < keys.length) 200 | { 201 | var key = keys[index]; 202 | modal.appendToModal('large', "Setting " + key + " to " + params[key] + "
"); 203 | inverter.sendCmd("set " + key + " " + params[key], function(reply) { 204 | modal.appendToModal('large', reply + "
"); 205 | // auto-scroll text in modal as it is added 206 | modal.largeModalScrollToBottom(); 207 | inverter.setParam(params, index + 1); 208 | }); 209 | } 210 | }, 211 | 212 | /** @brief Add/Delete a CAN mapping 213 | * @param direction, tx, rx, or del 214 | * @param name, spot value name 215 | * @param id, canid of message 216 | * @param pos, offset within frame 217 | * @param bits, length of field 218 | * @param gain, multiplier 219 | */ 220 | canMapping: function(direction, name, id, pos, bits, gain) 221 | { 222 | var cmd = "can " + direction + " " + name + " " + id + " " + pos + " " + bits + " " + gain; 223 | inverter.sendCmd(cmd); 224 | }, 225 | 226 | /** @brief get a list of files in the spiffs filesystem on the esp8266 */ 227 | getFiles: function(replyFunc) 228 | { 229 | var filesRequest = new XMLHttpRequest(); 230 | filesRequest.onload = function() 231 | { 232 | var filesJson = JSON.parse(this.responseText); 233 | replyFunc(filesJson); 234 | } 235 | filesRequest.onerror = function() 236 | { 237 | alert("error"); 238 | } 239 | filesRequest.open("GET", "/list", true); 240 | filesRequest.send(); 241 | }, 242 | 243 | /** @brief delete a file from the spiffs filessytem on the esp8266 */ 244 | deleteFile: function(filename, replyFunc) 245 | { 246 | var deleteFileRequest = new XMLHttpRequest(); 247 | deleteFileRequest.onload = function() 248 | { 249 | var responseJson = JSON.parse(this.responseText); 250 | replyFunc(responseJson); 251 | } 252 | deleteFileRequest.onerror = function() 253 | { 254 | alert("error"); 255 | } 256 | deleteFileRequest.open("DELETE", "/edit?f=" + filename, true); 257 | deleteFileRequest.send(); 258 | } 259 | 260 | 261 | }; 262 | -------------------------------------------------------------------------------- /data/jquery.core.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/jquery.core.min.js.gz -------------------------------------------------------------------------------- /data/jquery.knob.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/jquery.knob.min.js.gz -------------------------------------------------------------------------------- /data/log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | Huebner Inverter Management Console - Datalogger 26 | 27 | 28 | 29 | 30 |

Data Logger

31 |

32 | 33 | 34 | 35 | 36 |

37 | 38 | 39 |

40 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /data/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | var log = { 22 | 23 | items: [], 24 | samples: 0, 25 | textArea: undefined, 26 | minmax: false, 27 | stopLogging: true, 28 | 29 | /* @brief add field to data logger */ 30 | addLogItem: function() 31 | { 32 | var dataLoggerConfiguration = document.getElementById('data-logger-configuration'); 33 | 34 | // container for the drop down and the delete button 35 | var selectDiv = document.createElement("div"); 36 | selectDiv.classList.add('logger-field'); 37 | dataLoggerConfiguration.appendChild(selectDiv); 38 | 39 | // Create a drop down and populate it with the possible spot values 40 | var selectSpotValue = document.createElement("select"); 41 | selectSpotValue.classList.add('logger-field-select'); 42 | for ( var key in paramsCache.getData() ) 43 | { 44 | if ( ! paramsCache.getEntry(key).isparam ) 45 | { 46 | var option = document.createElement("option"); 47 | option.value = key; 48 | option.text = key; 49 | selectSpotValue.appendChild(option); 50 | } 51 | } 52 | selectDiv.appendChild(selectSpotValue); 53 | 54 | // Add the delete button 55 | var deleteButton = document.createElement("button"); 56 | var deleteButtonImg = document.createElement('img'); 57 | deleteButtonImg.src = '/icon-trash.png'; 58 | deleteButton.appendChild(deleteButtonImg); 59 | deleteButton.onclick = function() { this.parentNode.remove(); }; 60 | selectDiv.appendChild(deleteButton); 61 | }, 62 | 63 | /** @brief return a list of fields currently configured for logger */ 64 | getLogItems: function() 65 | { 66 | log.items = []; 67 | var formItems = document.forms["data-logger-configuration"].elements; 68 | for ( var i = 0; i < formItems.length; i++ ) 69 | { 70 | if ( formItems[i].type === 'select-one' && formItems[i].classList.contains('logger-field-select') ) 71 | { 72 | log.items.push(formItems[i].value); 73 | } 74 | } 75 | }, 76 | 77 | /* @brief start collecting log data */ 78 | start: function() 79 | { 80 | log.stopLogging = false; 81 | log.getLogItems(); 82 | log.textArea = document.getElementById("data-logger-text-area"); 83 | log.samples = document.getElementById("data-logger-samples").value; 84 | log.minmax = document.getElementById("data-logger-minmax").checked; 85 | log.textArea.innerHTML = "Timestamp" 86 | 87 | if (log.minmax) 88 | { 89 | for (var i = 0; i < log.items.length; i++) 90 | { 91 | log.textArea.innerHTML += "," + log.items[i] + " (avg)," + log.items[i] + " (min)," + log.items[i] + " (max)"; 92 | } 93 | } 94 | else 95 | { 96 | log.textArea.innerHTML += "," + log.items; 97 | } 98 | 99 | log.textArea.innerHTML += "\r\n"; 100 | log.acquire(log.samples); 101 | }, 102 | 103 | stop: function() 104 | { 105 | log.stopLogging = true; 106 | }, 107 | 108 | save: function() 109 | { 110 | var textToWrite = document.getElementById('data-logger-text-area').innerHTML; 111 | var textFileAsBlob = new Blob([ textToWrite ], { type: 'text/csv' }); 112 | var fileNameToSaveAs = "log.csv"; 113 | 114 | var downloadLink = document.createElement("a"); 115 | downloadLink.download = fileNameToSaveAs; 116 | downloadLink.innerHTML = "Download File"; 117 | if (window.webkitURL != null) 118 | { 119 | // Chrome allows the link to be clicked without actually adding it to the DOM. 120 | downloadLink.href = window.webkitURL.createObjectURL(textFileAsBlob); 121 | } else { 122 | // Firefox requires the link to be added to the DOM before it can be clicked. 123 | downloadLink.href = window.URL.createObjectURL(textFileAsBlob); 124 | downloadLink.onclick = function(event) {document.body.removeChild(event.target)}; 125 | downloadLink.style.display = "none"; 126 | document.body.appendChild(downloadLink); 127 | } 128 | 129 | downloadLink.click(); 130 | }, 131 | 132 | acquire: function(samples) 133 | { 134 | if ( log.stopLogging ){ return; } 135 | 136 | if (!log.items.length) return; 137 | 138 | inverter.getValues(log.items, log.samples, 139 | function(values) 140 | { 141 | var tzoffset = (new Date()).getTimezoneOffset() * 60000; //offset in milliseconds 142 | var localISOTime = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -1); 143 | var line = localISOTime; 144 | for (var name in values) 145 | { 146 | var avg = values[name].reduce((acc, c) => acc + c, 0) / log.samples; 147 | 148 | if (log.minmax) 149 | { 150 | line += "," + avg.toFixed(2) + "," + Math.min(...values[name]) + "," + Math.max(...values[name]); 151 | } 152 | else 153 | { 154 | line += "," + avg.toFixed(2); 155 | } 156 | } 157 | line += "\r\n"; 158 | log.textArea.innerHTML += line; 159 | log.textArea.scrollTop = log.textArea.scrollHeight; 160 | log.acquire(samples); 161 | }); 162 | }, 163 | } 164 | -------------------------------------------------------------------------------- /data/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/logo.png -------------------------------------------------------------------------------- /data/modal.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | var modal = { 22 | 23 | /** @brief Show the large dialog 24 | * @param modal - name of the modal. Valid values : large, small */ 25 | showModal: function(modal) 26 | { 27 | var m = document.getElementById(modal + "-modal-overlay"); 28 | if ( m !== undefined ){ 29 | m.style.display = 'block'; 30 | } 31 | else { 32 | console.log("warning, showModal, bad modal choice"); 33 | } 34 | }, 35 | 36 | /** @brief Hide the modal dialog 37 | * @param modal - name of the modal. Valid values : large, small */ 38 | hideModal: function(modal) 39 | { 40 | var m = document.getElementById(modal + "-modal-overlay"); 41 | if ( m !== undefined ){ 42 | m.style.display = 'none'; 43 | } 44 | else { 45 | console.log("warning, hideModal, bad modal choice"); 46 | } 47 | }, 48 | 49 | /** @brief Set the header text on the modal 50 | * @param modal - name of the modal. Valid values : large, small 51 | * @param headerText - string to place into the header field of the modal */ 52 | setModalHeader: function(modal, headerText) 53 | { 54 | var m = document.getElementById(modal + "-modal-header"); 55 | if ( m !== undefined ){ 56 | m.innerHTML = headerText; 57 | } 58 | else { 59 | console.log("warning, setModalHeader, bad modal choice"); 60 | } 61 | }, 62 | 63 | /** @brief Empty the contents of the large modal 64 | * @param modal - name of the modal. Valid values : large, small */ 65 | emptyModal: function(modal) 66 | { 67 | var m = document.getElementById(modal + "-modal-content"); 68 | if ( m !== undefined ){ 69 | m.innerHTML = ""; 70 | } 71 | else { 72 | console.log("warning, emptyModal, bad modal choice"); 73 | } 74 | }, 75 | 76 | /** @brief Append content to large modal 77 | * @param modal - name of the modal. Valid values : large, small 78 | * @appendText - string to append to the body of the modal. Note, can be html. */ 79 | appendToModal: function(modal, appendText) 80 | { 81 | var modalId = modal + "-modal-content"; 82 | //console.log("appendToModal : looking for div " + modalId); 83 | var modalContent = document.getElementById(modalId); 84 | if ( modalContent !== undefined ){ 85 | modalContent.innerHTML += appendText; 86 | } 87 | else { 88 | console.log("warning, appendToModal, bad modal choice"); 89 | } 90 | }, 91 | 92 | //* @brief auto-scroll large modal content */ 93 | largeModalScrollToBottom(){ 94 | var largeModalContent = document.getElementById("large-modal-content"); 95 | largeModalContent.scrollTop = largeModalContent.scrollHeight; 96 | }, 97 | 98 | }; -------------------------------------------------------------------------------- /data/plot.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | var plot = { 22 | 23 | stop: true, 24 | 25 | /** @brief launch new tab showing gauges based fields selected from the plot&gauge page */ 26 | launchGauges: function() 27 | { 28 | var items = ui.getPlotItems(); 29 | var req = "gauges.html?items=" + items.names.join(',') 30 | window.open(req); 31 | }, 32 | 33 | /** @brief generates chart at bottom of page */ 34 | generateChart: function() 35 | { 36 | chart = new Chart("canvas", { 37 | type: "line", 38 | options: { 39 | animation: { 40 | duration: 0 41 | }, 42 | scales: { 43 | yAxes: [{ 44 | type: "linear", 45 | display: true, 46 | position: "left", 47 | id: "left" 48 | }, { 49 | type: "linear", 50 | display: true, 51 | position: "right", 52 | id: "right", 53 | gridLines: { drawOnChartArea: false } 54 | }] 55 | } 56 | } }); 57 | }, 58 | 59 | /** @brief start plotting selected spot values */ 60 | startPlot: function() 61 | { 62 | items = ui.getPlotItems(); 63 | var colours = [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 159, 64)', 'rgb(153, 102, 255)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)' ]; 64 | 65 | chart.config.data.datasets = new Array(); 66 | 67 | for (var signalIdx = 0; signalIdx < items.names.length; signalIdx++) 68 | { 69 | var newDataset = { 70 | label: items.names[signalIdx], 71 | data: [], 72 | borderColor: colours[signalIdx % colours.length], 73 | backgroundColor: colours[signalIdx % colours.length], 74 | fill: false, 75 | pointRadius: 0, 76 | yAxisID: items.axes[signalIdx] 77 | }; 78 | chart.config.data.datasets.push(newDataset); 79 | } 80 | 81 | ui.setAutoReload(false); 82 | time = 0; 83 | chart.update(); 84 | plot.stop = false; 85 | document.getElementById("pauseButton").disabled = false; 86 | plot.acquire(); 87 | }, 88 | 89 | /** @brief Stop plotting */ 90 | stopPlot: function() 91 | { 92 | plot.stop = true; 93 | document.getElementById("pauseButton").disabled = false; 94 | ui.setAutoReload(true); 95 | }, 96 | 97 | /** @brief pause or resume plotting */ 98 | pauseResumePlot: function() 99 | { 100 | if (plot.stop) 101 | { 102 | plot.stop = false; 103 | plot.acquire(); 104 | } 105 | else 106 | { 107 | plot.stop = true; 108 | } 109 | }, 110 | 111 | acquire: function() 112 | { 113 | if (plot.stop) return; 114 | if (!items.names.length) return; 115 | var burstLength = document.getElementById('burstLength').value; 116 | var maxValues = document.getElementById('maxValues').value; 117 | 118 | inverter.getValues(items.names, burstLength, 119 | function(values) 120 | { 121 | for (var i = 0; i < burstLength; i++) 122 | { 123 | chart.config.data.labels.push(time); 124 | time++; 125 | } 126 | chart.config.data.labels.splice(0, Math.max(chart.config.data.labels.length - maxValues, 0)); 127 | 128 | for (var name in values) 129 | { 130 | var data = chart.config.data.datasets.find(function(element) { return element.label == name }).data; 131 | 132 | for (var i = 0; i < values[name].length; i++) 133 | { 134 | data.push(values[name][i]) 135 | data.splice(0, Math.max(data.length - maxValues, 0)); 136 | } 137 | } 138 | 139 | chart.update(); 140 | plot.acquire(); 141 | }); 142 | }, 143 | 144 | 145 | } 146 | -------------------------------------------------------------------------------- /data/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsphuebner/esp32-web-interface/869b5de1d2baea3b35d26e6f96e832565c9f6236/data/refresh.png -------------------------------------------------------------------------------- /data/remote.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | openinverter.org remote support module 4 | 5 | 6 | 7 | 8 |

Remote Support Module

9 | Welcome to remote support! In order for this module to work, this page must be opened on 10 | a device that is connected to both the inverter AND the internet. There are multiple ways 11 | to achieve this. Pick the one that suits you best. 12 |
    13 |
  • Open it on a smart phone that is connected to the internet via mobile data and to the inverter via wifi. Make sure your phone never enters standby mode during the support session. 14 |
  • Open it on a regular PC/laptop that is connected to the internet via Ethernet and connected to the inverter via wifi 15 |
  • Make the inverter wifi module connect to your local hotspot and access this wifi interface via the hotpot assigned IP address 16 |
17 | Below you can see which commands are issued by remote support. 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /data/rtc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | RTC Settings 26 | 27 | 94 | 102 | 103 | 104 |

Real Time Clock Settings

105 |

Current RTC Timestamp:

106 |

Device Timestamp:

107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /data/sdcard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | SD Card 26 | 27 | 28 | 29 | 84 | 87 | 88 | 89 | 94 |

SD Card

95 |
96 | 97 |
98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /data/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the esp8266 web interface 3 | * 4 | * Copyright (C) 2018 Johannes Huebner 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | * 19 | */ 20 | 21 | * { 22 | transition: 0.5s; 23 | -ms-overflow-style: none; 24 | scrollbar-width: none; 25 | } 26 | 27 | a { 28 | text-decoration: none; 29 | color: #4CAF50; 30 | } 31 | 32 | a:visited { 33 | color: #4CAF50; 34 | } 35 | 36 | p { 37 | margin: 0em 0em 0.5em 0rem ; 38 | } 39 | 40 | h2 { 41 | padding-top: 5px; 42 | } 43 | 44 | h3 { 45 | padding: 20px 0px 10px 0px; 46 | margin: 0px; 47 | } 48 | 49 | .underline { 50 | border-bottom: solid 1px #CCC; 51 | } 52 | 53 | html, body { 54 | font-family: sans-serif; 55 | font-size: 12pt; 56 | -webkit-text-size-adjust: none; 57 | -moz-text-size-adjust: none; 58 | -ms-text-size-adjust: none; 59 | margin:0; 60 | padding:0; 61 | height: 100%; 62 | } 63 | 64 | 65 | 66 | .graph { 67 | width: 298px; /* width and height are arbitrary, just make sure the #bar styles are changed accordingly */ 68 | height: 30px; 69 | border: 1px solid #888; 70 | background: rgb(168,168,168); 71 | background: -moz-linear-gradient(top, rgba(168,168,168,1) 0%, rgba(204,204,204,1) 23%); 72 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(168,168,168,1)), color-stop(23%,rgba(204,204,204,1))); 73 | background: -webkit-linear-gradient(top, rgba(168,168,168,1) 0%,rgba(204,204,204,1) 23%); 74 | background: -o-linear-gradient(top, rgba(168,168,168,1) 0%,rgba(204,204,204,1) 23%); 75 | background: -ms-linear-gradient(top, rgba(168,168,168,1) 0%,rgba(204,204,204,1) 23%); 76 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a8a8a8', endColorstr='#cccccc',GradientType=0 ); 77 | background: linear-gradient(top, rgba(168,168,168,1) 0%,rgba(204,204,204,1) 23%); 78 | position: relative; 79 | } 80 | 81 | #upload-firmware-bar { 82 | height: 29px; /* Not 30px because the 1px top-border brings it up to 30px to match #graph */ 83 | background: rgb(255,197,120); 84 | background: -moz-linear-gradient(top, rgba(255,197,120,1) 0%, rgba(244,128,38,1) 100%); 85 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,197,120,1)), color-stop(100%,rgba(244,128,38,1))); 86 | background: -webkit-linear-gradient(top, rgba(255,197,120,1) 0%,rgba(244,128,38,1) 100%); 87 | background: -o-linear-gradient(top, rgba(255,197,120,1) 0%,rgba(244,128,38,1) 100%); 88 | background: -ms-linear-gradient(top, rgba(255,197,120,1) 0%,rgba(244,128,38,1) 100%); 89 | background: linear-gradient(top, rgba(255,197,120,1) 0%,rgba(244,128,38,1) 100%); 90 | border-top: 1px solid #fceabb; 91 | } 92 | 93 | #upload-firmware-bar p { 94 | position: absolute; 95 | text-align: center; 96 | width: 100%; 97 | margin: 0; 98 | line-height: 30px; 99 | } 100 | 101 | .error { 102 | /* These styles are arbitrary */ 103 | background-color: #fceabb; 104 | padding: 1em; 105 | font-weight: bold; 106 | color: red; 107 | border: 1px solid red; 108 | } 109 | 110 | 111 | 112 | /* Navbar */ 113 | 114 | #navbar { 115 | background-color: #4CAF50; 116 | min-height: 100vh; 117 | position: fixed; 118 | width: 180px; 119 | overflow-y: scroll; 120 | height:100%; 121 | overflow-x:hidden; 122 | } 123 | 124 | #navbar a { 125 | text-decoration: none; 126 | display: block; 127 | margin:0; 128 | color: black; 129 | float: left; 130 | } 131 | 132 | #navbar img { 133 | display: block; 134 | float: left; 135 | padding: 5px 10px 5px 10px; 136 | } 137 | 138 | #navbar p { 139 | margin: 0; 140 | vertical-align: middle; 141 | } 142 | 143 | /* Logo */ 144 | 145 | #logo { 146 | padding: 10px 0 10px 0; 147 | height: 100px; 148 | margin: 0; 149 | } 150 | 151 | #logo img { 152 | width: 50%; 153 | margin-left: auto; 154 | margin-right: auto; 155 | display: block; 156 | float: none; 157 | } 158 | 159 | 160 | /* content-wrapper */ 161 | 162 | #content-wrapper { 163 | display: block; 164 | width: calc(100% - 180px); 165 | flex: 1; 166 | height: 100%; 167 | min-height: 100vh; 168 | position: absolute; 169 | top: 0; 170 | right: 0; 171 | bottom: 0; 172 | left: 180px; 173 | } 174 | 175 | #content-wrapper-inner { 176 | display: flex; 177 | flex-direction: row; 178 | flex-wrap: nowrap; 179 | width: 100%; 180 | min-height: 100vh; 181 | position: relative; 182 | align-items: stretch; 183 | } 184 | 185 | /* Button & forms */ 186 | 187 | textarea { 188 | width: 98%; 189 | } 190 | 191 | input[type=number]{ 192 | width: 6em; 193 | } 194 | 195 | form { 196 | margin:0px; padding:0px; display:inline; 197 | } 198 | 199 | .butt { 200 | font-size: 100%; 201 | padding: 5px; 202 | padding-right: 10px; 203 | background-color: #4CAF50; 204 | color: black; 205 | -webkit-transition-duration: 0.4s; /* Safari */ 206 | transition-duration: 0.4s; 207 | border: 2px solid #307533; 208 | margin: 5px 5px 5px 0px; 209 | display: flex; 210 | justify-content: safe left; 211 | align-items: center; 212 | flex-grow: 0; 213 | border-radius: 4px; 214 | } 215 | 216 | .butt label:hover { 217 | background-color: #efefef; 218 | color: black; 219 | } 220 | 221 | button { 222 | font-size: 100%; 223 | padding: 5px; 224 | padding-right: 10px; 225 | background-color: #4CAF50; 226 | color: black; 227 | -webkit-transition-duration: 0.4s; /* Safari */ 228 | transition-duration: 0.4s; 229 | border: 2px solid #307533; 230 | margin: 5px 5px 5px 0px; 231 | display: flex; 232 | justify-content: center; 233 | align-items: center; 234 | border-radius: 4px; 235 | } 236 | 237 | button:hover, input[type=file]:hover, input[type=button]:hover { 238 | background-color: #efefef; 239 | color: black; 240 | } 241 | 242 | .buttonimg { 243 | padding: 0 10px 0 10px; 244 | width: 20px; 245 | } 246 | 247 | input[type=file], input[type=button] { 248 | } 249 | 250 | select { 251 | appearance: none; 252 | font-size: 12pt; 253 | margin: 5px; 254 | padding: 5px; 255 | border: 2px solid #4CAF50; 256 | -webkit-appearance: none; 257 | -moz-appearance: none; 258 | background: #FFF; 259 | } 260 | 261 | input[type=number] { 262 | margin: 5px; 263 | padding: 5px; 264 | border: 2px solid #4CAF50; 265 | border-radius: 4px; 266 | font-size: 12pt; 267 | } 268 | 269 | /* tables */ 270 | 271 | thead tr th { 272 | position: sticky; 273 | top: 0; 274 | padding: 7px; 275 | } 276 | 277 | table { 278 | border-collapse: collapse; 279 | width: 100%; 280 | } 281 | 282 | tbody { 283 | overflow: auto; 284 | } 285 | 286 | td { 287 | padding: 2px; 288 | text-align: left; 289 | border-bottom: 1px solid #ddd; 290 | } 291 | 292 | th { 293 | padding: 2px; 294 | text-align: left; 295 | border-bottom: 1px solid #ddd; 296 | background-color:#e5e5e5; 297 | z-index: 1; 298 | } 299 | 300 | tr:hover { 301 | background-color:#f5f5f5; 302 | } 303 | 304 | .tabdiv { 305 | display: none; 306 | margin-left: 60px; 307 | padding-left: 20px; 308 | float: left; 309 | background-color:#FFFFFF; 310 | } 311 | 312 | .tablink { 313 | width: 200px; 314 | display: flex; 315 | justify-content: center; 316 | align-items: center; 317 | line-height: 30px; 318 | padding: 0px; 319 | } 320 | 321 | .fullheight { 322 | height: 90vh; 323 | } 324 | 325 | /* content */ 326 | 327 | #content { 328 | display: flex; 329 | height: 100%; 330 | } 331 | 332 | /* paramaters */ 333 | 334 | #params table { 335 | width: 100%; 336 | } 337 | 338 | #canvas { 339 | width: 100%; 340 | } 341 | 342 | /* content divs */ 343 | 344 | .main-content { 345 | order: 2; 346 | margin: 0px; 347 | flex: 10 1 300px; 348 | height: 100vh; 349 | } 350 | 351 | .main-left { 352 | order: 2; 353 | flex: 10 1 200px; 354 | margin-right: 10px; 355 | height: 100vh; 356 | overflow-y: auto; 357 | } 358 | 359 | .main-right { 360 | height: 100%; 361 | padding-top: 10px; 362 | order: 3; 363 | flex: 0 0 15%; 364 | } 365 | 366 | /* dashboard */ 367 | 368 | .dash-row { 369 | display: flex; 370 | } 371 | 372 | .dash-box { 373 | padding: 15px; 374 | margin: 5px; 375 | border-radius: 10px; 376 | border: 2px solid #444; 377 | height: 200px; 378 | flex: 1 1 300px; 379 | } 380 | 381 | .dash-box h3 { 382 | padding: 5px 0px 10px 0px; 383 | } 384 | 385 | /* CAN Mapping */ 386 | 387 | #addcanmapping { 388 | display: none; 389 | padding: 10px; 390 | border-radius: 20px; 391 | background: #DDD; 392 | margin-top: 30px; 393 | } 394 | 395 | #addcanmapping h3 { 396 | padding: 8px; 397 | } 398 | 399 | #canmapping tbody { 400 | padding: 100px; 401 | } 402 | 403 | /* modal */ 404 | 405 | .modal-overlay { 406 | display: none; 407 | position: fixed; 408 | width: 100%; 409 | height: 100%; 410 | z-index: 5; 411 | left: 0; 412 | top: 0; 413 | background-color: rgb(0,0,0); 414 | background-color: rgba(0,0,0,0.4); 415 | //overflow: auto; 416 | } 417 | 418 | .large-modal-container { 419 | background-color: #fefefe; 420 | margin: 5% auto; 421 | padding: 40px; 422 | border: 1px solid #888; 423 | border-radius: 10px; 424 | width: 50%; 425 | height: 50%; 426 | } 427 | 428 | .large-modal-container h2 { 429 | padding-bottom: 10px; 430 | display: inline; 431 | } 432 | 433 | #large-modal-header-div { 434 | padding: 0 0 1em 0; 435 | } 436 | 437 | .modal-content { 438 | //border: 1px solid #DDD; 439 | height: 90%; 440 | width: 100%; 441 | overflow: auto; 442 | } 443 | 444 | .modal-close { 445 | color: #aaaaaa; 446 | float: right; 447 | font-size: 28px; 448 | font-weight: bold; 449 | } 450 | 451 | .small-modal-container { 452 | background-color: #fefefe; 453 | margin: 5% auto; 454 | padding: 20px 40px 20px 40px; 455 | border: 1px solid #888; 456 | border-radius: 10px; 457 | width: 300px; 458 | } 459 | 460 | .can-mapping-modal-container { 461 | background-color: #fefefe; 462 | margin: 5% auto; 463 | padding: 40px; 464 | border: 1px solid #888; 465 | width: 50%; 466 | border-radius: 5px; 467 | } 468 | 469 | #commandoutput { 470 | height: calc(100% - 60px); 471 | overflow: auto; 472 | margin-bottom: 5px; 473 | } 474 | 475 | #commandinput { 476 | width: 80%; 477 | } 478 | 479 | #wifiInner { 480 | display: block; 481 | } 482 | 483 | /* plot */ 484 | 485 | .plotField { 486 | display: flex; 487 | } 488 | 489 | /* logger */ 490 | 491 | .logger-field { 492 | display: flex; 493 | } 494 | 495 | /* controls below navbar */ 496 | #nodeid-div input { 497 | width: 3em; 498 | } 499 | 500 | .control { 501 | margin-left: 10px; 502 | margin-top: 10px; 503 | } 504 | 505 | .beta-feature { 506 | display: none; 507 | } 508 | /* version */ 509 | 510 | #version { 511 | padding: 0px; 512 | padding-top: 5px; 513 | padding-bottom: 5px; 514 | margin: 0px; 515 | margin-left: 10px; 516 | margin-bottom: 10px; 517 | text-align: center; 518 | float: left; 519 | width: 160px; 520 | font-size: 10pt; 521 | border: 1px solid black; 522 | border-radius: 4px; 523 | height: 30px; 524 | 525 | } 526 | 527 | #version p { 528 | font-size: 12px; 529 | padding: 0px; 530 | margin: 0px; 531 | color: #444; 532 | margin-bottom: auto; 533 | } 534 | /* switch */ 535 | 536 | /* The switch - the box around the slider */ 537 | .switch { 538 | position: relative; 539 | display: inline-block; 540 | width: 30px; 541 | height: 17px; 542 | margin-right: 10px; 543 | } 544 | 545 | /* Hide default HTML checkbox */ 546 | .switch input { 547 | opacity: 0; 548 | width: 0; 549 | height: 0; 550 | } 551 | 552 | /* The slider */ 553 | 554 | .slider { 555 | position: absolute; 556 | cursor: pointer; 557 | top: 0; 558 | left: 0; 559 | right: 0; 560 | bottom: 0; 561 | background-color: #ccc; 562 | -webkit-transition: .4s; 563 | transition: .4s; 564 | } 565 | 566 | .slider:before { 567 | position: absolute; 568 | content: ""; 569 | height: 9px; 570 | width: 9px; 571 | left: 4px; 572 | bottom: 4px; 573 | background-color: white; 574 | -webkit-transition: .4s; 575 | transition: .4s; 576 | } 577 | 578 | input:checked + .slider { 579 | background-color: orange; 580 | } 581 | 582 | input:focus + .slider { 583 | box-shadow: 0 0 1px #2196F3; 584 | } 585 | 586 | input:checked + .slider:before { 587 | -webkit-transform: translateX(13px); 588 | -ms-transform: translateX(13px); 589 | transform: translateX(13px); 590 | } 591 | 592 | /* Rounded sliders */ 593 | .slider.round { 594 | border-radius: 17px; 595 | } 596 | 597 | .slider.round:before { 598 | border-radius: 50%; 599 | } 600 | 601 | 602 | /** TOOLTIP */ 603 | 604 | 605 | .tooltip { 606 | position: relative; 607 | display: inline-block; 608 | border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ 609 | } 610 | 611 | .tooltip .tooltiptext { 612 | visibility: hidden; 613 | width: 120px; 614 | background-color: black; 615 | color: #fff; 616 | text-align: center; 617 | padding: 10px; 618 | border-radius: 6px; 619 | position: absolute; 620 | z-index: 10; 621 | top: -5px; 622 | left: 120%; 623 | } 624 | 625 | .tooltip:hover .tooltiptext { 626 | visibility: visible; 627 | } 628 | 629 | @media only screen and (max-width: 1000px) { 630 | #logo { 631 | width: 80px; 632 | height: 50px; 633 | } 634 | .buttonimg { 635 | width: 60px; 636 | } 637 | } 638 | 639 | /* notification bar */ 640 | 641 | .communication-error-bar { 642 | display: none; 643 | padding: 15px; 644 | background-color: #f44336; 645 | color: white; 646 | z-index: 100; 647 | margin: 0 auto; 648 | position: relative; 649 | width: 30%; 650 | top: 0px; 651 | clear: left; 652 | height: 20px; 653 | border-radius: 0px 0px 20px 20px; 654 | } 655 | 656 | .communication-error-bar p { 657 | text-align: center; 658 | } 659 | -------------------------------------------------------------------------------- /data/syncofs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | Syncofs 26 | 27 | 28 | 29 | 30 | 31 | 235 | 243 | 244 | 245 | 246 | 247 | 250 | 253 | 254 | 255 | 260 | 261 | 262 | 265 | 268 | 269 | 270 | 271 | 272 |
248 |

syncofs

249 |
251 |

manualid

252 |
256 | 257 | 258 | 259 |
263 | 264 | 266 | 267 |
273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /data/wifi-updated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | 25 | Modify Wifi settings 26 | 27 | 28 | 29 | Wifi Settings updated. Return to main page. 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /data/wifi.html: -------------------------------------------------------------------------------- 1 | 20 |
21 |

Access Point Settings

22 | Here you can specify SSID and password of the access point that is created by the inverter. 23 |
24 |

25 |


26 | 27 | 29 |
30 |
31 |

Station Settings

32 | Here you can specify a Wifi network that the inverter joins. 33 |
34 |

35 |

36 | Current IP Address: %staIP% 37 | 38 | 40 |
41 |
-------------------------------------------------------------------------------- /data/wifi.js: -------------------------------------------------------------------------------- 1 | var wifi = { 2 | 3 | wifiValidatePasswordLength: function(pw) 4 | { 5 | document.getElementById("apsubmit").disabled = pw.length < 8; 6 | }, 7 | 8 | populateWiFiTab: function() 9 | { 10 | var wifiTab = document.getElementById("wifi"); 11 | var wifiFetchRequest = new XMLHttpRequest(); 12 | wifiFetchRequest.onload = function() 13 | { 14 | wifiTab.innerHTML = this.responseText; 15 | } 16 | wifiFetchRequest.open("GET", "/wifi"); 17 | wifiFetchRequest.send(); 18 | }, 19 | 20 | } -------------------------------------------------------------------------------- /doc/ARDUINO_IDE_setup.md: -------------------------------------------------------------------------------- 1 | Arduino IDE setup 2 | ================= 3 | 4 | # Table of Contents 5 |
6 | Click to open TOC 7 | 8 | 9 | - [About Arduino IDE](#about-arduino-ide) 10 | - [Installing Arduino IDE and plugins](#installing-arduino-ide-and-plugins) 11 | - [Configuring Arduino IDE](#configuring-arduino-ide) 12 | 13 | 14 |
15 | 16 | # About Arduino IDE 17 | 18 | Arduino IDE is an open-source integrated development environment with support for multiple platforms. 19 | 20 | Learn more : https://www.arduino.cc/en/software 21 | 22 | # Installing Arduino IDE and plugins 23 | 24 | [Download](https://www.arduino.cc/en/software#download) the IDE, and follow the [Getting Started](https://www.arduino.cc/en/Guide) 25 | guide. 26 | 27 | Additionally, install (by following the instructions in the following links) the 2 following IDE plugins: 28 | * https://github.com/esp8266/arduino-esp8266fs-plugin 29 | * https://github.com/earlephilhower/arduino-esp8266littlefs-plugin 30 | 31 | When you start the Arduino IDE, you should now have two additional options in the `Tools` menu: 32 | * ESP8266 LittleFS Data Upload 33 | * ESP8266 Sketch Data Upload 34 | 35 | # Configuring Arduino IDE 36 | 37 | In the `Preferences` pane for the IDE, look for `Additional Boards Manager URLs`, click on the button on the right, and append the following URL: 38 | `https://arduino.esp8266.com/stable/package_esp8266com_index.json` 39 | 40 | In the `Tools` menu, select the `Board` entry, click on the `Boards Manager...` submenu, enter `esp8266` in the search box and press Enter. 41 | 42 | You should have one entry named `esp8266 by ESP8266 Community` ; click on `Install` and wait for installation. 43 | 44 | Open the project `File` > `Open` and navigate to the `FSBrowser.ino` file, and open it. 45 | 46 | Go back to the `Tools` menu, and in the `Board` entry select the `ESP8266 Boards` choose your board (`Olimex MOD-WIFI-ESP8266(-DEV)) 47 | 48 | Configure the other parameters the following way: 49 | 50 | * Upload Speed : 921600 51 | * CPU Frequency : 80MHz 52 | * Flash Size: 2MB (FS:512KB OTA:~768KB) 53 | * Debug port: Disabled 54 | * Debug level: None 55 | * lwIP Variant: v2 Lower Memory 56 | * VTables: Flash 57 | * C++ Exceptions: Disabled (new aborts on oom) 58 | * Stack Protection: Disabled 59 | * Erase Flash: Only Sketch 60 | * SSL Support: All SSL Ciphers (most compatible) 61 | * MMU: 32KB cache + 32KB IRAM (balanced) 62 | * Non-32-bit access: Use pgm_read macros for IRAM/PROGMEM 63 | * Port: (_lookup the port on which your USB/Serial adapter is. You can also choose the board if it's up, connected to your WiFi, for OTA flashing_) 64 | 65 | That's it ! Your IDE should now be configured for your day to day operations. 66 | -------------------------------------------------------------------------------- /doc/ARDUINO_IDE_usage.md: -------------------------------------------------------------------------------- 1 | Arduino IDE usage 2 | ================= 3 | 4 | # Table of Contents 5 |
6 | Click to open TOC 7 | 8 | 9 | - [Compilation](#compilation) 10 | - [Compile and Flash board](#compile-and-flash-board) 11 | - [Flashing the filesystem](#flashing-the-filesystem) 12 | - [Debugging](#debugging) 13 | 14 | 15 |
16 | 17 | # Compilation 18 | To compile, you need to choose, in the `Sketch` menu, `Verify / Compile` 19 | 20 | # Compile and Flash board 21 | Choose, in the `Sketch` menu, `Upload`. It will compile and send to the board with the chosen method (serial / OTA) 22 | 23 | # Flashing the filesystem 24 | Choose, in the `Tools` menu, the `ESP8266 Sketch Data Upload` option, which will package, and flash the files in the `data` directory (HTML files for the web interface) 25 | 26 | # Debugging 27 | In case you're debugging and have enabled (`Tools` > `Debug port` / `Tools` > `Debug level`) some kind of debugging output, you may use the `Tools` / `Serial monitor` to check the debug messages. 28 | 29 | Please note that the serial port is intended to be attached to the inverter (or other application), so do not leave debug messages in normal operation. -------------------------------------------------------------------------------- /doc/PLATFORMIO_setup.md: -------------------------------------------------------------------------------- 1 | # About PlatformIO 2 | 3 | PlatformIO is an open-source development environment with support for multiple platforms. 4 | 5 | Learn more : https://docs.platformio.org/ 6 | 7 | # Installing PlatformIO Core (Command line) 8 | 9 | Follow these instructions for the initial install: 10 | * [PlatformIO Core](http://docs.platformio.org/page/core.html) 11 | 12 | Ensure that the tools are properly installed, and that you can test the following command: 13 | ``` 14 | $ pio --version 15 | PlatformIO Core, version 6.1.0 16 | ``` 17 | 18 | # Customization of the project configuration file 19 | 20 | In some cases you will want to customize some aspects of the project configuration file `platformio.ini`. 21 | 22 | If these aspects are useful to others, then you should consider doing a PR to share those change. 23 | 24 | However in some cases there are some changes that are specific to your working environment, for example the 25 | name of the serial port on which you communicate with the ESP8266 board, etc... 26 | 27 | To that end, you can create a local file named `platformio-local-override.ini` ; which is explicitely ignored 28 | by the git version control (cf `.gitignore`). 29 | In this file you'll be able to override the settings of the main `platformio.ini` file without having pending 30 | file modifications. 31 | 32 | As a starting point you can create it from the existing `platformio-local-override.ini.example`: 33 | ``` 34 | cp platformio-local-override.ini.example platformio-local-override.ini 35 | ``` 36 | and modify the file according to your own tastes / needs. 37 | -------------------------------------------------------------------------------- /doc/PLATFORMIO_usage.md: -------------------------------------------------------------------------------- 1 | PlatformIO usage 2 | ================ 3 | 4 | # Table of Contents 5 |
6 | Click to open TOC 7 | 8 | 9 | - [Existing targets and environments](#existing-targets-and-environments) 10 | - [Building the code](#building-the-code) 11 | - [Flashing resulting firmware to the ESP8266 board](#flashing-resulting-firmware-to-the-esp8266-board) 12 | - [Check device output](#check-device-output) 13 | - [ROM bootloader messages](#rom-bootloader-messages) 14 | - [Normal output to the inverter](#normal-output-to-the-inverter) 15 | - [Building and flashing the filesystem \(optional\)](#building-and-flashing-the-filesystem-optional) 16 | - [Building the filesystem](#building-the-filesystem) 17 | - [Flashing the filesystem](#flashing-the-filesystem) 18 | - [Clean build files if needed](#clean-build-files-if-needed) 19 | 20 | 21 |
22 | 23 | All the following commands assume that you are at the 'root' of the project, i.e. 24 | in the same directory as the `platformio.ini` file. 25 | 26 | # Existing targets and environments 27 | You can list existing target and environments with 28 | ``` 29 | $ pio run --list-targets 30 | Environment Group Name Title Description 31 | ------------- -------- ----------- --------------------------- ---------------------- 32 | debug Platform buildfs Build Filesystem Image 33 | debug Platform erase Erase Flash 34 | debug Platform size Program Size Calculate program size 35 | debug Platform upload Upload 36 | debug Platform uploadfs Upload Filesystem Image 37 | debug Platform uploadfsota Upload Filesystem Image OTA 38 | 39 | release Platform buildfs Build Filesystem Image 40 | release Platform erase Erase Flash 41 | release Platform size Program Size Calculate program size 42 | release Platform upload Upload 43 | release Platform uploadfs Upload Filesystem Image 44 | release Platform uploadfsota Upload Filesystem Image OTA 45 | ``` 46 | 47 | Some additional targets exist but are not show in this list: 48 | * `clean` : clean all built files 49 | * `envdump` : dump current build environment 50 | * `monitor` : automatically start pio device monitor after successful build operation. 51 | 52 | You will be able to select an environment with the `-e`, or `--environment` command line flag. The default environment is `release` 53 | You will be able to select an target with the `-t`, or `--target` command line flag. With no flag, the default behaviour is to build the sources. 54 | 55 | # Building the code 56 | Run this command (need to do this after each update of the files): 57 | 58 | ``` 59 | $ pio run 60 | Processing release (platform: espressif8266; framework: arduino; board: modwifi) 61 | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 62 | Verbose mode can be enabled via `-v, --verbose` option 63 | CONFIGURATION: https://docs.platformio.org/page/boards/espressif8266/modwifi.html 64 | PLATFORM: Espressif 8266 (4.0.1) > Olimex MOD-WIFI-ESP8266(-DEV) 65 | HARDWARE: ESP8266 80MHz, 80KB RAM, 2MB Flash 66 | PACKAGES: 67 | - framework-arduinoespressif8266 @ 3.30002.0 (3.0.2) 68 | - tool-esptool @ 1.413.0 (4.13) 69 | - tool-esptoolpy @ 1.30300.0 (3.3.0) 70 | - toolchain-xtensa @ 2.100300.210717 (10.3.0) 71 | Converting FSBrowser.ino 72 | LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf 73 | LDF Modes: Finder ~ chain, Compatibility ~ soft 74 | Found 35 compatible libraries 75 | Scanning dependencies... 76 | Dependency Graph 77 | |-- ArduinoOTA @ 1.0 78 | | |-- ESP8266WiFi @ 1.0 79 | | |-- ESP8266mDNS @ 1.2 80 | | | |-- ESP8266WiFi @ 1.0 81 | |-- ESP8266HTTPUpdateServer @ 1.0 82 | | |-- ESP8266WebServer @ 1.0 83 | | | |-- ESP8266WiFi @ 1.0 84 | | |-- ESP8266WiFi @ 1.0 85 | |-- ESP8266WebServer @ 1.0 86 | | |-- ESP8266WiFi @ 1.0 87 | |-- ESP8266WiFi @ 1.0 88 | |-- ESP8266mDNS @ 1.2 89 | | |-- ESP8266WiFi @ 1.0 90 | |-- Ticker @ 1.0 91 | Building in release mode 92 | Compiling .pio/build/release/src/FSBrowser.ino.cpp.o 93 | Compiling .pio/build/release/src/src/arm_debug.cpp.o 94 | ... 95 | 96 | ... 97 | Compiling .pio/build/release/FrameworkArduino/umm_malloc/umm_malloc.cpp.o 98 | Compiling .pio/build/release/FrameworkArduino/umm_malloc/umm_poison.c.o 99 | Archiving .pio/build/release/libFrameworkArduino.a 100 | Indexing .pio/build/release/libFrameworkArduino.a 101 | Linking .pio/build/release/firmware.elf 102 | Retrieving maximum program size .pio/build/release/firmware.elf 103 | Checking size .pio/build/release/firmware.elf 104 | Advanced Memory Usage is available via "PlatformIO Home > Project Inspect" 105 | RAM: [==== ] 38.9% (used 31896 bytes from 81920 bytes) 106 | Flash: [==== ] 36.9% (used 385089 bytes from 1044464 bytes) 107 | Building .pio/build/release/firmware.bin 108 | Creating BIN file ".pio/build/release/firmware.bin" using "..../.platformio/packages/framework-arduinoespressif8266/bootloaders/eboot/eboot.elf" and ".pio/build/release/firmware.elf" 109 | ====================================================================================================================================== [SUCCESS] Took 9.97 seconds ====================================================================================================================================== 110 | 111 | Environment Status Duration 112 | ------------- -------- ------------ 113 | release SUCCESS 00:00:09.971 114 | ====================================================================================================================================== 115 | ``` 116 | 117 | 118 | # Flashing resulting firmware to the ESP8266 board 119 | Note: you should first setup the ESP8266 in UART mode. (In general, keep the button depressed when applying power, then release the button) 120 | 121 | ``` 122 | $ pio run --target upload 123 | Processing release (platform: espressif8266; framework: arduino; board: modwifi) 124 | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 125 | Verbose mode can be enabled via `-v, --verbose` option 126 | CONFIGURATION: https://docs.platformio.org/page/boards/espressif8266/modwifi.html 127 | PLATFORM: Espressif 8266 (4.0.1) > Olimex MOD-WIFI-ESP8266(-DEV) 128 | HARDWARE: ESP8266 80MHz, 80KB RAM, 2MB Flash 129 | PACKAGES: 130 | - framework-arduinoespressif8266 @ 3.30002.0 (3.0.2) 131 | - tool-esptool @ 1.413.0 (4.13) 132 | - tool-esptoolpy @ 1.30300.0 (3.3.0) 133 | - tool-mklittlefs @ 1.203.210628 (2.3) 134 | - tool-mkspiffs @ 1.200.0 (2.0) 135 | - toolchain-xtensa @ 2.100300.210717 (10.3.0) 136 | Converting FSBrowser.ino 137 | LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf 138 | LDF Modes: Finder ~ chain, Compatibility ~ soft 139 | Found 35 compatible libraries 140 | Scanning dependencies... 141 | Dependency Graph 142 | |-- ArduinoOTA @ 1.0 143 | | |-- ESP8266WiFi @ 1.0 144 | | |-- ESP8266mDNS @ 1.2 145 | | | |-- ESP8266WiFi @ 1.0 146 | |-- ESP8266HTTPUpdateServer @ 1.0 147 | | |-- ESP8266WebServer @ 1.0 148 | | | |-- ESP8266WiFi @ 1.0 149 | | |-- ESP8266WiFi @ 1.0 150 | |-- ESP8266WebServer @ 1.0 151 | | |-- ESP8266WiFi @ 1.0 152 | |-- ESP8266WiFi @ 1.0 153 | |-- ESP8266mDNS @ 1.2 154 | | |-- ESP8266WiFi @ 1.0 155 | |-- Ticker @ 1.0 156 | Building in release mode 157 | Compiling .pio/build/release/src/FSBrowser.ino.cpp.o 158 | ... 159 | 160 | ... 161 | Retrieving maximum program size .pio/build/release/firmware.elf 162 | Checking size .pio/build/release/firmware.elf 163 | Advanced Memory Usage is available via "PlatformIO Home > Project Inspect" 164 | RAM: [==== ] 38.9% (used 31896 bytes from 81920 bytes) 165 | Flash: [==== ] 36.9% (used 385089 bytes from 1044464 bytes) 166 | Configuring upload protocol... 167 | AVAILABLE: espota, esptool 168 | CURRENT: upload_protocol = esptool 169 | Looking for upload port... 170 | Using manually specified: /dev/cu.usbserial-DGAJb113318 171 | Uploading .pio/build/release/firmware.bin 172 | esptool.py v3.3 173 | Serial port /dev/cu.usbserial-DGAJb113318 174 | WARNING: Pre-connection option "no_reset" was selected. Connection may fail if the chip is not in bootloader or flasher stub mode. 175 | Connecting.... 176 | Chip is ESP8266EX 177 | Features: WiFi 178 | Crystal is 26MHz 179 | MAC: c4:5b:be:76:1b:b8 180 | Uploading stub... 181 | Running stub... 182 | Stub running... 183 | Changing baud rate to 921600 184 | Changed. 185 | Configuring flash size... 186 | Flash will be erased from 0x00000000 to 0x0005ffff... 187 | Compressed 389248 bytes to 277860... 188 | Writing at 0x00000000... (5 %) 189 | Writing at 0x00005b7b... (11 %) 190 | Writing at 0x0000b880... (17 %) 191 | Writing at 0x00011ae4... (23 %) 192 | Writing at 0x00017987... (29 %) 193 | Writing at 0x0001d5ad... (35 %) 194 | Writing at 0x00022dbe... (41 %) 195 | Writing at 0x00028689... (47 %) 196 | Writing at 0x0002df5c... (52 %) 197 | Writing at 0x000330f0... (58 %) 198 | Writing at 0x000380f6... (64 %) 199 | Writing at 0x0003d293... (70 %) 200 | Writing at 0x00042b6d... (76 %) 201 | Writing at 0x000482ec... (82 %) 202 | Writing at 0x0004d696... (88 %) 203 | Writing at 0x00052cb4... (94 %) 204 | Writing at 0x0005986c... (100 %) 205 | Wrote 389248 bytes (277860 compressed) at 0x00000000 in 3.8 seconds (effective 816.7 kbit/s)... 206 | Hash of data verified. 207 | 208 | Leaving... 209 | Soft resetting... 210 | ====================================================================================================================================== [SUCCESS] Took 8.37 seconds ====================================================================================================================================== 211 | 212 | Environment Status Duration 213 | ------------- -------- ------------ 214 | release SUCCESS 00:00:08.371 215 | ====================================================================================================================================== 1 succeeded in 00:00:08.371 ====================================================================================================================================== 216 | ``` 217 | 218 | 219 | # Check device output 220 | 221 | ## ROM bootloader messages 222 | 223 | Most of the ESP8266 boards have a 26MHz crystal (instead of the standard 40MHz) so the ROM bootloader outputs at 115200 * 26/40 = 74880. 224 | 225 | These messages are not very useful but can help you to confirm that the board is working as expected. 226 | ``` 227 | $ pio device monitor --baud 74880 228 | --- Terminal on /dev/cu.usbserial-DGAJb113318 | 74880 8-N-1 229 | --- Available filters and text transformations: colorize, debug, default, direct, esp8266_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time 230 | --- More details at https://bit.ly/pio-monitor-filters 231 | --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H 232 | 233 | ets Jan 8 2013,rst cause:1, boot mode:(3,0) 234 | 235 | load 0x4010f000, len 3460, room 16 236 | tail 4 237 | chksum 0xcc 238 | load 0x3fff20b8, len 40, room 4 239 | tail 4 240 | chksum 0xc9 241 | csum 0xc9 242 | v0005f080 243 | ~ld 244 | ``` 245 | 246 | ## Normal output to the inverter 247 | 248 | In normal operations, the Web Interface wants to talk to an inverter. The following will show the messages that it tries to send to the inverter: 249 | ``` 250 | $ pio device monitor 251 | --- Terminal on /dev/cu.usbserial-DGAJb113318 | 115200 8-N-1 252 | --- Available filters and text transformations: colorize, debug, default, direct, esp8266_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time 253 | --- More details at https://bit.ly/pio-monitor-filters 254 | --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H 255 | fastuart 256 | 257 | json 258 | ``` 259 | 260 | # Building and flashing the filesystem (optional) 261 | It's also possible to automate the building of the filesystem, and its uploading. 262 | 263 | (It's optional as you can also do it by using the `/edit` endpoint of the main application - there's an `upload.sh` file for this.) 264 | 265 | ## Building the filesystem 266 | This will only build it. 267 | ``` 268 | $ pio run --target buildfs 269 | Processing release (platform: espressif8266; framework: arduino; board: modwifi) 270 | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 271 | Verbose mode can be enabled via `-v, --verbose` option 272 | CONFIGURATION: https://docs.platformio.org/page/boards/espressif8266/modwifi.html 273 | PLATFORM: Espressif 8266 (4.0.1) > Olimex MOD-WIFI-ESP8266(-DEV) 274 | HARDWARE: ESP8266 80MHz, 80KB RAM, 2MB Flash 275 | PACKAGES: 276 | - framework-arduinoespressif8266 @ 3.30002.0 (3.0.2) 277 | - tool-esptool @ 1.413.0 (4.13) 278 | - tool-esptoolpy @ 1.30300.0 (3.3.0) 279 | - tool-mklittlefs @ 1.203.210628 (2.3) 280 | - tool-mkspiffs @ 1.200.0 (2.0) 281 | - toolchain-xtensa @ 2.100300.210717 (10.3.0) 282 | Converting FSBrowser.ino 283 | LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf 284 | LDF Modes: Finder ~ chain, Compatibility ~ soft 285 | Found 35 compatible libraries 286 | Scanning dependencies... 287 | Dependency Graph 288 | |-- ArduinoOTA @ 1.0 289 | | |-- ESP8266WiFi @ 1.0 290 | | |-- ESP8266mDNS @ 1.2 291 | | | |-- ESP8266WiFi @ 1.0 292 | |-- ESP8266HTTPUpdateServer @ 1.0 293 | | |-- ESP8266WebServer @ 1.0 294 | | | |-- ESP8266WiFi @ 1.0 295 | | |-- ESP8266WiFi @ 1.0 296 | |-- ESP8266WebServer @ 1.0 297 | | |-- ESP8266WiFi @ 1.0 298 | |-- ESP8266WiFi @ 1.0 299 | |-- ESP8266mDNS @ 1.2 300 | | |-- ESP8266WiFi @ 1.0 301 | |-- Ticker @ 1.0 302 | Building in release mode 303 | Building file system image from 'FSBrowser/data' directory to .pio/build/release/spiffs.bin 304 | /wifi-updated.html 305 | /gauges.html 306 | /ajax-loader.gif 307 | /index.html 308 | /inverter.js 309 | /remote.html 310 | /chart.min.js.gz 311 | /syncofs.html 312 | /gauge.min.js.gz 313 | /log.js 314 | /index.js 315 | /jquery.core.min.js.gz 316 | /chartjs-annotation.min.js.gz 317 | /wifi.html 318 | /log.html 319 | /style.css 320 | /gauges.js 321 | /jquery.knob.min.js.gz 322 | /refresh.png 323 | ====================================================================================================================================== [SUCCESS] Took 1.38 seconds ====================================================================================================================================== 324 | 325 | Environment Status Duration 326 | ------------- -------- ------------ 327 | release SUCCESS 00:00:01.383 328 | ====================================================================================================================================== 1 succeeded in 00:00:01.383 ====================================================================================================================================== 329 | ``` 330 | 331 | ## Flashing the filesystem 332 | This action does the build + flash steps in one operation. 333 | ``` 334 | $ pio run --target uploadfs 335 | Processing release (platform: espressif8266; framework: arduino; board: modwifi) 336 | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 337 | Verbose mode can be enabled via `-v, --verbose` option 338 | CONFIGURATION: https://docs.platformio.org/page/boards/espressif8266/modwifi.html 339 | PLATFORM: Espressif 8266 (4.0.1) > Olimex MOD-WIFI-ESP8266(-DEV) 340 | HARDWARE: ESP8266 80MHz, 80KB RAM, 2MB Flash 341 | PACKAGES: 342 | - framework-arduinoespressif8266 @ 3.30002.0 (3.0.2) 343 | - tool-esptool @ 1.413.0 (4.13) 344 | - tool-esptoolpy @ 1.30300.0 (3.3.0) 345 | - tool-mklittlefs @ 1.203.210628 (2.3) 346 | - tool-mkspiffs @ 1.200.0 (2.0) 347 | - toolchain-xtensa @ 2.100300.210717 (10.3.0) 348 | Converting FSBrowser.ino 349 | LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf 350 | LDF Modes: Finder ~ chain, Compatibility ~ soft 351 | Found 35 compatible libraries 352 | Scanning dependencies... 353 | Dependency Graph 354 | |-- ArduinoOTA @ 1.0 355 | | |-- ESP8266WiFi @ 1.0 356 | | |-- ESP8266mDNS @ 1.2 357 | | | |-- ESP8266WiFi @ 1.0 358 | |-- ESP8266HTTPUpdateServer @ 1.0 359 | | |-- ESP8266WebServer @ 1.0 360 | | | |-- ESP8266WiFi @ 1.0 361 | | |-- ESP8266WiFi @ 1.0 362 | |-- ESP8266WebServer @ 1.0 363 | | |-- ESP8266WiFi @ 1.0 364 | |-- ESP8266WiFi @ 1.0 365 | |-- ESP8266mDNS @ 1.2 366 | | |-- ESP8266WiFi @ 1.0 367 | |-- Ticker @ 1.0 368 | Building in release mode 369 | Building file system image from 'FSBrowser/data' directory to .pio/build/release/spiffs.bin 370 | /wifi-updated.html 371 | /gauges.html 372 | /ajax-loader.gif 373 | /index.html 374 | /inverter.js 375 | /remote.html 376 | /chart.min.js.gz 377 | /syncofs.html 378 | /gauge.min.js.gz 379 | /log.js 380 | /index.js 381 | /jquery.core.min.js.gz 382 | /chartjs-annotation.min.js.gz 383 | /wifi.html 384 | /log.html 385 | /style.css 386 | /gauges.js 387 | /jquery.knob.min.js.gz 388 | /refresh.png 389 | Looking for upload port... 390 | Using manually specified: /dev/cu.usbserial-DGAJb113318 391 | Uploading .pio/build/release/spiffs.bin 392 | esptool.py v3.3 393 | Serial port /dev/cu.usbserial-DGAJb113318 394 | WARNING: Pre-connection option "no_reset" was selected. Connection may fail if the chip is not in bootloader or flasher stub mode. 395 | Connecting.... 396 | Chip is ESP8266EX 397 | Features: WiFi 398 | Crystal is 26MHz 399 | MAC: c4:5b:be:76:1b:b8 400 | Uploading stub... 401 | Running stub... 402 | Stub running... 403 | Changing baud rate to 921600 404 | Changed. 405 | Configuring flash size... 406 | Flash will be erased from 0x00180000 to 0x001f9fff... 407 | Compressed 499712 bytes to 140809... 408 | Writing at 0x00180000... (11 %) 409 | Writing at 0x0018c88a... (22 %) 410 | Writing at 0x001945b8... (33 %) 411 | Writing at 0x0019c553... (44 %) 412 | Writing at 0x001a4604... (55 %) 413 | Writing at 0x001af7a7... (66 %) 414 | Writing at 0x001b9998... (77 %) 415 | Writing at 0x001c5b85... (88 %) 416 | Writing at 0x001cdaa6... (100 %) 417 | Wrote 499712 bytes (140809 compressed) at 0x00180000 in 2.9 seconds (effective 1371.8 kbit/s)... 418 | Hash of data verified. 419 | 420 | Leaving... 421 | Soft resetting... 422 | ====================================================================================================================================== [SUCCESS] Took 5.93 seconds ====================================================================================================================================== 423 | 424 | Environment Status Duration 425 | ------------- -------- ------------ 426 | release SUCCESS 00:00:05.926 427 | ====================================================================================================================================== 1 succeeded in 00:00:05.926 ====================================================================================================================================== 428 | ``` 429 | 430 | # Clean build files if needed 431 | ``` 432 | $ pio run --target clean 433 | Processing release (platform: espressif8266; framework: arduino; board: modwifi) 434 | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 435 | Build environment is clean 436 | Done cleaning 437 | ====================================================================================================================================== [SUCCESS] Took 0.39 seconds ====================================================================================================================================== 438 | 439 | Environment Status Duration 440 | ------------- -------- ------------ 441 | release SUCCESS 00:00:00.389 442 | ====================================================================================================================================== 1 succeeded in 00:00:00.389 ====================================================================================================================================== 443 | ``` 444 | -------------------------------------------------------------------------------- /esp32-web-interface.cbp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 70 | 71 | -------------------------------------------------------------------------------- /esp32-web-interface.ino: -------------------------------------------------------------------------------- 1 | /* 2 | FSWebServer - Example WebServer with SPIFFS backend for esp8266 3 | Copyright (c) 2015 Hristo Gochkov. All rights reserved. 4 | This file is part of the ESP8266WebServer library for Arduino environment. 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | You should have received a copy of the GNU Lesser General Public 15 | License along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | upload the contents of the data folder with MkSPIFFS Tool ("ESP8266 Sketch Data Upload" in Tools menu in Arduino IDE) 19 | or you can upload the contents of a folder if you CD in that folder and run the following command: 20 | for file in `ls -A1`; do curl -F "file=@$PWD/$file" esp8266fs.local/edit; done 21 | 22 | access the sample web page at http://esp8266fs.local 23 | edit the page by going to http://esp8266fs.local/edit 24 | */ 25 | /* 26 | * This file is part of the esp8266 web interface 27 | * 28 | * Copyright (C) 2018 Johannes Huebner 29 | * 30 | * This program is free software: you can redistribute it and/or modify 31 | * it under the terms of the GNU General Public License as published by 32 | * the Free Software Foundation, either version 3 of the License, or 33 | * (at your option) any later version. 34 | * 35 | * This program is distributed in the hope that it will be useful, 36 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 37 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 38 | * GNU General Public License for more details. 39 | * 40 | * You should have received a copy of the GNU General Public License 41 | * along with this program. If not, see . 42 | * 43 | */ 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | 53 | #include 54 | #include "RTClib.h" 55 | #include 56 | #include 57 | #include "driver/uart.h" 58 | 59 | #define DBG_OUTPUT_PORT Serial2 60 | #define INVERTER_PORT UART_NUM_0 61 | #define INVERTER_RX 3 62 | #define INVERTER_TX 1 63 | #define UART_TIMEOUT (100 / portTICK_PERIOD_MS) 64 | #define UART_MESSBUF_SIZE 100 65 | #define LED_BUILTIN 13 //clashes with SDIO, need to change to suit hardware and uncomment lines 66 | 67 | #define RESERVED_SD_SPACE 2000000000 68 | #define SDIO_BUFFER_SIZE 16384 69 | #define FLUSH_WRITES 60 //flush file every 60 blocks 70 | 71 | #define MAX_SD_FILES 200 72 | 73 | #define LOG_DELAY_VAL 10000 74 | 75 | //HardwareSerial Inverter(INVERTER_PORT); 76 | 77 | const char* host = "inverter"; 78 | bool fastUart = false; 79 | bool fastUartAvailable = true; 80 | char uartMessBuff[UART_MESSBUF_SIZE]; 81 | 82 | WebServer server(80); 83 | HTTPUpdateServer updater; 84 | //holds the current upload 85 | File fsUploadFile; 86 | Ticker sta_tick; 87 | 88 | //SWD over ESP8266 89 | /* 90 | https://github.com/scanlime/esp8266-arm-swd 91 | */ 92 | #include 93 | 94 | RTC_PCF8523 ext_rtc; 95 | ESP32Time int_rtc; 96 | bool haveRTC = false; 97 | bool haveSDCard = false; 98 | bool fastLoggingEnabled = true; 99 | bool fastLoggingActive = false; 100 | uint8_t SDIObuffer[SDIO_BUFFER_SIZE]; 101 | uint16_t indexSDIObuffer = 0; 102 | uint16_t blockCountSD = 0; 103 | File dataFile; 104 | int startLogAttempt = 0; 105 | 106 | bool createNextSDFile() 107 | { 108 | char filename[50]; 109 | 110 | uint32_t nextFileIndex = deleteOldest(RESERVED_SD_SPACE); 111 | 112 | if(haveRTC) 113 | nextFileIndex = 0; //have a date so restart index from 0 (still needed in case serial stream fails to start) 114 | 115 | do 116 | { 117 | if(haveRTC) 118 | snprintf(filename, 50, "/%d-%02d-%02d-%02d-%02d-%02d_%d.bin", int_rtc.getYear(), int_rtc.getMonth(), int_rtc.getDay(), int_rtc.getHour(), int_rtc.getMinute(), int_rtc.getSecond(), nextFileIndex++); 119 | else 120 | snprintf(filename, 50, "/%010d.bin", nextFileIndex++); 121 | } 122 | while(SD_MMC.exists(filename)); 123 | 124 | dataFile = SD_MMC.open(filename, FILE_WRITE); 125 | if (dataFile) 126 | { 127 | dataFile.flush(); //make sure FAT updated for debugging purposes 128 | DBG_OUTPUT_PORT.println("Created file: " + String(filename)); 129 | return true; 130 | } 131 | else 132 | return false; 133 | } 134 | 135 | uint32_t deleteOldest(uint64_t spaceRequired) 136 | { 137 | time_t oldestTime = 0; 138 | File root, file; 139 | String oldestFileName; 140 | uint64_t spaceRem; 141 | time_t t; 142 | uint32_t nextIndex = 0; 143 | uint32_t fileCount = 0; 144 | 145 | spaceRem = SD_MMC.totalBytes() - SD_MMC.usedBytes(); 146 | 147 | DBG_OUTPUT_PORT.println("Space Required = " + formatBytes(spaceRequired)); 148 | DBG_OUTPUT_PORT.println("Space Remaining = " + formatBytes(spaceRem)); 149 | 150 | do 151 | { 152 | root = SD_MMC.open("/"); 153 | 154 | oldestTime = 0; 155 | fileCount = 0; 156 | while(file = root.openNextFile()) 157 | { 158 | if(haveRTC) 159 | t = file.getLastWrite(); 160 | else 161 | { 162 | String fname = file.name(); 163 | fname.remove(0,1); //lose starting / 164 | t = fname.toInt()+1; //make sure 0 special case isnt used 165 | if(t > nextIndex) 166 | nextIndex = t; 167 | } 168 | if(!file.isDirectory()) 169 | { 170 | fileCount++; 171 | if((oldestTime==0) || (t= MAX_SD_FILES)) 183 | { 184 | if(oldestFileName.length() > 0) 185 | { 186 | 187 | if(SD_MMC.remove(oldestFileName)) 188 | DBG_OUTPUT_PORT.println("Deleted file: " + oldestFileName); 189 | else 190 | DBG_OUTPUT_PORT.println("Couldn't delete: " + oldestFileName); 191 | } 192 | else 193 | { 194 | DBG_OUTPUT_PORT.println("No files found, can't free space"); 195 | break;//no files so can do no more 196 | } 197 | } 198 | 199 | spaceRem = SD_MMC.totalBytes() - SD_MMC.usedBytes(); 200 | } while((spaceRem < spaceRequired) || (fileCount >= MAX_SD_FILES)); 201 | 202 | 203 | return(nextIndex); 204 | } 205 | 206 | //format bytes 207 | String formatBytes(uint64_t bytes){ 208 | if (bytes < 1024){ 209 | return String(bytes)+"B"; 210 | } else if(bytes < (1024 * 1024)){ 211 | return String(bytes/1024.0)+"KB"; 212 | } else if(bytes < (1024 * 1024 * 1024)){ 213 | return String(bytes/1024.0/1024.0)+"MB"; 214 | } else { 215 | return String(bytes/1024.0/1024.0/1024.0)+"GB"; 216 | } 217 | } 218 | 219 | String getContentType(String filename){ 220 | if(server.hasArg("download")) return "application/octet-stream"; 221 | else if(filename.endsWith(".bin")) return "application/octet-stream"; 222 | else if(filename.endsWith(".htm")) return "text/html"; 223 | else if(filename.endsWith(".html")) return "text/html"; 224 | else if(filename.endsWith(".css")) return "text/css"; 225 | else if(filename.endsWith(".js")) return "application/javascript"; 226 | else if(filename.endsWith(".png")) return "image/png"; 227 | else if(filename.endsWith(".gif")) return "image/gif"; 228 | else if(filename.endsWith(".jpg")) return "image/jpeg"; 229 | else if(filename.endsWith(".ico")) return "image/x-icon"; 230 | else if(filename.endsWith(".xml")) return "text/xml"; 231 | else if(filename.endsWith(".pdf")) return "application/x-pdf"; 232 | else if(filename.endsWith(".zip")) return "application/x-zip"; 233 | else if(filename.endsWith(".gz")) return "application/x-gzip"; 234 | return "text/plain"; 235 | } 236 | 237 | bool handleFileRead(String path){ 238 | //DBG_OUTPUT_PORT.println("handleFileRead: " + path); 239 | if(path.endsWith("/")) path += "index.html"; 240 | String contentType = getContentType(path); 241 | String pathWithGz = path + ".gz"; 242 | if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){ 243 | if(SPIFFS.exists(pathWithGz)) 244 | path += ".gz"; 245 | File file = SPIFFS.open(path, "r"); 246 | size_t sent = server.streamFile(file, contentType); 247 | file.close(); 248 | return true; 249 | } 250 | //try download from the sdcard 251 | if (haveSDCard) { 252 | DBG_OUTPUT_PORT.print("handleFileRead Trying SD Card: "); 253 | DBG_OUTPUT_PORT.println(path); 254 | DBG_OUTPUT_PORT.print("SD_MMC.exists: "); 255 | DBG_OUTPUT_PORT.println(SD_MMC.exists( path)); 256 | 257 | if (SD_MMC.exists(path)) { 258 | File file = SD_MMC.open(path, "r"); 259 | size_t sent = server.streamFile(file, contentType); 260 | file.close(); 261 | return true; 262 | } 263 | } 264 | return false; 265 | } 266 | 267 | void handleFileUpload(){ 268 | if(server.uri() != "/edit") return; 269 | HTTPUpload& upload = server.upload(); 270 | if(upload.status == UPLOAD_FILE_START){ 271 | String filename = upload.filename; 272 | if(!filename.startsWith("/")) filename = "/"+filename; 273 | //DBG_OUTPUT_PORT.print("handleFileUpload Name: "); DBG_OUTPUT_PORT.println(filename); 274 | fsUploadFile = SPIFFS.open(filename, "w"); 275 | filename = String(); 276 | } else if(upload.status == UPLOAD_FILE_WRITE){ 277 | //DBG_OUTPUT_PORT.print("handleFileUpload Data: "); DBG_OUTPUT_PORT.println(upload.currentSize); 278 | if(fsUploadFile) 279 | fsUploadFile.write(upload.buf, upload.currentSize); 280 | } else if(upload.status == UPLOAD_FILE_END){ 281 | if(fsUploadFile) 282 | fsUploadFile.close(); 283 | //DBG_OUTPUT_PORT.print("handleFileUpload Size: "); DBG_OUTPUT_PORT.println(upload.totalSize); 284 | } 285 | } 286 | 287 | void handleFileDelete(){ 288 | if(server.args() == 0) return server.send(500, "text/plain", "BAD ARGS"); 289 | String path = server.arg(0); 290 | //DBG_OUTPUT_PORT.println("handleFileDelete: " + path); 291 | if(path == "/") 292 | return server.send(500, "text/plain", "BAD PATH"); 293 | if(!SPIFFS.exists(path)) 294 | return server.send(404, "text/plain", "FileNotFound"); 295 | SPIFFS.remove(path); 296 | server.send(200, "text/plain", ""); 297 | path = String(); 298 | } 299 | 300 | void handleFileCreate(){ 301 | if(server.args() == 0) 302 | return server.send(500, "text/plain", "BAD ARGS"); 303 | String path = server.arg(0); 304 | //DBG_OUTPUT_PORT.println("handleFileCreate: " + path); 305 | if(path == "/") 306 | return server.send(500, "text/plain", "BAD PATH"); 307 | if(SPIFFS.exists(path)) 308 | return server.send(500, "text/plain", "FILE EXISTS"); 309 | File file = SPIFFS.open(path, "w"); 310 | if(file) 311 | file.close(); 312 | else 313 | return server.send(500, "text/plain", "CREATE FAILED"); 314 | server.send(200, "text/plain", ""); 315 | path = String(); 316 | } 317 | 318 | void handleRTCNow() { 319 | String output = "{ \"now\":\""; 320 | if (haveRTC) { 321 | DateTime t = ext_rtc.now(); 322 | output += t.timestamp(); 323 | } else { 324 | output += "NO RTC"; 325 | } 326 | output += "\"}"; 327 | server.send(200, "text/json", output); 328 | } 329 | 330 | void handleRTCSet() { 331 | 332 | if (server.hasArg("timestamp")) { 333 | String timestamp = server.arg("timestamp"); 334 | server.send(200, "text/json", "{\"result\":\"" + timestamp + "\"}"); 335 | DateTime now = DateTime(timestamp.toInt()); 336 | ext_rtc.adjust(now); 337 | int_rtc.setTime(now.unixtime()); 338 | handleRTCNow(); 339 | } else { 340 | server.send(500, "text/json", "{\"result\":\"timestamp missing\"}"); 341 | 342 | } 343 | } 344 | void handleSdCardDeleteAll() { 345 | if (haveSDCard) { 346 | File root, file; 347 | if (haveSDCard) { 348 | root = SD_MMC.open("/"); 349 | while(file = root.openNextFile()) 350 | { 351 | String filename = file.name(); 352 | if(SD_MMC.remove("/" + filename)) 353 | DBG_OUTPUT_PORT.println("Deleted file: " + filename); 354 | else 355 | DBG_OUTPUT_PORT.println("Couldn't delete: " + filename); 356 | } 357 | } 358 | } 359 | 360 | server.send(200, "text/json", "{\"result\": \"done\"}"); 361 | 362 | } 363 | void handleSdCardList() { 364 | 365 | if (!haveSDCard) { 366 | server.send(200, "text/json", "{\"error\": \"No SD Card\"}"); 367 | return; 368 | } 369 | File root = SD_MMC.open("/"); 370 | if(!root){ 371 | server.send(200, "text/json", "{\"error\": \"Failed to open directory\"}"); 372 | return; 373 | } 374 | if(!root.isDirectory()){ 375 | server.send(200, "text/json", "{\"error\": \"Root is not a directory\"}"); 376 | return; 377 | } 378 | File sdFile = root.openNextFile(); 379 | String output = "["; 380 | int count = 0; 381 | while(sdFile && count < 200){ 382 | if (output != "[") output += ','; 383 | output += "\""; 384 | output += String(sdFile.name()); 385 | output += "\""; 386 | sdFile = root.openNextFile(); 387 | 388 | count++; 389 | } 390 | output += "]"; 391 | server.send(200, "text/json", output); 392 | return; 393 | } 394 | 395 | void handleFileList() { 396 | String path = "/"; 397 | if(server.hasArg("dir")) 398 | String path = server.arg("dir"); 399 | //DBG_OUTPUT_PORT.println("handleFileList: " + path); 400 | File root = SPIFFS.open(path); 401 | String output = "["; 402 | 403 | if(!root){ 404 | //DBG_OUTPUT_PORT.print("- failed to open directory"); 405 | return; 406 | } 407 | 408 | File file = root.openNextFile(); 409 | while(file){ 410 | if (output != "[") output += ','; 411 | bool isDir = false; 412 | output += "{\"type\":\""; 413 | output += file.isDirectory()?"dir":"file"; 414 | output += "\",\"name\":\""; 415 | output += String(file.name()); 416 | output += "\"}"; 417 | file = root.openNextFile(); 418 | } 419 | 420 | output += "]"; 421 | server.send(200, "text/json", output); 422 | } 423 | 424 | // static void sendCommand(String cmd) 425 | // { 426 | // DBG_OUTPUT_PORT.println("Sending '" + cmd + "' to inverter"); 427 | // Inverter.print("\n"); 428 | // delay(1); 429 | // while(Inverter.available()) 430 | // Inverter.read(); //flush all previous output 431 | // Inverter.print(cmd); 432 | // Inverter.print("\n"); 433 | // Inverter.readStringUntil('\n'); //consume echo 434 | // } 435 | 436 | void uart_readUntill(char val) 437 | { 438 | int retVal; 439 | do 440 | { 441 | retVal = uart_read_bytes(INVERTER_PORT, uartMessBuff, 1, UART_TIMEOUT); 442 | } 443 | while((retVal>0) && (uartMessBuff[0] != val)); 444 | } 445 | 446 | bool uart_readStartsWith(const char *val) 447 | { 448 | bool retVal = false; 449 | int rxBytes = uart_read_bytes(INVERTER_PORT, uartMessBuff, strnlen(val,UART_MESSBUF_SIZE), UART_TIMEOUT); 450 | if(rxBytes >= strnlen(val,UART_MESSBUF_SIZE)) 451 | { 452 | if(strncmp(val, uartMessBuff, strnlen(val,UART_MESSBUF_SIZE))==0) 453 | retVal = true; 454 | uartMessBuff[rxBytes] = 0; 455 | DBG_OUTPUT_PORT.println(uartMessBuff); 456 | } 457 | return retVal; 458 | } 459 | 460 | 461 | 462 | static void sendCommand(String cmd) 463 | { 464 | DBG_OUTPUT_PORT.println("Sending '" + cmd + "' to inverter"); 465 | //Inverter.print("\n"); 466 | uart_write_bytes(INVERTER_PORT, "\n", 1); 467 | delay(1); 468 | //while(Inverter.available()) 469 | // Inverter.read(); //flush all previous output 470 | uart_flush(INVERTER_PORT); 471 | //Inverter.print(cmd); 472 | uart_write_bytes(INVERTER_PORT, cmd.c_str(), cmd.length()); 473 | //Inverter.print("\n"); 474 | uart_write_bytes(INVERTER_PORT, "\n", 1); 475 | //Inverter.readStringUntil('\n'); //consume echo 476 | uart_readUntill('\n'); 477 | } 478 | 479 | static void handleCommand() { 480 | const int cmdBufSize = 128; 481 | if(!server.hasArg("cmd")) {server.send(500, "text/plain", "BAD ARGS"); return;} 482 | 483 | String cmd = server.arg("cmd").substring(0, cmdBufSize); 484 | int repeat = 0; 485 | char buffer[255]; 486 | size_t len = 0; 487 | String output; 488 | 489 | if (server.hasArg("repeat")) 490 | repeat = server.arg("repeat").toInt(); 491 | 492 | if (!fastUart && fastUartAvailable) 493 | { 494 | sendCommand("fastuart"); 495 | uart_set_baudrate(INVERTER_PORT, 921600); 496 | fastUart = true; 497 | } 498 | 499 | sendCommand(cmd); 500 | do { 501 | memset(buffer,0,sizeof(buffer)); 502 | //len = Inverter.readBytes(buffer, sizeof(buffer) - 1); 503 | len = uart_read_bytes(INVERTER_PORT, buffer, sizeof(buffer), UART_TIMEOUT); 504 | if(len > 0) output.concat(buffer, len);// += buffer; 505 | 506 | if (repeat) 507 | { 508 | repeat--; 509 | //Inverter.print("!"); 510 | uart_write_bytes(INVERTER_PORT, "!", 1); 511 | //Inverter.readBytes(buffer, 1); //consume "!" 512 | uart_read_bytes(INVERTER_PORT, buffer, 1, UART_TIMEOUT); 513 | } 514 | } while (len > 0); 515 | DBG_OUTPUT_PORT.println(output); 516 | server.sendHeader("Access-Control-Allow-Origin","*"); 517 | server.send(200, "text/json", output); 518 | } 519 | 520 | static uint32_t crc32_word(uint32_t Crc, uint32_t Data) 521 | { 522 | int i; 523 | 524 | Crc = Crc ^ Data; 525 | 526 | for(i=0; i<32; i++) 527 | if (Crc & 0x80000000) 528 | Crc = (Crc << 1) ^ 0x04C11DB7; // Polynomial used in STM32 529 | else 530 | Crc = (Crc << 1); 531 | 532 | return(Crc); 533 | } 534 | 535 | static uint32_t crc32(uint32_t* data, uint32_t len, uint32_t crc) 536 | { 537 | for (uint32_t i = 0; i < len; i++) 538 | crc = crc32_word(crc, data[i]); 539 | return crc; 540 | } 541 | 542 | 543 | static void handleUpdate() 544 | { 545 | if(!server.hasArg("step") || !server.hasArg("file")) {server.send(500, "text/plain", "BAD ARGS"); return;} 546 | size_t PAGE_SIZE_BYTES = 1024; 547 | int step = server.arg("step").toInt(); 548 | File file = SPIFFS.open(server.arg("file"), "r"); 549 | uint8_t pages = (file.size() + PAGE_SIZE_BYTES - 1) / PAGE_SIZE_BYTES; 550 | String message; 551 | 552 | if (server.hasArg("pagesize")) 553 | { 554 | PAGE_SIZE_BYTES = server.arg("pagesize").toInt(); 555 | } 556 | 557 | if (step == -1) 558 | { 559 | //int c; 560 | char c; 561 | sendCommand("reset"); 562 | 563 | if (fastUart) 564 | { 565 | //Inverter.begin(115200, SERIAL_8N1, INVERTER_RX, INVERTER_TX); 566 | //Inverter.updateBaudRate(115200); 567 | uart_set_baudrate(INVERTER_PORT, 115200); 568 | fastUart = false; 569 | fastUartAvailable = true; //retry after reboot 570 | } 571 | do { 572 | //c = Inverter.read(); 573 | uart_read_bytes(INVERTER_PORT, &c, 1, UART_TIMEOUT); 574 | } while (c != 'S' && c != '2'); 575 | 576 | if (c == '2') //version 2 bootloader 577 | { 578 | //Inverter.write(0xAA); //Send magic 579 | c = 0xAA; 580 | uart_write_bytes(INVERTER_PORT, &c, 1); 581 | //while (Inverter.read() != 'P'); 582 | do { 583 | uart_read_bytes(INVERTER_PORT, &c, 1, UART_TIMEOUT); 584 | } while (c != 'S'); 585 | } 586 | 587 | //Inverter.write(pages); 588 | uart_write_bytes(INVERTER_PORT, &pages, 1); 589 | //while (Inverter.read() != 'P'); 590 | do { 591 | uart_read_bytes(INVERTER_PORT, &c, 1, UART_TIMEOUT); 592 | } while (c != 'P'); 593 | message = "reset"; 594 | } 595 | else 596 | { 597 | bool repeat = true; 598 | file.seek(step * PAGE_SIZE_BYTES); 599 | char buffer[PAGE_SIZE_BYTES]; 600 | size_t bytesRead = file.readBytes(buffer, sizeof(buffer)); 601 | 602 | while (bytesRead < PAGE_SIZE_BYTES) 603 | buffer[bytesRead++] = 0xff; 604 | 605 | uint32_t crc = crc32((uint32_t*)buffer, PAGE_SIZE_BYTES / 4, 0xffffffff); 606 | 607 | while (repeat) 608 | { 609 | //Inverter.write(buffer, sizeof(buffer)); 610 | uart_write_bytes(INVERTER_PORT, buffer, sizeof(buffer)); 611 | //while (!Inverter.available()); 612 | char res;// = Inverter.read(); 613 | while(uart_read_bytes(INVERTER_PORT, &res, 1, UART_TIMEOUT)<=0); 614 | 615 | if ('C' == res) { 616 | //Inverter.write((char*)&crc, sizeof(uint32_t)); 617 | uart_write_bytes(INVERTER_PORT, (char*)&crc, sizeof(uint32_t)); 618 | //while (!Inverter.available()); 619 | //res = Inverter.read(); 620 | while(uart_read_bytes(INVERTER_PORT, &res, 1, UART_TIMEOUT)<=0); 621 | } 622 | 623 | switch (res) { 624 | case 'D': 625 | message = "Update Done"; 626 | repeat = false; 627 | fastUartAvailable = true; 628 | break; 629 | case 'E': 630 | //while (Inverter.read() != 'T'); 631 | do { 632 | uart_read_bytes(INVERTER_PORT, uartMessBuff, 1, UART_TIMEOUT); 633 | } while (uartMessBuff[0] != 'T'); 634 | break; 635 | case 'P': 636 | message = "Page write success"; 637 | repeat = false; 638 | break; 639 | default: 640 | case 'T': 641 | break; 642 | } 643 | } 644 | } 645 | server.send(200, "text/json", "{ \"message\": \"" + message + "\", \"pages\": " + pages + " }"); 646 | file.close(); 647 | } 648 | 649 | static void handleWifi() 650 | { 651 | bool updated = true; 652 | if(server.hasArg("apSSID") && server.hasArg("apPW")) 653 | { 654 | WiFi.softAP(server.arg("apSSID").c_str(), server.arg("apPW").c_str()); 655 | } 656 | else if(server.hasArg("staSSID") && server.hasArg("staPW")) 657 | { 658 | WiFi.mode(WIFI_AP_STA); 659 | WiFi.begin(server.arg("staSSID").c_str(), server.arg("staPW").c_str()); 660 | } 661 | else 662 | { 663 | File file = SPIFFS.open("/wifi.html", "r"); 664 | String html = file.readString(); 665 | file.close(); 666 | html.replace("%staSSID%", WiFi.SSID()); 667 | html.replace("%apSSID%", WiFi.softAPSSID()); 668 | html.replace("%staIP%", WiFi.localIP().toString()); 669 | server.send(200, "text/html", html); 670 | updated = false; 671 | } 672 | 673 | if (updated) 674 | { 675 | File file = SPIFFS.open("/wifi-updated.html", "r"); 676 | size_t sent = server.streamFile(file, getContentType("wifi-updated.html")); 677 | file.close(); 678 | } 679 | } 680 | 681 | static void handleBaud() 682 | { 683 | if (fastUart) 684 | server.send(200, "text/html", "fastUart on"); 685 | else 686 | server.send(200, "text/html", "fastUart off"); 687 | } 688 | 689 | void staCheck(){ 690 | sta_tick.detach(); 691 | if(!(uint32_t)WiFi.localIP()){ 692 | WiFi.mode(WIFI_AP); //disable station mode 693 | } 694 | } 695 | 696 | void setup(void){ 697 | DBG_OUTPUT_PORT.begin(115200); 698 | //Inverter.setRxBufferSize(50000); 699 | //Inverter.begin(115200, SERIAL_8N1, INVERTER_RX, INVERTER_TX); 700 | //Need to use low level Espressif IDF API instead of Serial to get high enough data rates 701 | uart_config_t uart_config = { 702 | .baud_rate = 115200, 703 | .data_bits = UART_DATA_8_BITS, 704 | .parity = UART_PARITY_DISABLE, 705 | .stop_bits = UART_STOP_BITS_1, 706 | .flow_ctrl = UART_HW_FLOWCTRL_DISABLE}; 707 | 708 | uart_param_config(INVERTER_PORT, &uart_config); 709 | uart_set_pin(INVERTER_PORT, INVERTER_TX, INVERTER_RX, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); 710 | uart_driver_install(INVERTER_PORT, SDIO_BUFFER_SIZE * 3, 0, 0, NULL, 0); //x3 allows twice card write size to buffer while writes 711 | delay(100); 712 | 713 | 714 | //check for external RTC and if present use to initialise on-chip RTC 715 | if (ext_rtc.begin()) 716 | { 717 | haveRTC = true; 718 | DBG_OUTPUT_PORT.println("External RTC found"); 719 | if (! ext_rtc.initialized() || ext_rtc.lostPower()) 720 | { 721 | DBG_OUTPUT_PORT.println("RTC is NOT initialized, setting to build time"); 722 | ext_rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); 723 | } 724 | 725 | ext_rtc.start(); 726 | DateTime now = ext_rtc.now(); 727 | int_rtc.setTime(now.unixtime()); 728 | } 729 | else 730 | DBG_OUTPUT_PORT.println("No RTC found, defaulting to sequential file names"); 731 | 732 | //initialise SD card in SDIO mode 733 | //if (SD_MMC.begin("/sdcard", true, false, 40000, 5U)) { 734 | if (SD_MMC.begin()) { 735 | DBG_OUTPUT_PORT.println("Started SD_MMC"); 736 | haveSDCard = true; 737 | } 738 | else 739 | DBG_OUTPUT_PORT.println("Couldn't start SD_MMC"); 740 | 741 | //Start SPI Flash file system 742 | SPIFFS.begin(); 743 | 744 | //WIFI INIT 745 | #ifdef WIFI_IS_OFF_AT_BOOT 746 | enableWiFiAtBootTime(); 747 | #endif 748 | WiFi.mode(WIFI_AP_STA); 749 | //WiFi.setPhyMode(WIFI_PHY_MODE_11B); 750 | WiFi.setSleep(false); 751 | WiFi.setTxPower(WIFI_POWER_19_5dBm);//25); //dbm 752 | WiFi.begin(); 753 | sta_tick.attach(10, staCheck); 754 | 755 | MDNS.begin(host); 756 | 757 | updater.setup(&server); 758 | 759 | //SERVER INIT 760 | ArduinoOTA.setHostname(host); 761 | ArduinoOTA.begin(); 762 | //list directory 763 | server.on("/list", HTTP_GET, handleFileList); 764 | 765 | server.on("/rtc/now", HTTP_GET, handleRTCNow); 766 | server.on("/rtc/set", HTTP_POST, handleRTCSet); 767 | server.on("/sdcard/list", HTTP_GET, handleSdCardList); 768 | server.on("/sdcard/deleteAll", HTTP_GET, handleSdCardDeleteAll); 769 | 770 | //load editor 771 | server.on("/edit", HTTP_GET, [](){ 772 | if(!handleFileRead("/edit.htm")) server.send(404, "text/plain", "FileNotFound"); 773 | }); 774 | //create file 775 | server.on("/edit", HTTP_PUT, handleFileCreate); 776 | //delete file 777 | server.on("/edit", HTTP_DELETE, handleFileDelete); 778 | //first callback is called after the request has ended with all parsed arguments 779 | //second callback handles file uploads at that location 780 | server.on("/edit", HTTP_POST, [](){ server.send(200, "text/plain", ""); }, handleFileUpload); 781 | 782 | server.on("/wifi", handleWifi); 783 | server.on("/cmd", handleCommand); 784 | server.on("/fwupdate", handleUpdate); 785 | server.on("/baud", handleBaud); 786 | server.on("/version", [](){ server.send(200, "text/plain", "1.1.R"); }); 787 | 788 | //called when the url is not defined here 789 | //use it to load content from SPIFFS 790 | server.onNotFound([](){ 791 | if(!handleFileRead(server.uri())) 792 | { 793 | server.sendHeader("Refresh", "6; url=/update"); 794 | server.send(404, "text/plain", "FileNotFound"); 795 | } 796 | }); 797 | 798 | server.begin(); 799 | server.client().setNoDelay(1); 800 | 801 | MDNS.addService("http", "tcp", 80); 802 | } 803 | 804 | void binaryLoggingStart() 805 | { 806 | if(createNextSDFile()) 807 | { 808 | sendCommand(""); //flush out buffer in case just had power up 809 | delay(10); 810 | sendCommand("binarylogging 1"); //send start logging command to inverter 811 | delayMicroseconds(200); 812 | if (uart_readStartsWith("OK")) 813 | { 814 | uart_set_baudrate(INVERTER_PORT, 2250000); 815 | fastLoggingActive = true; 816 | DBG_OUTPUT_PORT.println("Binary logging started"); 817 | } 818 | else //no response - in case it did actually switch but we missed response send the turn off command 819 | { 820 | dataFile.close(); 821 | uart_set_baudrate(INVERTER_PORT, 2250000); 822 | uart_write_bytes(INVERTER_PORT, "\n", 1); 823 | delay(1); 824 | uart_write_bytes(INVERTER_PORT, "binarylogging 0", strnlen("binarylogging 0", UART_MESSBUF_SIZE)); 825 | uart_write_bytes(INVERTER_PORT, "\n", 1); 826 | uart_wait_tx_done(INVERTER_PORT, UART_TIMEOUT); 827 | uart_set_baudrate(INVERTER_PORT, 115200); 828 | } 829 | delay(10); 830 | uart_flush(INVERTER_PORT); 831 | } 832 | } 833 | 834 | void binaryLoggingStop() 835 | { 836 | uart_write_bytes(INVERTER_PORT, "\n", 1); 837 | delay(1); 838 | uart_write_bytes(INVERTER_PORT, "binarylogging 0", strnlen("binarylogging 0", UART_MESSBUF_SIZE)); 839 | uart_write_bytes(INVERTER_PORT, "\n", 1); 840 | uart_wait_tx_done(INVERTER_PORT, UART_TIMEOUT); 841 | uart_set_baudrate(INVERTER_PORT, 115200); 842 | delay(100); 843 | uart_flush(INVERTER_PORT); 844 | //data should now have stopped so send command again and check response 845 | sendCommand("binarylogging 0"); 846 | if (uart_readStartsWith("OK")) 847 | { 848 | uart_set_baudrate(INVERTER_PORT, 115200); 849 | fastUart = false; 850 | fastLoggingActive = false; 851 | dataFile.flush(); //make sure up to date 852 | dataFile.close(); 853 | DBG_OUTPUT_PORT.println("Binary logging terminated"); 854 | } 855 | else 856 | { //assume still logging so try again next time round 857 | uart_set_baudrate(INVERTER_PORT, 2250000); 858 | } 859 | delay(10); 860 | uart_flush(INVERTER_PORT); 861 | } 862 | 863 | 864 | void loop(void){ 865 | // note: ArduinoOTA.handle() calls MDNS.update(); 866 | server.handleClient(); 867 | ArduinoOTA.handle(); 868 | 869 | if((WiFi.softAPgetStationNum() > 0) || (WiFi.status() == WL_CONNECTED)) 870 | { //have connections so stop logging 871 | startLogAttempt=0; //restart log attempts when next disconnected 872 | if(fastLoggingActive) //was it active last pass 873 | binaryLoggingStop(); 874 | } 875 | else 876 | { //no connections so log 877 | if(fastLoggingActive) //already active, just carry on writing data 878 | { 879 | int spaceAvail = SDIO_BUFFER_SIZE - indexSDIObuffer; 880 | int bytesRead = uart_read_bytes(INVERTER_PORT, &SDIObuffer[indexSDIObuffer], spaceAvail, UART_TIMEOUT); 881 | if(bytesRead > 0) 882 | { 883 | indexSDIObuffer += bytesRead; 884 | if(indexSDIObuffer >= SDIO_BUFFER_SIZE) 885 | { 886 | dataFile.write(SDIObuffer, SDIO_BUFFER_SIZE); 887 | indexSDIObuffer = 0; 888 | blockCountSD++; 889 | if(blockCountSD >= FLUSH_WRITES) 890 | { 891 | blockCountSD = 0; 892 | dataFile.flush(); 893 | } 894 | } 895 | } 896 | } 897 | else //not active so start 898 | { 899 | if(haveSDCard && fastLoggingEnabled && (startLogAttempt < 3) && (millis() > LOG_DELAY_VAL)) 900 | { 901 | startLogAttempt++; 902 | binaryLoggingStart(); 903 | } 904 | } 905 | } 906 | } 907 | -------------------------------------------------------------------------------- /platformio-local-override.ini.example: -------------------------------------------------------------------------------- 1 | [env] 2 | board_build.flash_mode = qout 3 | upload_port = /dev/cu.usbserial-DGAJb113318 4 | upload_speed = 921600 5 | monitor_port = /dev/cu.usbserial-DGAJb113318 6 | monitor_speed = 115200 7 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | description = Web interface for Huebner inverter 13 | default_envs = release 14 | src_dir = . 15 | data_dir = data 16 | extra_configs = platformio-local-override.ini 17 | 18 | [common] 19 | monitor_speed = 115200 20 | 21 | [env] 22 | platform = espressif32 23 | framework = arduino 24 | platform_packages = platformio/tool-esptoolpy 25 | board = esp32dev 26 | board_build.filesystem = spiffs 27 | board_build.flash_mode = qout 28 | build_src_filter = +<*> -<.git/> -<.svn/> - 29 | upload_speed = 921600 30 | upload_flags = 31 | --after 32 | no_reset_stub 33 | 34 | [env:release] 35 | build_flags = 36 | ${env.build_flags} 37 | -D RELEASE 38 | build_type = release 39 | lib_deps = 40 | fbiego/ESP32Time@^2.0.0 41 | adafruit/RTClib@^2.1.1 42 | SPI 43 | 44 | [env:debug] 45 | build_flags = 46 | ${env.build_flags} 47 | -D DEBUG 48 | -DDEBUG_ESP_PORT=Serial 49 | -DDEBUG_ESP_CORE 50 | -DDEBUG_ESP_WIFI 51 | build_type = debug 52 | lib_deps = 53 | fbiego/ESP32Time@^2.0.0 54 | adafruit/RTClib@^2.1.1 55 | SPI 56 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -lt 1 ] || [ $# -gt 2 ]; then 4 | echo "This tool will send either one, or all data files to the web interface" 5 | echo "" 6 | echo "Syntax: $0 []" 7 | exit 255 8 | fi 9 | 10 | IP="$1" 11 | echo "Uploading to $IP" 12 | 13 | if [ $# -gt 1 ]; then 14 | files="$2" 15 | else 16 | files="./data/*" 17 | fi 18 | 19 | for file in $files; do 20 | echo "Sending: $file" 21 | # curl -v --trace-ascii - -F 'data=@"'"$file"'";filename="'"$(basename "$file")"'"' http://"$IP"/edit 22 | curl -F 'data=@"'"$file"'";filename="'"$(basename "$file")"'"' http://"$IP"/edit 23 | done 24 | --------------------------------------------------------------------------------