8 Channel IoT Triac Controller


With this low-cost IoT device it is easy to control a standard wall outlet devices with your internet browser. No additional software installation required. Working with single 5 volts DC power supply and controlling eight outputs with voltages between 1.7-600VAC. The power can be taken from the USB port or external power supply. There is a screw terminal for that.

Video https://youtu.be/RsZQNslSJ4I

Outputs uses a VO2223 power phototriacs. Designed to drive low power (<0.6A per channel) AC loads. Can be extended with external AC relay or power triac module. The outputs have screw terminals.

Ethernet connection is established using ENC28J60 chip and HanRun HR911105A ethernet connector because it includes transformers and filter elements needed for proper working. This simplifies board design.

The heart of the device is AtMega2560 chip. This makes it to be Arduino™ Mega compatible system. On board standard ICSP connector makes it easy to upload bootloader with using Arduino™ as ISP or other AVR programmers.

Device uses FT232RL chip to communicate via USB. With bootloader installed can be programmed via USB using Arduino™ IDE or similar programs. The DTR pin is connected through capacitor to the AtMega2560 reset pin. So reset works when it is needed by software.

AtMega2560 is programmed so that ethernet connection supports automatic DHCP, so you do not have to enter the IP address manually.

It includes easy to use web interface to modify and save custom triac control programs. The web interface is stored in to the AtMega2560’s program memory and it is included in the program code. It is programmed using HTML, CSS and javascript. It uses AJAX and HTTP Get request to communicate with the AtMega2560 which acts as a web server. The web interface is available with computers connected to the same LAN network.

Custom triac control programs is saved in to EEPROM memory. It automatically begins on startup and loops forevermore regardless of the ethernet connection. There is also individual manual override possibility with external pulse from AC side on each channel.

The device supports additional cards via the P2 connector. It is also used as an ICSP connector (SPI line) but it contains two extra pins that are serial port 2 RX and TX line. They can also be used as GPIO or as an SS signal in the SPI line. Additional cards are not yet designed but it can be anything. For example real time clock or digital input port expander or RS485 interface.

Usage examples

  • amusement park lighting systems
  • carneval lights
  • building face lights
  • household lights and appliances


The background of the menu bar is green if the connection is ok and red if there is no connection. It is possible to upload and download user program to/from local hard drive by using ‘Open list’ and ‘Save list’ buttons. It is in JSON format.

The control panel has line numbering and lines can be added and deleted with related buttons. Eight channels are individually programmed on each line by clicking it on or off. If it is on ‘on’-state then it’s background color is green otherwise it’s white. The delay in each line is given in milliseconds and it can be any between zero and near infinity (32-bit number from 0 to 4,294,967,295).

The program is automatically saved once per minute if changes are made. Program runs continuously and is editable in real time.

Development stage

One prototype has been made so far with double sided printed circuit board and with through hole components. Thoroughly tested and proven to work.

Further development ideas includes DIN rail mounting option, power input protective diode and controlling outputs over the USB connection. The top idea for an additional card is the RS485 interface.

Bill Of Materials

  • R1, 10k, 1*0.01
  • R2, R3, R6, R7, R10, R11, R14, R15, R23-R28, 470 ohm, 14*0.01
  • R4, R5, R8, R9, R12, R13, R16, R17, 180 ohm (optional), 8*0.01
  • R18, 2.7k, 1*0.01
  • R19-R22, 50 ohm, 4*0.01

Subtotal $0.28

  • C1, C2, C7, C10, 18pF, 4*0.01
  • C3, C4-C6, C9, C11, C12, C14, C19-C22, 100nF, 12*0.01
  • C16-C18, 10nF, 3*0.01
  • C8, C13, C15, 10µF tantalum 3*0.12

Subtotal $0.55

  • L1, L2, Ferrite bead with 4-14 turns (10-100µH) of thin enameled wire, 2*0.08
  • D17-D20, 5mm LED, 4*0.03
  • Y1, 16MHz, 0.1
  • Y2, 25MHz, 0.1

Subtotal $0.39

  • IC1, ATMEGA2560-A, 5.75
  • U2-U9, VO2223A, 8*0.79
  • U10, LM1117-3.3, 0.4
  • U11, ENC28J60-I/SO, 5.03
  • U12, FT232RL, 1.88
  • U13, MC74VHC1GT125, 2.08

Subtotal $21.46

  • P1, USB mini connector, 1.38
  • P2, pin header 2X4, 0.021
  • J1, HANRUN HR911105A, 1.76
  • J2-J9, screw terminal 1X3, 8*0.20
  • J10, screw terminal 1X2, 0.15
  • 8-pin DIP socket, 8*0.03
  • PCB, 8.88

Subtotal $14.031
Total $36.711


Reference schematics for how to make connection between the ENC28J60 and HR911105A.

Direct link from above

ENC28J60 datasheet

HR911105A datasheet

Schematics and board layout

Here is the schematics. Click to enlarge.


The project requires special soldering skills. Microcontroller AtMega2560 is 100 pin TQFP package. USB-to-serial interface FT232RL is 28-pin SSOP package. And ENC28J60 is 28 pin SOIC package. Board also contains MC74VHC1GT125 buffer and level shifter ic in SOT-353 package. Hot air soldering gun is recommended tool and use of solder paste.

All trough hole and SMD components are hand-soldered on the PCB. After the SMD components are soldered, the remaining components are easy to solder on the circuit board. VO2223A is sensitive to heat so using a ic socket is recommended.


My device works when simple interface is needed with little effort. If more current driving capacity is needed then user can use AC relays with DIN rail mounting. For example in the amusement park lighting.

This product works on all the different mains voltages in Europe and rest of the world too. My product has eight outputs and internet connection as IoT means.

This product does not use bulky relays so is smaller than its competitors.

USB control is also an option on my device. It just depends on how it is programmed. I also have an option for additional cards which can be anything. For example RS485 connection.


All parts are open source.

The program is written in C++, HTML, Javascript and CSS languages. The code uses more than 41KB of program memory so it is too long to fit into AtMega328 chip therefore AtMega2560 chip has been used.

The bootloader must first be programmed using on board standard ICSP connector and ISP. I personally like to use Arduino™ as ISP and Arduino™ IDE. Be sure to choose the Arduino™ Mega from the Tools→Board menu. After installing the bootloader, the program can be uploaded using a normal on board USB-to-serial interface.

FT232RL also needs to be programmed. Programming is done with FT-PROG program found on the FTDIchip.com website. It only works in Windows. Simplest method is to clone other ready made device. I cloned memory content from Sparkfun FTDI Basic board.

ENC28J60 is configurable via SPI but default settings are fine. Full-duplex operation is selected with LEDB. Connection of the LED is automatically read on reset. When the pin sinks current to illuminate LED, the PHCON1.PDPXMD bit is set and the PHY defaults to full-duplex operation. In full-duplex mode, switch and station can send and receive simultaneously, and therefore it is completely collision-free. Connection speed is 100Mbit/s.

I’m using UIPEthernet library for ENC28J60. It is licensed under GNU General Public License version 3. Other libraries available are Arduino uIP by Norbert Truchsess (GPL3), EtherCard (GPL2), EtherSia (3-clause BSD), ETHER_28J60 (LGPLv2.1).

Program code.

/*  Ethernet Triac Board
 *    - Program ver. 1.0
 *    - Compatible with board ver. 1.1
 *  Eight channel digital triac control board with Ethernet and USB 
 *  connection. Drives 8 output with AC load. Ethernet connection 
 *  supports automatic DHCP. No need to manual enter IP address. 
 *  Arduino Mega based system with AtMega2560 chip.
 *  Easy to use web interface to modify and save custom triac control 
 *  programs. Program is saved in to EEPROM. It automatically begins on 
 *  startup and loops forevermore.
 *  Control Panel layout by Lari Varjonen
 *  Copyright (C) 2017-2018  Juha-Pekka Varjonen
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  GNU General Public License for more details.
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.

#include <UIPEthernet.h>
#include <EEPROM.h>

unsigned long LedMillis = 0; // delays
unsigned long PrgMillis = 0;
unsigned long SaveMillis = 0;

int pos = 0;
boolean changed = false; // save program to EEPROM

// MAC address
const uint8_t mac[6] PROGMEM = {
  0x80, 0xAA, 0xBB, 0xCC, 0xDE, 0x02

// Stores the HTTP request
String HTTP_req;

// stores current program
int rowTotal = 0;
char contrArr[30][8];
int delayArr[30];

// Initialize the Ethernet server library
EthernetServer server(80); // port 80

void setup() {
  pinMode(4, OUTPUT); // LED output

  pinMode(33, OUTPUT); // data outputs
  pinMode(34, OUTPUT);
  pinMode(35, OUTPUT);
  pinMode(36, OUTPUT);
  pinMode(37, OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(10, OUTPUT);
  // Open serial communication
  Serial.println(F("Ethernet Triac Board Copyright (C) 2017-2018  Juha-Pekka Varjonen"));
  Serial.println(F("This program comes with ABSOLUTELY NO WARRANTY; for details "
    "see license. This is free software, and you are welcome to redistribute it "
    "under certain conditions; see license for details."));

  // EEPROM initial content
//  String temp = "[[1,1,1,1,1,1,1,1,500],[0,0,0,0,0,0,0,0,500]]";
//  for (int i = 0; i < temp.length(); i++) {
//    EEPROM.write(i,temp.charAt(i));
//  }

  // Read saved program from EEPROM
  String temp;  
  boolean valid = false;
  for (int i = 0; i < EEPROM.length(); i++) {
    char n = EEPROM.read(i);
    char nn = EEPROM.read(i+1);
    if (n == '[' && nn == '[') valid = true;
    if (valid) temp += String(n);
    if (n == ']' && nn == ']') {
      valid = false;
      temp += String(nn);
      temp += String('\0');
  // Ethernet Chip Select
  pinMode(53, OUTPUT);
  digitalWrite(53, LOW);
  // start the Ethernet connection
  // Start listening for clients

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - LedMillis >= 100) {
    digitalWrite(4, !digitalRead(4));
    LedMillis = currentMillis;
  // Wait for a new client
  EthernetClient client = server.available();

  // Got client?
  if (client) {
    Serial.println(F("New client"));
    // Empty string
    HTTP_req = "";
    // An HTTP request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      // Client data available to read
      if (client.available()) {
        // Read one byte from client
        char c = client.read();
        // Save the HTTP request one char at a time
        HTTP_req += c;
        // Last line of client request is blank and ends with \n
        // Respond to client only after last line received        
        if (c == '\n' && currentLineIsBlank) {
          // GET receive program data
          if (HTTP_req.indexOf(F("?json=")) > -1) {
            int subStart = HTTP_req.indexOf(F("?json=")) + 6;
            int subEnd = HTTP_req.indexOf("HTTP") - 1;
            String input = HTTP_req.substring(subStart, subEnd);
            // Parse JSON string
            pos = 0;
            // Save to EEPROM
            changed = true;
            // start save counter from zero
            SaveMillis = currentMillis;
          // AJAX request current line
          if (HTTP_req.indexOf(F("line.txt")) > -1) {
            // Send current line number only
            client.println(F("HTTP/1.1 200 OK"));
            client.println(F("Content-Type: text/plain"));
            client.println(F("Connection: keep-alive"));
          // AJAX request program code
          else if (HTTP_req.indexOf(F("file.json")) > -1) {
            // Send program in JSON format
            String temp;
            temp = '[';
            for (int i = 0; i <= rowTotal; i++) {
              temp += '[';
              for (int ii = 0; ii < 8; ii++) {
                temp += contrArr[i][ii];
                temp += ',';
              temp += delayArr[i];
              temp += ']';
              if (i < rowTotal) temp += ',';
            temp += ']';
            client.println(F("HTTP/1.1 200 OK"));
            client.println(F("Content-Type: application/json"));
            client.println(F("Connection: keep-alive"));
          // HTTP request
          else {
            if (HTTP_req.indexOf(F("index.html")) > -1 || HTTP_req.indexOf(F("GET / ")) > -1) {
              // Send html page
              client.println(F("HTTP/1.1 200 OK"));
              client.println(F("Content-Type: text/html"));
              client.println(F("Connection: keep-alive"));
              client.println(F("<!DOCTYPE HTML><html><head><meta charset=\"UTF-8\"><title>8 Channel Triac Board - Control Panel</title><link rel=\"stylesheet\" type=\"text/css\" href=\"css/style.css\"></head><body><div class=\"top\"><div class=\"bar\"><div class=\"col\"><label for=\"file-upload\">Open list</label><input type=\"file\" name=\"open_button\" value=\"Open list\" id=\"file-upload\"/></div><div class=\"col\"><input type=\"button\" name=\"save_button\" value=\"Save list\"/></div><div class=\"col\"><input type=\"button\" name=\"remove_button\" value=\"Remove line\"/></div><div class=\"col\"><input type=\"button\" name=\"add_button\" value=\"Add line\"/></div></div></div><div class=\"background\"></div><div class=\"container\"><div class=\"row\" style=\"display:none\" id=\"1\"><div class=\"line_n\"><div class=\"arrow-right\"></div><div class=\"n\">1</div></div><input type=\"checkbox\" name=\"checkbox\"/><div class=\"checkbox_light\"></div><div class=\"led_background\"><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">1</div></div><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">2</div></div><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">3</div></div><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">4</div></div><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">5</div></div><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">6</div></div><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">7</div></div><div class=\"led\"><input type=\"checkbox\" name=\"led\" onchange=\"javascript: ledClick(this);\"/><div class=\"light\">8</div></div><input type=\"number\" min=\"0\" name=\"delay_t\" value=\"\" onchange=\"javascript: delayChange(this);\"/></div></div></div><script type=\"text/javascript\" src=\"js/java.js\" ></script></body></html>"));

            } else if (HTTP_req.indexOf(F("style.css")) > -1) {
              client.println(F("HTTP/1.1 200 OK"));
              client.println(F("Content-Type: text/css"));
              client.println(F("Connection: keep-alive"));
              client.println(F("body{margin:0;padding:0;background:#10162E;overflow-y:scroll;overflow-x:hidden}body,html{height:100%}@-moz-document url-prefix(){*::-moz-focus-inner,*::-moz-focus-outer,*:focus{border:none;outline:none;max-width:100%;border-radius:0;box-shadow:none;padding:0}}.top{position:fixed;z-index:10;width:100%;height:36px;background:#364168;display:block;margin:0;padding:0;top:0;left:0}.background,.container,.top .bar{width:269px}.top .bar{height:32px;margin:2px auto;display:table}.top .bar .col{display:table-cell;height:32px}input[type=file]{display:none !important}.top .bar input,label{width:100%;display:block;height:24px;border:none;outline:none;border-radius:0;box-shadow:none;margin:4px 0 0;padding:0;background:#fff;border-radius:6px;text-align:center;font-family:arial;font-size:14px;color:#7E7E7E;line-height:24px}.top .bar .col:nth-child(odd){padding:0 4px}.col:nth-child(1){padding:0 4px 0 0 !important}.top .bar input:hover,label:hover{background:#F8F8F8}.top .bar input:active{background:#F0F0F0;color:#000}.background{position:fixed;z-index:0;height:100%;margin:0 0 0 -134px;left:50%;background:#3E4769}.container{position:absolute;z-index:1;margin:36px 0 53px -134px;left:50%;padding:0;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.container .row,.container .row .led_background,.container .row .led_background .led,.container .row .led_background .led .light,.container .row .led_background .led input,.container .row input[name=checkbox],.container .row input[name=delay_t]{height:24px}.container .row .led_background .led,.container .row input[name=delay_t]{float:left}.container .row .led_background .led .light,.container .row .line_n .n,.container .row input[name=delay_t]{font-family:arial;font-size:10px;color:#848484;text-align:center}.container .row .led_background .led .light,.container .row input[name=delay_t]{line-height:24px}.container .row,.container .row .checkbox_light,.container .row .led_background,.container .row .line_n,.top{-o-transition:0.2s;-ms-transition:0.2s;-moz-transition:0.2s;-webkit-transition:0.2s;transition:0.2s}.container .row,.container .row .led_background{width:269px}.container .row{position:relative;background:#fff}.container .row:hover{background:#FBF7E1}.container .row:hover > .line_n{opacity:1}.container .row .line_n:hover{opacity:0.8}.container .row .line_n{height:16px;position:absolute;right:100%;margin-right:8px;opacity:0.8;margin-top:4px;background:#fff;border-radius:4px;padding:0 4px}.container .row .line_n .n{line-height:16px}.container .row .line_n .arrow-right{width:0;height:0;position:absolute;left:100%;margin-top:4px;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:4px solid #fff}.container .row input[name=delay_t]{border:none;outline:none;border-radius:0;box-shadow:none;margin:0;padding:0;width:68px;background:transparent;border-radius:0;line-height:30px;letter-spacing:1px}.container .row .led_background .led .light,.container .row .led_background .led input{position:absolute;top:0;left:0}.container .row .led_background .led,.container .row .led_background .led .light,.container .row .led_background .led input,.container .row input[name=checkbox]{width:24px}.container .row input[name=checkbox]{position:absolute;left:100%;margin:0}.container .row .led_background .led{position:relative;float:left;border-right:1px solid #F2F2F2;overflow:hidden}.container .row .led_background .led:last-child{border:none}.container .row .checkbox_light,.container .row .led_background .led .light,.container .row input[name=checkbox]{position:absolute}.container .row .checkbox_light{left:100%;width:12px;height:12px;margin:6px 0 0 6px;border-radius:6px}.container .row .led_background .led input,.container .row input[name=checkbox]{border:none;outline:none;box-shadow:none;margin:0;padding:0;vertical-align:middle;-webkit-appearance:none;cursor:pointer;z-index:1;opacity:0}.container .row .led_background .led input:checked + .light{color:#424242;background:#CEF6CE}.container .row input[name=checkbox]:hover + .checkbox_light{background:#3B0B0B}.container .row input[name=checkbox]:checked + .checkbox_light{background:red;background:-webkit-linear-gradient(left top, red, #610B0B);background:-o-linear-gradient(bottom right, red, #610B0B);background:-moz-linear-gradient(bottom right, red, #610B0B);background:linear-gradient(to bottom right, red, #610B0B)}.container .row input[name=checkbox]:checked ~ .led_background{background:#FFE1E1}"));
            } else if (HTTP_req.indexOf(F("java.js")) > -1) {
              String IPaddr;
              for (byte thisByte = 0; thisByte < 4; thisByte++) {
                // print the value of each byte of the IP address:
                IPaddr += String(Ethernet.localIP()[thisByte], DEC);
                if (thisByte < 3) IPaddr += ".";
              client.println(F("HTTP/1.1 200 OK"));
              client.println(F("Content-Type: text/javascript"));
              client.println(F("Connection: keep-alive"));
              client.print(F("var container,clone,data;var json_list=[];var button_click=false;function updateRemote(){var xhr=new XMLHttpRequest();xhr.open(\"GET\",\"http://"));
              client.print(F("/index.html?json=\"+JSON.stringify(json_list),true);xhr.timeout=2000;xhr.send(null)}function parseResult(){container.innerHTML=\"\";for(var i=0;i<json_list.length;i+=1){add_line(json_list[i][0],json_list[i][1],json_list[i][2],json_list[i][3],json_list[i][4],json_list[i][5],json_list[i][6],json_list[i][7],json_list[i][8],)};updateRemote()}function read_json_file(){var nocache=\"&nocache=\"+Math.random()*1000000;var xhr=new XMLHttpRequest();xhr.open(\"GET\",\"http://"));
              client.print(F("/file.json\"+nocache,true);xhr.timeout=2000;xhr.onreadystatechange=function(){if(this.readyState==4){if(this.status==200){if(this.responseText!=null){json_list=JSON.parse(this.responseText);parseResult()}}}};xhr.send(null)};function getCurrentLine(){var nocache=\"&nocache=\"+Math.random()*1000000;var xhr=new XMLHttpRequest();xhr.open(\"GET\",\"http://"));
              client.print(F("/line.txt\"+nocache,true);xhr.timeout=2000;var err=0;xhr.onreadystatechange=function(){if(this.readyState==4){if(this.status==200){if(this.responseText!=null){/*var pos=this.responseText;var obj=container.getElementsByClassName(\"row\");for(var i=0;i<obj.length;i+=1){obj[i].style.background=\"#fff\"}obj[pos].style.background=\"#FBF7E1\";*/setTimeout(\"getCurrentLine()\",10500);document.getElementsByClassName(\"top\")[0].title=\"online\";document.getElementsByClassName(\"top\")[0].style.background=\"#3bb151\"}else{err=1}}else{err=1}}; if(err==1){setTimeout(\"getCurrentLine()\",10500);document.getElementsByClassName(\"top\")[0].title=\"offline\";document.getElementsByClassName(\"top\")[0].style.background=\"#b13b3b\"}};xhr.send(null)}function readFile(event){json_list=JSON.parse(event.target.result);parseResult()}function open_button_change(){var file=document.getElementsByName(\"open_button\")[0].files[0];var reader=new FileReader();var tiedosto=reader.readAsText(file);reader.addEventListener('load',readFile)}function remove_button_click(){for(var i=0;i<container.getElementsByClassName(\"row\").length;i+=1){var obj=container.getElementsByClassName(\"row\")[i];if(obj.getElementsByTagName(\"input\")[0].checked){if(json_list.length==1){json_list=[]}else{var deletedItem=json_list.splice(i,1)}}}parseResult()}function save_button_click(){var blob=new Blob([JSON.stringify(json_list)],{type:'application/x-download'});var blobUrl=URL.createObjectURL(blob);var a=document.createElement('A');a.href=blobUrl;a.download=\"file.json\";document.body.appendChild(a);a.click();document.body.removeChild(a)}function ledClick(obj){var i=obj.parentNode.getElementsByClassName(\"light\")[0].innerHTML;var a=obj.parentNode.parentNode.parentNode.id;json_list[a-1][i-1]=obj.checked+0;updateRemote()}function delayChange(obj){var a=obj.parentNode.parentNode.id;json_list[a-1][8]=parseInt(obj.value);updateRemote()}function add_line(i1,i2,i3,i4,i5,i6,i7,i8,delay_t){var new_size=container.getElementsByClassName(\"row\").length+1;(clone.firstElementChild||clone.firstChild).getElementsByClassName(\"n\")[0].innerHTML=new_size;clone.id=new_size;container.appendChild(clone.cloneNode(true));var leds=document.getElementsByName(\"led\");leds[(new_size-1)*8].checked=i1;leds[((new_size-1)*8)+1].checked=i2;leds[((new_size-1)*8)+2].checked=i3;leds[((new_size-1)*8)+3].checked=i4;leds[((new_size-1)*8)+4].checked=i5;leds[((new_size-1)*8)+5].checked=i6;leds[((new_size-1)*8)+6].checked=i7;leds[((new_size-1)*8)+7].checked=i8;document.getElementsByName(\"delay_t\")[new_size-1].value=delay_t;button_click=true;window.location.hash=clone.id}function add_button_click(){add_line(0,0,0,0,0,0,0,0,500);json_list.push([0,0,0,0,0,0,0,0,500])}window.onload=function(){container=document.getElementsByClassName(\"container\")[0];clone=container.getElementsByClassName(\"row\")[0].cloneNode(true);clone.removeAttribute(\"style\");container.innerHTML=\"\";read_json_file();getCurrentLine();document.getElementsByName(\"open_button\")[0].onchange=open_button_change;document.getElementsByName(\"remove_button\")[0].onclick=remove_button_click;document.getElementsByName(\"save_button\")[0].onclick=save_button_click;document.getElementsByName(\"add_button\")[0].onclick=add_button_click};window.onhashchange=function(){if(!button_click){window.scrollBy(0,-36)}else{button_click=false}};"));
            } else {
              client.println(F("HTTP/1.1 404 Not Found"));
              client.println(F("Content-Type: text/html"));
              client.println(F("Connection: close"));
          // Display received HTTP request on serial port
        if (c == '\n') currentLineIsBlank = true;
        else if (c != '\r') currentLineIsBlank = false;
    // Give the web browser time to receive the data
    // Close the connection

  // Delayed save to EEPROM
  if (changed && currentMillis - SaveMillis >= 60000) {
    changed = false;
    SaveMillis = currentMillis;
    // make JSON string
    String temp;
    temp = '[';
    for (int i = 0; i <= rowTotal; i++) {
      temp += '[';
      for (int ii = 0; ii < 8; ii++) {
        temp += contrArr[i][ii];
        temp += ',';
      temp += delayArr[i];
      temp += ']';
      if (i < rowTotal) temp += ',';
    temp += ']';
    // save to EEPROM
    Serial.println("Saving to EEPROM");
    //EEPROM.put(0, temp); Does not work - too easy
    for (int i = 0; i < temp.length(); i++) {

  // Update outputs
  if (currentMillis - PrgMillis >= delayArr[pos]) {
    PrgMillis = currentMillis;
    if (pos > rowTotal) pos = 0;

void parseJSON(String input) {
  String tempStr = "";
  int row = 0;
  int col = 0;
  for (int i = 0; i < input.length(); i++) {
    if (input.charAt(i) == ',') col++;
    else if (col < 8) contrArr[row][col] = input.charAt(i);
    else if (col == 8) tempStr += input.charAt(i);
    if (col == 9) {
      delayArr[row] = tempStr.toInt();
      tempStr = "";
      col = 0;
  delayArr[row] = tempStr.toInt(); // last line
  rowTotal = row;

Other projects

Other projects which uses VO2223 phototriac

ARDUINO is a registered trademark.

USB/Ethernet Relay Card with 8 relays


Arduino compatible web server based relay card which also can be used with USB connection. No special soldering skills required. Easy to program with popular ENC28J60 Ethernet controller. All Arduino Ethernet libraries are compatible e.g. this ENC28J60 library. Only one +5V USB power supply needed. All relais can be controlled individually. Relais can drive 5A load from 230V.

Board uses FTDI USB i.c. Arduino bootloader natively supports USB and virtual serial port. Device is programmable via USB port with Arduino IDE or similar programs.

All parts can buy from http://www.taydaelectronics.com. Only exception is ethernet connector. It must include transformers and filter elements.