Raspberry PI4 and PXE with NFS4

  • dhcp server
  • NFS server, preferably NFSv4
  • rpi 4 with raspbian (buster)

DHCP server settings

We need to add code 66 to DHCP options. I’m using mikrotik, so I’ve added one DHCP option

  • name: ‘tftp-server-name’
  • code: 66
  • value: s’192.168.1.100′

please note the value with preceding ‘s’, it’s required. Then add one dhcp option group, add this option and add this option group to certain dhcp lease

NFS server configuration

Configure your server with privileges from given IPs, sys, async. You’ll need one mountpoint (or if you need more security, you can define one per host), like /pxe with rw privileges, limited to given IP(s). My directory structure looks like

- pxe
  - tftp-server
    - [serial]
  - rpis
    - [serial]

Whereas tftp-server/[serial] contains boot code for certain IP, and rpis/[serial] is root.

To obtain serial, use this code.

vcgencmd otp_dump | grep 28: | sed s/.*://g

TFTP server

To obtain boot from network, you need one TFTP server. Ideally, use one of mountpoints. Root of your TFTP must be set to /pxe/tftp-server

Preparing eeprom to PXE boot

You need to do following steps – update apt and upgrade to the lastest, and install latest rpi-eeprom package

apt update
apt full-upgrade
apt install rpi-eeprom

Then copy latest .bin to the new one and extract parameters

cp /lib/firmware/raspberrypi/bootloader/stable/pieeprom-[find latest].bin pieeprom.bin
rpi-eeprom-config pieeprom.bin > bootconf.txt
vim bootconf.txt

and edit bootconf.txt like this

[all]
BOOT_UART=0
WAKE_ON_GPIO=1
POWER_OFF_ON_HALT=0
DHCP_TIMEOUT=45000
DHCP_REQ_TIMEOUT=4000
TFTP_FILE_TIMEOUT=30000
TFTP_IP=
TFTP_PREFIX=0
BOOT_ORDER=0x21
SD_BOOT_MAX_RETRIES=3
NET_BOOT_MAX_RETRIES=5
[none]
FREEZE_VERSION=0

The most important part is BOOT_ORDER. 0x21 tells to try sd card, then network boot. Now to update eeprom.

rpi-eeprom-config --out pieeprom-new.bin --config bootconf.txt pieeprom.bin
rpi-eeprom-update -d -f ./pieeprom-new.bin
reboot

Filesystem preparation

as there’s no swap possible, remove dphys-swap

apt remove -y --purge dphys-swapfile
rm /var/swap

We need to disable resize2fs if enabled

systemctl disable resize2fs_once

Now we need to copy our /boot into /pxe/tftp-server/[serial]. So prepare mount and do the copy

mkdir -p /mnt/pxe
mount 192.168.1.100:/pxe /mnt/pxe -t nfs -o proto=tcp,vers=4.2,port=2049
export serial=[serial from above]
copy -r /boot /mnt/pxe/tftp-server/$serial/

And now to copy root filesystem to NFS

rsync -xa --progress --exclude /mnt/pxe / /mnt/pxe/rpis/$serial/

Once the copy is done, edit fstab in the copy (in /mnt/pxe/rpis/$serial/etc/fstab) and remove all entries for / and replace /boot mountpoint. fstab should then look like this

proc            /proc           proc    defaults          0       0
192.168.1.100:/pxe/tftp-server/[serial] /boot nfs defaults,proto=tcp,port=2049,vers=4.2 0 0

Now it’s time to modify cmdline.txt in boot /mnt/pxe/tftp-server/[serial] directory, to specify root mountpoint

console=tty1 root=/dev/nfs nfsroot=192.168.1.100:/pxe/rpis/[serial],proto=tcp,port=2049,vers=4.2 rw ip=dhcp rootwait elevator=deadline

It must be one oneliner only.

Now it’s time to shutdown your rpi, remove SD card and let it boot from the network. Enjoy!

bash to resolve dependencies

#!/bin/bash

declare -A USE_MAP
declare -a ENABLES

# export_name dependency dependecy dependency...
USE_MAP['HBASE']='USE_HBASE HCATALOG HUE'
USE_MAP['PIG']='USE_PIG'
USE_MAP['HUE']='USE_HUE'
USE_MAP['HCATALOG']='USE_HCATALOG'
USE_MAP['PRESTO']='USE_PRESTO HCATALOG'
ENABLES=()

function use() {
  local dep=(${USE_MAP["$1"]})
  ENABLES+=(${dep[0]})
  dep=("${dep[@]:1}")
  if [ ! -z "$dep" ]; then
    for item in ${dep[@]}; do
      use $item
    done
  fi
}

function export_enabled() {
  sorted_use=($(echo "${ENABLES[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
  for item in ${sorted_use[@]}; do
    echo "export $item";
  done
}

use HBASE
export_enabled

------

$ bash resolve.sh
export USE_HBASE
export USE_HCATALOG
export USE_HUE

Ambari – user can log in but has no privileges

We ran into some interesting issue – user has all the privileges, was in correct ldap group, can log in to the ambari, but privileges weren’t effective.

I did multiple discoveries – checked log ambari-server.log and found out following

com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key myuser.

So, tried to restart ambari-server, didn’t work. Did further investigation, using ambari API

$ curl -XGET -H"X-Requested-By: ambari" -u admin:... https://ambari.mycluster.com/api/v1/users/myuser
{
  "href" : "http://ambari.mycluster.com/api/v1/users/myuser",
  "Users" : {
    "active" : true,
    "admin" : false,
    "groups" : [
      "ldap-users",
      "cluster-admins"
    ],
    "ldap_user" : false,
    "user_name" : "MYUSER",
    "user_type" : "PAM"
  },

so, this worked. Notice uppercase in user_name. So I tried to fetch user info with privileges

$ curl -XGET -H"X-Requested-By: ambari" -u admin:... https://ambari.mycluster.com/api/v1/users/myuser?fields=privileges/*
{
  "href" : "http://ambari.mycluster.com/api/v1/users/myuser?fields=privileges/*",
  "Users" : {
    "user_name" : "MYUSER"
  },
  "privileges" : [
    {
      "href" : "http://ambari.mycluster.com/api/v1/users/myuser/privileges/153",
      "PrivilegeInfo" : {
        "instance_name" : "INSTANCE",
        "permission_label" : "View User",
        "permission_name" : "VIEW.USER",
        "principal_name" : "cluster-admins",
        "principal_type" : "GROUP",
        "privilege_id" : 153,
        "type" : "VIEW",
        "user_name" : "MYUSER",
        "version" : "2.4.3.0",
        "view_name" : "ADMIN_VIEW"
      }
    },

Nice. Works too. So another check – load only certain privileges – 153.

$ curl -XGET -H"X-Requested-By: ambari" -u admin:... https://ambari.mycluster.com/api/v1/users/myuser/privileges/153
{
  "status": 500,
  "message": "Server Error"
}

and even upcase

$ curl -XGET -H"X-Requested-By: ambari" -u admin:... https://ambari.mycluster.com/api/v1/users/MYUSER/privileges/153
{
  "status": 500,
  "message": "Server Error"
}

Aha! Looks like there’s some inconsitency in the postgres database. So, check it!

ambari=> select * from users where user_name='myuser';
 user_id | principal_id | ldap_user | user_name | create_time | user_password | active | active_widget_layouts | user_type
---------+--------------+-----------+-----------+-------------+---------------+--------+-----------------------+-----------
(0 rows)

ambari=> select * from users where user_name='MYUSER';
 user_id | principal_id | ldap_user | user_name |      create_time       | user_password | active | active_widget_layouts | user_type
---------+--------------+-----------+-----------+------------------------+---------------+--------+-----------------------+-----------
   16005 |        20005 |         0 | MYUSER  | 2019-11-22 04:37:44.18 |               |      1 | [{"id":"29405"}]      | PAM
(1 row)

Gotcha. There’s no such user named myuser in the database – and pg is case sensitive. So, rename the user

ambari=> update users set user_name='myuser' where user_id=16005;
UPDATE 1
ambari=> \q

and restart ambari-server. Now we can try again the api call

$ curl -XGET -H"X-Requested-By: ambari" -u admin:... https://ambari.mycluster.com/api/v1/users/myuser/privileges/153
{
  "href" : "http://ambari.mycluster.com/api/v1/users/myuser/privileges/153",
  "PrivilegeInfo" : {
    "instance_name" : "INSTANCE",
    "permission_label" : "View User",
    "permission_name" : "VIEW.USER",
    "principal_name" : "cluster-admins",
    "principal_type" : "GROUP",
    "privilege_id" : 153,
    "type" : "VIEW",
    "user_name" : "myuser",
    "version" : "2.4.3.0",
    "view_name" : "ADMIN_VIEW"
  }
}

Voila!

Foxtrot: Programy pro ČOV

Program pro řídící jednotku Foxtrot a čistírnu odpadních vod Aquatec AT-6. Nabízí 10 programů, dle zatížení domácnosti. Dá se později napojit na vodoměr (počitadlo) a podle toho měnit program.

TYPE
  T_COVProgram :STRUCT
    runs: ARRAY[0..23] OF usint;
  END_STRUCT
END_TYPE
VAR_GLOBAL
  COV_Programs: ARRAY[1..10] OF T_COVProgram := [
  // cas - hodina rozdelena na 12 useku po 5min.
  // hodnoty: 1 -> 1 min bezi, 5-1=4 stoji
//            0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// 1 - 5.6h celkem, 4 min stoji, 1 min bezi
    (runs := [5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
// 2 - 8.8h celkem, 4 min stoji, 1 min bezi
    (runs := [5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5]),
// 3 - 10.4h celkem, 4 min stoji, 1 min bezi
    (runs := [5, 5, 5, 5, 5, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 1, 1]),
// 4 - 12h celkem, 4 min stoji, 1 min bezi
    (runs := [5, 5, 5, 5, 5, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 5, 5]),
// 5 - 15h celkem, 3 min stoji, 2 min bezi
    (runs := [5, 5, 5, 5, 5, 2, 2, 2, 2, 2, 5, 2, 2, 2, 2, 2, 5, 2, 2, 2, 2, 2, 5, 5]),
// 6 - 18h celkem, 2 min stoji, 3 min bezi
    (runs := [5, 5, 5, 5, 5, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 3, 5, 5]),
// 7 - 20.4h celkem, 2 min stoji, 3 min bezi
    (runs := [5, 5, 5, 5, 5, 5, 3, 3, 3, 5, 5, 5, 3, 3, 3, 5, 5, 5, 3, 3, 3, 5, 5, 5]),
// 8 - 21.6h celkem, 2 min stoji, 3 min bezi
    (runs := [5, 5, 5, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 3, 5, 5, 5, 5]),
// 9 - 22.8h celkem, 2 min stoji, 3 min bezi
    (runs := [5, 5, 5, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 3, 5, 5, 5, 5, 5, 3, 5, 5, 5, 5]),
// 10 - 24h celkem, 24h bezi
    (runs := [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5])
   ];
END_VAR
VAR_GLOBAL RETAIN
  COV_PROGRAM : usint := 0;  
END_VAR

FUNCTION_BLOCK fbCistickaProgram
  VAR_INPUT
    prog_num: usint;
  END_VAR
  VAR_OUTPUT
    output: BOOL;
  END_VAR
  VAR_IN_OUT
  END_VAR
  VAR
    ton_stop: ton;
    ton_run: ton;
    light: fb_iLight;
    curr_time: time;
    hour: usint;
    covprg : T_COVProgram;
    mins : usint;
  END_VAR

curr_time := GetTime();
hour := hour_of_time(in := curr_time);
covprg := COV_Programs[prog_num];
mins := covprg.runs[hour];

ton_stop( IN := not ton_run.q, pt := encodeTime(M := 5-mins));
ton_run( IN := ton_stop.q, pt := encodeTime(M:= mins));

light( lightOn := ton_stop.q, lightOff := ton_run.q, name:= 'Cisticka', out => output );
END_FUNCTION_BLOCK
PROGRAM prgJson
  VAR
    cisticka : fbCistickaProgram;
  END_VAR

if COV_PROGRAM = 0 then
  COV_PROGRAM := 1;
end_if;

cisticka( prog_num := COV_PROGRAM, output => RO04_COV );
END_PROGRAM

Jsou tam ještě nějaké záseky, např. že pokud je cyklus plný (5 minut) a následuje ten samý (bez pauzy), bylo by fajn vynechat úplně vypínání výstupu, popř. zkrátit cyklus o párset ms. Dále pak přidat reset v případě změny programu atd. Každopádně to ale zatím funguje k mé spokojenosti.

R – Četnost podruhé

Mějme následující data:

> head(dotaznik.csv[,c(2,3)])
  type gender
1    H      F
2    H      M
3    A      F
4    A      M
5    A      F
6    H      M

A potřebujeme zjistit absolutní a relativní četnost výskytu žen a mužů, rozdělenou ještě podle hodnoty v type .

> tbl <- table(dotaznik.csv$gender, dotaznik.csv$type)
> tbl
   
     A  H
  M 18 16
  Ž 17 19

Opět převedeme na data frame

> tbl.frame <- as.data.frame(tbl)
> tbl.frame
  Var1 Var2 Freq
1    M    A   18
2    Ž    A   17
3    M    H   16
4    Ž    H   19

A máme absolutní četnost. Teď vypočítáme relativní četnost.

> library(plyr) 
> ddply(tbl.frame, .(Var2), transform, prop=Freq/sum(Freq))
  Var1 Var2 Freq      prop
1    M    A   18 0.5142857
2    Ž    A   17 0.4857143
3    M    H   16 0.4571429
4    Ž    H   19 0.5428571

Což znamená: pro každý subset z framu tbl.frame , rozdělený podle proměnné Var2 , proveď transform , a vypočti relativní četnost podle frekvence (abs. četnosti) a součtu hodnot frekvence.

R – četnost a graf

Dostal jsem pověření z nejvyšších míst vyrobit statistické zhodnocení dotazníků. A protože bytostně nemám rád Excel, našel jsem R a zkouším.

Udělal jsem data do CSV souboru a nahrál do R (RStudio)

> head(dotaznik.csv)
  num type gender age height weight           edu
1   1    H      M  60    182    100 Vysokoškolské
2   2    H      M  49    188    102      Vyučen/a
3   3    H      M  61    176     75      Vyučen/a
4   4    H      M  56    180    110       Střední
5   5    H      M  47    180     95 Vysokoškolské
6   6    H      M  48    178     95       Střední

Zkusíme vyrobit četnost vzdělání (edu) a potom ji ještě rozdělit podle pohlaví (gender).

> edu <- table(dotaznik.csv$edu)
> edu

      Střední Vysokoškolské Vyšší odborné      Vyučen/a 
           30             7             2            25 
     Základní 
            6

Máme tabulku, ale potřebujeme z ní frame

> fedu <- as.data.frame(edu)
> fedu
           Var1 Freq
1       Střední   30
2 Vysokoškolské    7
3 Vyšší odborné    2
4      Vyučen/a   25
5      Základní    6

Přidáme relativní četnost (mean)

> fedu$mean <- fedu$Freq / sum(fedu$Freq)
> fedu
           Var1 Freq       mean
1       Střední   30 0.42857143
2 Vysokoškolské    7 0.10000000
3 Vyšší odborné    2 0.02857143
4      Vyučen/a   25 0.35714286
5      Základní    6 0.08571429

A teď na graf. Používáme ggplot2.

ggplot(fedu, aes(x=Var1, y=Freq)) + geom_bar(stat='identity')

Rplot1

Fajn. Ale chceme graf otočit

ggplot(fedu, aes(x=Var1, y=Freq)) + geom_bar(stat='identity') + coord_flip()

Rplot2

Teď ještě smazat popisek osy x a přepsat osu y.

ggplot(fedu, aes(x=Var1, y=Freq)) + geom_bar(stat='identity') +
coord_flip() +
xlab('Vzdělání') + 
theme(axis.title.x = element_blank()) + 
ggtitle('Dosažené vzdělání')

Rplot3

A finálně přidáme hodnoty frekvence do jednotlivých sloupců.

ggplot(fedu, aes(x=Var1, y=Freq)) + 
geom_bar(stat='identity', position=position_dodge()) + 
geom_text(aes(label=Freq), hjust=1.3, color="white",position = position_dodge(0.9), size=3.5) + 
coord_flip() + 
xlab('Vzdělání') + 
theme(axis.title.x = element_blank()) + 
ggtitle('Dosažené vzdělání')

Rplot4

A teď trochu komplexněji. Ještě to rozdělíme na muže a ženy..

> edu <- table(dotaznik.csv$gender, dotaznik.csv$edu)
> edu
   
    Střední Vysokoškolské Vyšší odborné Vyučen/a Základní
  M      10             4             2       17        1
  Ž      20             3             0        8        5

> fedu <- as.data.frame(edu)
> fedu
   Var1          Var2 Freq
1     M       Střední   10
2     Ž       Střední   20
3     M Vysokoškolské    4
4     Ž Vysokoškolské    3
5     M Vyšší odborné    2
6     Ž Vyšší odborné    0
7     M      Vyučen/a   17
8     Ž      Vyučen/a    8
9     M      Základní    1
10    Ž      Základní    5

a přidáme graf

> ggplot(fedu, aes(x=Var2, y=Freq, fill=Var1)) + 
geom_bar(stat='identity', position=position_dodge()) + 
geom_text(aes(label=Freq), hjust=1.6, color="white",position = position_dodge(0.9), size=3.5) + 
coord_flip() + 
xlab('Vzdělání') + 
theme(axis.title.x = element_blank()) + 
ggtitle('Dosažené vzdělání') + 
labs(fill = 'Pohlaví')

Rplot6

A co když budeme chtít jeden bar, ale rozdělený podle hodnot?

# seridime podle Var2 a Freq
> library(plyr)
> fedu_s <- arrange(fedu, Var2, Freq)
> fedu_s
   Var1          Var2 Freq
1     M       Střední   10
2     Ž       Střední   20
3     Ž Vysokoškolské    3
4     M Vysokoškolské    4
5     Ž Vyšší odborné    0
6     M Vyšší odborné    2
7     Ž      Vyučen/a    8
8     M      Vyučen/a   17
9     M      Základní    1
10    Ž      Základní    5

# a pridame souhrnny soucet, rozdeleny s kazdym jinym Var2
> fedu_s_sum <- ddply(fedu_s, 'Var2', transform, label_ypos=cumsum(Freq))
> fedu_s_sum
   Var1          Var2 Freq label_ypos
1     M       Střední   10         10
2     Ž       Střední   20         30
3     Ž Vysokoškolské    3          3
4     M Vysokoškolské    4          7
5     Ž Vyšší odborné    0          0
6     M Vyšší odborné    2          2
7     Ž      Vyučen/a    8          8
8     M      Vyučen/a   17         25
9     M      Základní    1          1
10    Ž      Základní    5          6

A graf

ggplot(fedu_s_sum, aes(x=Var2, y=Freq, fill=Var1)) + 
geom_bar(stat='identity') + 
geom_text(aes(label=Freq, y=label_ypos), hjust=1.6, color="white", size=3.5) + 
coord_flip() + 
xlab('Vzdělání') + 
theme(axis.title.x = element_blank()) + 
ggtitle('Dosažené vzdělání') + 
labs(fill = 'Pohlaví')

Rplot7

 

STM32F4Discovery + eLua + OSX

I fixed few bugs when compiling elua from git on osx – clone my branch clone official repo here.

$ ./build_elua.lua board=stm32f4discovery

create .bin file

$ arm-none-eabi-objcopy -O binary elua_lua_stm32f4discovery.elf elua_lua_stm32f4discovery.bin

create openocd config file for stm32f4 board:

$ cat ~/stm32f4discovery.cfg
# stm32f4discover board
source [find interface/stlink-v2-1.cfg]
transport select hla_swd
source [find target/stm32f4x.cfg]
reset_config srst_only

and upload to the board:

$ openocd -f ~/stm32f4discovery.cfg \
   -c "init" \   
   -c "reset halt" \
   -c "sleep 100" \
   -c "wait_halt 2" 
   -c "echo \"--- Writing elua_lua_stm32f4discovery.bin\"" \
   -c "flash write_image erase elua_lua_stm32f4discovery.bin 0x08000000" \
   -c "sleep 100" \
   -c "echo \"--- Verifying\"" \
   -c "verify_image elua_lua_stm32f4discovery.bin 0x08000000" \
   -c "sleep 100" \
   -c "echo \"--- Done\"" \
   -c "resume" \
   -c "shutdown"

Open On-Chip Debugger 0.9.0 (2015-11-16-01:48)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 2000 kHz
adapter_nsrst_delay: 100
none separate
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : clock speed 1800 kHz
Info : STLINK v2 JTAG v25 API v2 SWIM v14 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.884520
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
target state: halted
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x080063f0 msp: 0x20000c78
--- Writing elua_lua_stm32f4discovery.bin
auto erase enabled
Info : device id = 0x10076413
Info : flash size = 1024kbytes
wrote 262144 bytes from file elua_lua_stm32f4discovery.bin in 7.783807s (32.889 KiB/s)
--- Verifying
target state: halted
target halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x2000002e msp: 0x20000c78
verified 220776 bytes in 2.229936s (96.685 KiB/s)
--- Done
shutdown command invoked

Now you can connect to the board using /dev/tty.usbmodem621, 115200/8/n/1. You should get this prompt:

eLua#
eLua# help
Shell commands:
  help   - shell help
  lua    - start a Lua session
  ls     - lists files and directories
  dir    - lists files and directories
  cat    - list the contents of a file
  type   - list the contents of a file
  recv   - receive files via XMODEM
  cp     - copy files
  mv     - move/rename files
  rm     - remove files
  ver    - show version information
  mkdir  - create directories
  exit   - exit the shell
For more information use 'help <command>'.
eLua# lua
Press CTRL+Z to exit Lua
Lua 5.1.4  Copyright (C) 1994-2011 Lua.org, PUC-Rio
> print(pd.board() .. "/" .. pd.platform() .. "/" .. pd.cpu())
STM32F4DISCOVERY/STM32F4/STM32F407VG

enjoy :)

nodemcu – simple webserver

I’ve modified and updated this source:

local SSID = "my_wifi_ssid"
local SSID_PASSWORD = "my_wifi_password"

local function http_header(conn)
  conn:send('HTTP/1.1 200 OK\n\n')
  conn:send('<!DOCTYPE HTML>\n')
  conn:send('<html>\n')
  conn:send('<head><meta  content="text/html; charset=utf-8">\n')
  conn:send('<title>ESP8266 znouza test</title></head>\n')
end

local function connect (conn, data)
   local query_data

   conn:on ("receive",
      function (cn, req_data)
         query_data = get_http_req (req_data)
         print (query_data["METHOD"] .. " " .. " " .. query_data["User-Agent"])
         http_header(cn)
         cn:send ("<body>")
         cn:send ("<h1>Hello World from ESP8266 and NodeMCU!!</h1>")
         cn:send ("</body></html>")
         -- Close the connection for the request
         cn:close ( )
      end)
end

function wait_for_wifi_conn ( )
   tmr.alarm (1, 1000, 1, function ( )
      if wifi.sta.getip ( ) == nil then
         print ("Waiting for Wifi connection")
      else
         tmr.stop (1)
         print ("ESP8266 mode is: " .. wifi.getmode ( ))
         print ("The module MAC address is: " .. wifi.sta.getmac ( ))
         print ("Config done, IP is " .. wifi.sta.getip ( ))
      end
   end)
end

-- Build and return a table of the http request data
function get_http_req (instr)
   local t = {}
   local first = nil
   local key, v, strt_ndx, end_ndx

   for str in string.gmatch (instr, "([^\n]+)") do
      -- First line in the method and path
      if (first == nil) then
         first = 1
         strt_ndx, end_ndx = string.find (str, "([^ ]+)")
         v = trim (string.sub (str, end_ndx + 2))
         key = trim (string.sub (str, strt_ndx, end_ndx))
         t["METHOD"] = key
         t["REQUEST"] = v
      else -- Process and remaining ":" fields
         strt_ndx, end_ndx = string.find (str, "([^:]+)")
         if (end_ndx ~= nil) then
            v = trim (string.sub (str, end_ndx + 2))
            key = trim (string.sub (str, strt_ndx, end_ndx))
            t[key] = v
         end
      end
   end

   return t
end

-- String trim left and right
function trim (s)
  return (s:gsub ("^%s*(.-)%s*$", "%1"))
end

-- Configure the ESP as a station (client)
wifi.setmode (wifi.STATION)
wifi.sta.config (SSID, SSID_PASSWORD,1)

-- Hang out until we get a wifi connection before the httpd server is started.
wait_for_wifi_conn ( )

-- Create the httpd server
svr = net.createServer (net.TCP, 30)

-- Server listening on port 80, call connect function if a request is received
svr:listen (80, connect)