How to Make an Interactive TCP Server with NodeMCU on the ESP8266

July 22, 2015 by Patrick Lloyd

Learn how to connect your device with Linux Screen using the built-in Lua interpreter to set up the file system and connect to the network. Then, learn how to automate the scripting process with Luatool and put it all together with a basic TCP server that can interact with a pulse-width modulated LED, a potentiometer, and a switch.

Recommended Level



This is Part 2 of an advanced series on connecting to and working with the "Internet of Things." Part 1 is located here. This project is relatively straightforward but still involves working intimately with the Linux terminal. Before attempting this project, it's recommended to have some experience with Linux as well as the Lua scripting language. Here is some documentation on Lua  as well as the documentation for the NodeMCU API. Remember, docs are your friend!

Last time, we discussed how to customize and compile the NodeMCU firmware from source using open source tools. Assuming a working NodeMCU installation, we're now going to explore some of the actual functionality of the device and begin interacting with both the "internet" and "things" aspects of IoT. Starting with a fresh flashing of the NodeMCU firmware, this project will walk through connecting to the device with Screen and using the built-in Lua interpreter to set up the file system and connect to the network. We'll then look at automating the scripting process with Luatool and put it all together with a basic TCP server that can interact with a pulse-width modulated LED, a potentiometer, and a switch.

Supplies Needed:

  • Linux Computer (Ubuntu 15.04 Vivid Vervet)
  • NodeMCU Devkit v0.9 with the following user modules compiled: Node, File, GPIO, WiFi, Net, PWM, Timer, ADC, UART
  • WiFi access 
  • 1x LED
  • 1x 100-300 Ohm resistor
  • 1x 5K resistor
  • 1x 10K potentiometer
  • 1x Normally open momentary switch


  • Git​
  • Python 2.7
  • Pyserial
  • Linux Screen
  • Luatool

​To install these in Ubuntu 15.04, type the following into the terminal:

sudo apt-get install git screen python2.7 python-serial
mkdir -p ~/.opt && cd ~/.opt # just a user-owned directory for software. Replace with what suits your needs
git clone

Connecting to NodeMCU for the First Time

1) Connect to NodeMCU:

All interaction with NodeMCU happens through the serial port at 9600 baud. I prefer to use Screen in the terminal but Minicom or any program that can interact with the serial port will do. Plug in your NodeMCU Devkit and type:

screen /dev/ttyUSB0 9600

If your NodeMCU Devkit happens to live on a different serial port than /dev/ttyUSB0, use dmesg | grep tty to find it.

This should show just a plain black terminal screen with no text. Once connected, press the button labeled USER to reset the device. Or type the following into the terminal and press enter:



Press this lil' guy right here

You should now get a bit of gibberish (communication at a different baudrate) and then some kind of welcome message and a '>' prompt. You are now inside NodeMCU's Lua interpreter. The init.lua error is expected since this is a brand new install and we haven't yet sent any scripts to the device. Init.lua is what NodeMCU runs once at startup (sort of like the setup() function in Arduino).

Jacking into the Matrix
The interpreter's inside the computer?! It's so simple!


2) Format node file system

If you are working with a fresh install of NodeMCU, we need to format NodeMCU's file system before we can start writing and saving Lua scripts. In the interpreter, type:



Once completed, you can see information on the file system by using file.fsinfo() call:

remaining, used, total = file.fsinfo()
print("\nFile system info:\nTotal: "" Bytes\nUsed: "..used.." Bytes\nRemaining: "..remaining.." Bytes\n")


This will show you total size, free space, and occupied space of just the NodeMCU file system, not the raw memory information of the ESP8266. The ESP8266 has 64 KiB of instruction RAM, 96 KiB of data RAM and about 4 MiB of external flash memory.

Three and a half megabytes? We can work with that.

Note: If you see something like "stdin:1: unexpected symbol near ‘char(27)’” or “stdin: bad header in precompiled chunk” while you're typing commands, don't worry: it's just a little tic of Screen. Hannes Lehmann wrote about the issue on his website saying, "... If you get some errors like “stdin:1: unexpected symbol near ‘char(27)’” or “stdin: bad header in precompiled chunk” then your terminal doesn’t support backspace or arrow input (either copy&paste issue, or you have done a correction of your input). Don’t worry, just repeat the command."

My particular setup with Screen seems like it can handle corrections using backspace but it tends to mess up when I try use the arrow key.

Hello World, Hello WiFi!

3) Connect to WiFi network

Since the main selling point of the ESP8266 is its WiFi stack, let's use the Lua interpreter to connect to the local network and get an IP address. 

The interactive Lua terminal on the device is good for prototyping small bits of code. To connect to your local WiFi and display the IP information, type into the terminal:

wifi.sta.config("wifi_name","wifi_pass") -- Replace these two args with your own network
ip, nm, gw=wifi.sta.getip()
print("\nIP Info:\nIP Address: "..ip.." \nNetmask: "..nm.." \nGateway Addr: ""\n")


We're connected!
We're connected!


4) Automate with Luatool

Testing small pieces of code with the interpreter is great but what if you want to write something more complicated and have it run automatically at startup? Github user 4refr0nt wrote a program called Luatool that can upload Lua scripts from your computer to the NodeMCU Devkit and save them on the device's file system. Navigate to the Luatool folder that you cloned from Github in the beginning:

cd ~/.opt/luatool/luatool


It should have two files in it in addition to init.lua and main.lua. Using your favorite editor, modify the respective files to look like this:

-- init.lua --

-- Global Variables (Modify for your network)
ssid = "my_ssid"
pass = "my_pass"

-- Configure Wireless Internet
print('\nAll About Circuits init.lua\n')
print('set mode=STATION (mode='..wifi.getmode()..')\n')
print('MAC Address: ',wifi.sta.getmac())
print('Chip ID: ',node.chipid())
print('Heap Size: ',node.heap(),'\n')
-- wifi config start
-- wifi config end

-- Run the main file

-- main.lua --

-- Connect 
print('\nAll About Circuits main.lua\n')
tmr.alarm(0, 1000, 1, function()
   if wifi.sta.getip() == nil then
      print("Connecting to AP...\n")
      ip, nm, gw=wifi.sta.getip()
      print("IP Info: \nIP Address: ",ip)
      print("Netmask: ",nm)
      print("Gateway Addr: ",gw,'\n')

 -- Start a simple http server
    conn:send("<h1> Hello, NodeMCU!!! </h1>")
  conn:on("sent",function(conn) conn:close() end)

5) Close your current Screen session (Luatool can't communicate with NodeMCU otherwise) and then upload both files to the NodeMCU: 

python --port /dev/ttyUSB0 --src init.lua --dest init.lua --verbose
python --port /dev/ttyUSB0 --src main.lua --dest main.lua --verbose


6) Reconnect to the NodeMCU Devkit with Screen and press the USER button to reset the device:

screen /dev/ttyUSB0 9600


You should then see something like this:

Go to that IP address (in my case, and Voila! 

Check it out! We have a teeny, tiny server!

Now for Some Hardware

7) Build the circuit and upload the server code

Here is the schematic. It's fairly simple since we're dealing mostly with software. 


The hardware laid out in Fritzing


Here's my setup
My own setup


Now edit the init.lua and main.lua files from before to look like the following:

-- init.lua --

-- Network Variables
ssid = "your_ssid"
pass = "your_pass"

-- Byline
print('\ NodeMCU Example\n')

-- Configure Wireless Internet
print('set mode=STATION (mode='..wifi.getmode()..')\n')
print('MAC Address: ',wifi.sta.getmac())
print('Chip ID: ',node.chipid())
print('Heap Size: ',node.heap(),'\n')

-- Configure WiFi


-- main.lua --

-- WiFi Connection Verification --
tmr.alarm(0, 1000, 1, function()
   if wifi.sta.getip() == nil then
      print("Connecting to AP...\n")
      ip, nm, gw=wifi.sta.getip()
      print("IP Info: \nIP Address: ",ip)
      print("Netmask: ",nm)
      print("Gateway Addr: ",gw,'\n')

-- Global Variables --
led_pin = 1
sw_pin = 2
adc_id = 0 -- Not really necessary since there's only 1 ADC...
adc_value = 512

-- Amy from Gargantia on the Verdurous Planet
blink_open = ""
blink_close = ""
site_image = blink_open

-- GPIO Setup --
print("Setting Up GPIO...")
-- Inable PWM output
pwm.setup(led_pin, 2, 512) -- 2Hz, 50% duty default

-- Enable input
gpio.mode(sw_pin, gpio.INPUT)

-- Web Server --
print("Starting Web Server...")
-- Create a server object with 30 second timeout
srv = net.createServer(net.TCP, 30)

-- server listen on 80, 
-- if data received, print data to console,
-- then serve up a sweet little website
	conn:on("receive", function(conn, payload)
		--print(payload) -- Print data from browser to serial terminal
		function esp_update()
            if mcu_do == "Update+LED" then 
            	if == 1 then
            		site_image = blink_open
            		-- Adjust freq
            		pwm.setclock(led_pin, adc_value)
            		print("Set PWM Clock")	
        		elseif == 0 then
        			site_image = blink_close
        			-- Adjust duty cycle
        			pwm.setduty(led_pin, adc_value)
        			print("Set PWM Duty")	
            if mcu_do == "Read+ADC" then
            	adc_value =
            	-- Sanitize ADC reading for PWM
				if adc_value > 1023 then
					adc_value = 1023
				elseif adc_value < 0 then
					adc_value = 0
				print("ADC: ", adc_value)

        --parse position POST value from header
        --If POST value exist, set LED power
        if postparse[2]~=nil then esp_update()end

        -- HTML Header Stuff
        conn:send('HTTP/1.1 200 OK\n\n')
        conn:send('<!DOCTYPE HTML>\n')
        conn:send('<head><meta  content="text/html; charset=utf-8">\n')
        conn:send('<title>ESP8266 Blinker Thing</title></head>\n')
        conn:send('<body><h1>ESP8266 Blinker Thing!</h1>\n')
        -- Images... just because
        conn:send('<IMG SRC="'..site_image..'" WIDTH="392" HEIGHT="196" BORDER="1"><br><br>\n')

        -- Labels
        conn:send('<p>ADC Value: '..adc_value..'</p><br>')
        conn:send('<p>PWM Frequency (Input High): '..adc_value..'Hz</p>')
        conn:send('<p>PWM Duty Cycle (Input Low): '..(adc_value * 100 / 1024)..'%</p><br>')

       	-- Buttons 
       	conn:send('<form action="" method="POST">\n')
        conn:send('<input type="submit" name="mcu_do" value="Read ADC">\n')
        conn:send('<input type="submit" name="mcu_do" value="Update LED">\n')
        conn:on("sent", function(conn) conn:close() end)

Again, close any active Screen sessions for Luatool and upload both files to the NodeMCU: 

python --port /dev/ttyUSB0 --src init.lua --dest init.lua --verbose
python --port /dev/ttyUSB0 --src main.lua --dest main.lua --verbose


8) Reconnect to the NodeMCU Devkit with Screen and press the USER button to reset the device:

screen /dev/ttyUSB0 9600


Here is a video of the project in action:

What's Going On Here?

When the user presses the "Read ADC" button in the browser, the browser is updated with the current ADC reading of the potentiometer and that value is posted to the NodeMCU's serial terminal if you have it open. If the pushbutton is not pressed, the input pin is pulled high which means that the current ADC reading will be used to set the LED's PWM frequency. If it is pressed, and the input pin is pulled low, the LED's PWM duty cycle is adjusted. You also get a different image in the browser depending on what LED parameter is being set.

Now let's take a moment to dig through the code to see how this is implemented. Init.lua contains mostly code from the "Hello Word, Hello WiFi" section. It displays some information about the chip and connects to the wireless network. Main.lua is where all the fun happens -- it's a modification of the code here. The flow of that script is to print IP information, initialize some global variables, configure the I/O, and then create a TCP server that listens on port 80. Any time a button is pressed, an HTTP POST method is called by the browser. The string.find() method in the script looks though the HTTP header and tries to find any mention of a button named "mcu_do". If this does turn up, the esp_update() function is called and depending on the value assigned to mcu_do, it will either read the ADC or update the parameters of the LED. And there you have it, a bit of hardware that can interact with a browser in a meaningful way and vice versa.


Closing Remarks

This project only scratches the surface of what you can do with the ESP8266 and NodeMCU. It can act as an MQTT broker, talk UDP as well as TCP, perform cryptography, communicate with peripherals over I2C and SPI, and a ton more. The NodeMCU Devkit is a powerful hardware device that can enable very powerful IoT applications but is by no means the only or even the best solution for all projects. Keep your eyes peeled as new solutions in software and hardware spring into the blossoming IoT landscape.


Give this project a try for yourself! Get the BOM.

  • X
    xtal September 07, 2015

    I downloaded the interactive web server with the blink picture - it works fine IF I use a wired connection to router and
    wireless to ESP8266,,,However if I use wireless to router and wireless to ESP8266 I get double displays ,,,maybe I’m getting an error and seeing a retransmit , if so how do I stop the double display , or get it to reposition correctly… read ADC several times to see problem

    Like. Reply
  • E
    edmondkreuk December 26, 2015

    All I get is a white page in my browser… At first I edited main.lua and init.lua myself, but I also did a copy-paste. Same result. In Screen I do see the text, but the “Hello World” doesn’t appear in my browser. What went wrong?

    Like. Reply
  • Marcel Stör January 11, 2016

    It was kind of a bug this ever worked. Due to the event-driven asynchronous nature of NodeMCU multiple consecutive calls to `conn:send` are not guaranteed to be executed in the order you define them. See for more details.

    Like. Reply
  • K
    kamranmu February 20, 2016

    Hello Mr. Patrick Lloyd
    I put both the files init.lua and main.lua in esp8266, everything done as you told, in my case the ip address is But the problem is when I open this page on my browser (mozilla firefox) it displays some html codes,not the image like yours. the response is given below…please tell me, how to resolve this problem….

    Like. Reply
  • cobrp March 10, 2016

    Very nice example of a TCP-server on the nodeMCU!
    Thank you!

    Like. Reply
  • S
    sm1th_ March 11, 2016

    Created custom build using and noticed multiple “send” commands do not work. Only the first one was received in the browser. I found this issue mentioned here:  Using ‘recursive’ send commands as described there does work. I needed this for html that was about 5kb in size.

    Like. Reply
  • rcbsimoes May 09, 2016

    great post!!!! i will try this myself!
    still i have one question, is there a way to access this server from outside the intranet?
    if i understood correctly this is only valid for internal ip addresses. right?
    thanks in advance!

    Like. Reply
    • onion May 22, 2016
      OP here. I'm not totally sure since I haven't tried it myself but I think Mosquitto has some configuration options that will allow you to do that. It also probably involves opening a network port in your router/firewall.
      Like. Reply
  • N
    nayiv February 12, 2017

    hi, how can I change the 80 port to 88 port here srv:listen(80,function(conn)?

    Like. Reply
  • Mild Mint February 24, 2017

    Thanks for sharing

    Like. Reply
  • G
    geert56 March 11, 2017

    Bit of an on the side question, but what tool was used to draw the nice schematic?

    Like. Reply