Using the OLinuXino Micro and a USB 3G modem to control something via web

OLinuXino with a USB 3G modem

OlinuXino Micro with a USB 3G modem

First you need a working internet connection on the OLinuXino, either via WiFi or Ethernet to install some tools. In my previous blog post there is an example how to configure a kernel with WiFi and USB 3G modem support and how to set up an internet connection on the OLinuXino.

Update: The default Arch Linux ARM image now has proper WiFi and USB 3G modem support (A new SD card image for the iMX233-OLinuXino), building your own kernel is not longer necessary.

Set up an internet connection

Connect an USB WiFi adapter and set up a network connection:

[root@alarm ~]#
[  858.720000] usb 1-1: new high-speed USB device number 3 using ci_hdrc
[  858.880000] usb 1-1: ath9k_htc: Firmware htc_7010.fw requested
[  859.200000] usb 1-1: ath9k_htc: Transferred FW: htc_7010.fw, size: 72992
[  859.260000] ath9k_htc 1-1:1.0: ath9k_htc: HTC initialized with 45 credits
[  859.680000] ath9k_htc 1-1:1.0: ath9k_htc: FW Version: 1.3
[  859.760000] ieee80211 phy0: Atheros AR9287 Rev:2

[root@alarm ~]# ifconfig wlan0 down
[root@alarm ~]# iwconfig wlan0 mode ad-hoc
[root@alarm ~]# iwconfig wlan0 channel 4
[root@alarm ~]# iwconfig wlan0 essid tuxnet
[root@alarm ~]# iwconfig wlan0 key 73598253812539275395295235
[root@alarm ~]# ifconfig wlan0 up
[ 1143.570000] wlan0: Selected IBSS BSSID a2:1c:4f:5c:ef:c4 based on configured SSID
[root@alarm ~]# dhcpcd wlan0
dhcpcd[256]: version 5.5.6 starting
dhcpcd[256]: all: not configured to accept IPv6 RAs
dhcpcd[256]: wlan0: rebinding lease of 10.42.0.48
dhcpcd[256]: wlan0: broadcasting for a lease
dhcpcd[256]: wlan0: offered 10.42.0.48 from 10.42.0.1
dhcpcd[256]: wlan0: acknowledged 10.42.0.48 from 10.42.0.1
dhcpcd[256]: wlan0: checking for 10.42.0.48
dhcpcd[256]: wlan0: leased 10.42.0.48 for 3600 seconds
dhcpcd[256]: forked to background, child pid 282
[root@alarm ~]# ping google.de
PING google.de (74.125.230.248) 56(84) bytes of data.
64 bytes from par08s10-in-f24.1e100.net (74.125.230.248): icmp_req=1 ttl=53 time=385 ms
64 bytes from par08s10-in-f24.1e100.net (74.125.230.248): icmp_req=2 ttl=53 time=314 ms
64 bytes from par08s10-in-f24.1e100.net (74.125.230.248): icmp_req=3 ttl=53 time=333 ms

--- google.de ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 314.719/344.448/385.125/29.767 ms
[root@alarm ~]#

Install usb_modeswitch and wvdial

We use the pacman package manager to install some tools:

[root@alarm ~]# pacman -Sy
:: Synchronizing package databases...
core                      38.3 KiB  89.5K/s 00:00 [######################] 100%
extra                    434.8 KiB  7.17K/s 01:01 [######################] 100%
community                438.6 KiB  7.35K/s 01:00 [######################] 100%
alarm                      4.9 KiB   166K/s 00:00 [######################] 100%
aur                       12.2 KiB  71.6K/s 00:00 [######################] 100%
[root@alarm ~]# pacman -S usb_modeswitch wvdial vim
resolving dependencies...
looking for inter-conflicts...

Targets (5): tcl-8.5.13-1  wvstreams-4.6.1-5  xplc-0.3.13-4
            usb_modeswitch-1.2.5-1  wvdial-1.61-4

Total Download Size:    0.75 MiB
Total Installed Size:   15.04 MiB

Proceed with installation? [Y/n] Y
:: Retrieving packages from community...
wvstreams-4.6.1-5-arm   1032.6 KiB  8.64K/s 02:00 [######################] 100%
wvdial-1.61-4-arm         57.2 KiB  8.47K/s 00:07 [######################] 100%
(5/5) checking package integrity                   [######################] 100%
(5/5) loading package files                        [######################] 100%
(5/5) checking for file conflicts                  [######################] 100%
(5/5) checking available disk space                [######################] 100%
(1/5) installing tcl                               [######################] 100%
(2/5) installing usb_modeswitch                    [######################] 100%
(3/5) installing xplc                              [######################] 100%
(4/5) installing wvstreams                         [######################] 100%
(5/5) installing wvdial                            [######################] 100%
[root@alarm ~]#

Note

I have also updated all packages pacman -Syu and enabled systemd as default init system but this is not necessary to run the webapp, there are some manual changes necessary, see also https://wiki.archlinux.org/index.php/Systemd. You can check if systemd is currently active by using the following command: cat /proc/1/comm. This should return the string systemd and $ cat /proc/cmdline should return noinitrd console=ttyAMA0,115200 root=/dev/mmcblk0p2 init=/usr/lib/systemd/systemd rw rootwait ssp1=mmc.

To get rid of some warnings:

HOSTNAME= is deprecated. See rc.conf(5) and hostname(5) for details.
HARDWARECLOCK= is deprecated. See rc.conf(5) and hwclock(8) for details.
TIMEZONE= is deprecated. See rc.conf(5) for details.

Comment out some lines in /etc/rc.conf:

#LOCALE="en_US.UTF-8"
#HARDWARECLOCK="UTC"
#TIMEZONE="America/Chicago"
#KEYMAP="us"
#HOSTNAME="alarm"

And execute the following commands:

[root@alarm ~]# echo LANG=en_US.UTF-8 > /etc/locale.conf
[root@alarm ~]# echo Europe/Berlin > /etc/timezone
[root@alarm ~]# rm /etc/localtime
[root@alarm ~]# ln -s /usr/share/zoneinfo/Europe/Berlin /etc/localtime
[root@alarm ~]# echo KEYMAP=us > /etc/vconsole.conf

Connect and configure the USB 3G modem

How to setup a USB 3G Modem under Arch Linux is explained in the ArchWiki: https://wiki.archlinux.org/index.php/USB_3G_Modem.

Now disable WiFi, disconnect your WiFi adapter and plug in the 3G modem:

[root@alarm ~]# ifconfig wlan0 down
[root@alarm ~]#
[ 2776.880000] usb 1-1: USB disconnect, device number 3
[ 2776.960000] usb 1-1: ath9k_htc: USB layer deinitialized
[  162.480000] usb 1-1: new high-speed USB device number 2 using ci_hdrc
[  162.660000] scsi0 : usb-storage 1-1:1.0
[  163.700000] scsi 0:0:0:0: CD-ROM            USBModem Disk             2.31 PQ: 0 ANSI: 2
[  165.490000] usb 1-1: USB disconnect, device number 2
[  165.890000] usb 1-1: new high-speed USB device number 3 using ci_hdrc
[  166.070000] option 1-1:1.0: GSM modem (1-port) converter detected
[  166.090000] usb 1-1: GSM modem (1-port) converter now attached to ttyUSB0
[  166.100000] option 1-1:1.1: GSM modem (1-port) converter detected
[  166.120000] usb 1-1: GSM modem (1-port) converter now attached to ttyUSB1
[  166.130000] option 1-1:1.2: GSM modem (1-port) converter detected
[  166.150000] usb 1-1: GSM modem (1-port) converter now attached to ttyUSB2
[  166.160000] scsi1 : usb-storage 1-1:1.3
[  167.170000] scsi 1:0:0:0: Direct-Access     USBModem Disk             2.31 PQ: 0 ANSI: 2
[  167.400000] sd 1:0:0:0: [sda] Attached SCSI removable disk
[root@alarm ~]# lsusb
Bus 001 Device 005: ID 1c9e:9603 OMEGA TECHNOLOGY
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

We use wvdial to connect via UMTS to the internet.

You need to create the configuration file /etc/wvdial.conf, just google for wvdial.conf and the name of your USB 3G modem and you will likely find a working configuration, mine looks like this:

[root@alarm ~]# vim /etc/wvdial.conf
[root@alarm ~]# cat /etc/wvdial.conf
[Dialer Defaults]
Phone = *99#
Username = "blau"
Password = "blau"
Stupid Mode = on
Dial Command = ATDT
Modem Type = USB Modem
Modem = /dev/ttyUSB2
Init1 = ATZ
[Dialer nopin]
Init2= AT+CPIN="1234"
Init3= AT+CLCK="SC",0,"1234"
[Dialer umts]
Carrier Check = on
Init2= AT+CGDCONT?
Init3= AT+CGDCONT=1,"IP","internet.eplus.de"
ISDN = off
[root@alarm ~]#

Once the configuration files are prepared, the internet connection is established by running:

[root@alarm ~]# wvdial umts &
[1] 304
--> WvDial: Internet dialer version 1.61
--> Initializing modem.
--> Sending: ATZ
ATZ
OK
--> Sending: AT+CGDCONT=1,"IP","internet.eplus.de"
AT+CGDCONT=1,"IP","internet.eplus.de"
OK
--> Modem initialized.
--> Sending: ATDT*99#
--> Waiting for carrier.
ATDT*99#
CONNECT 7200000
--> Carrier detected.  Starting PPP immediately.
--> Starting pppd at Wed Dec 31 18:34:43 1969
--> Pid of pppd: 305
--> Using interface ppp0
--> pppd: ??6
--> pppd: ??6
--> pppd: ??6
--> pppd: ??6
--> pppd: ??6
--> pppd: ??6
--> pppd: ??6
--> local  IP address 10.120.143.161
--> pppd: ??6
--> remote IP address 10.64.64.64
--> pppd: ??6
--> primary   DNS address 212.23.115.148
--> pppd: ??6
--> secondary DNS address 212.23.97.2
--> pppd: ??6

[root@alarm ~]# ping google.de
PING google.de (74.125.230.248) 56(84) bytes of data.
64 bytes from par08s10-in-f24.1e100.net (74.125.230.248): icmp_req=1 ttl=54 time=363 ms
64 bytes from par08s10-in-f24.1e100.net (74.125.230.248): icmp_req=2 ttl=54 time=332 ms
64 bytes from par08s10-in-f24.1e100.net (74.125.230.248): icmp_req=3 ttl=54 time=312 ms
64 bytes from par08s10-in-f24.1e100.net (74.125.230.248): icmp_req=4 ttl=54 time=332 ms

--- google.de ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3001ms
rtt min/avg/max/mdev = 312.688/335.242/363.438/18.173 ms
[root@alarm ~]#

Sometimes it could be necessary to manually add a nameserver to /etc/resolf.conf:

[root@alarm ~]# echo 'nameserver 208.67.222.222' | tee -a /etc/resolv.conf

Get a public IP address with PageKite

The main problem with mobile internet is that if you connect through a 3G USB dongle you don’t have a public IP address but one behind a NAT gateway that is not accessible from the internet.

So theoretically you can’t run a web server via UMTS but luckily there exists a little Open Source Python script called PageKite to overcome this problem.

Install PageKite on the OLinuXino

The QuickStart Guide: https://pagekite.net/support/quickstart/

[root@alarm ~]# wget http://pagekite.net/pk/pagekite.py
--1969-12-31 19:28:26--  http://pagekite.net/pk/pagekite.py
Resolving pagekite.net (pagekite.net)... 69.164.211.158
Connecting to pagekite.net (pagekite.net)|69.164.211.158|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 160192 (156K) [text/x-python]
Saving to: `pagekite.py'

100%[======================================>] 160,192     11.2K/s   in 14s

1969-12-31 19:28:43 (11.2 KB/s) - `pagekite.py' saved [160192/160192]

[root@alarm ~]# python pagekite.py --signup
-bash: python: command not found
[root@alarm ~]# pacman -S python
resolving dependencies...
looking for inter-conflicts...

Targets (1): python-3.3.0-2

Total Download Size:    11.39 MiB
Total Installed Size:   90.20 MiB

Proceed with installation? [Y/n] y
:: Retrieving packages from extra...
python-3.3.0-2-arm        11.4 MiB  6.76K/s 28:47 [######################] 100%
(1/1) checking package integrity                   [######################] 100%
(1/1) loading package files                        [######################] 100%
(1/1) checking for file conflicts                  [######################] 100%
(1/1) checking available disk space                [######################] 100%
(1/1) installing python                            [######################] 100%
Optional dependencies for python
    tk: for tkinter
    sqlite
[root@alarm ~]#
[root@alarm ~]# python pagekite.py --signup
  File "pagekite.py", line 242
    exec __FILES[".SELF/sockschain/__init__.py"] in sys.modules["sockschain"].__dict__
              ^
SyntaxError: invalid syntax
[root@alarm ~]# # fuck we need python2
[root@alarm ~]# pacman -S python2
resolving dependencies...
looking for inter-conflicts...

Targets (2): sqlite-3.7.14.1-1  python2-2.7.3-3

Total Download Size:    8.61 MiB
Total Installed Size:   68.77 MiB

Proceed with installation? [Y/n] y
:: Retrieving packages from extra...
python2-2.7.3-3-arm        8.6 MiB  6.78K/s 21:41 [######################] 100%
(2/2) checking package integrity                   [######################] 100%
(2/2) loading package files                        [######################] 100%
(2/2) checking for file conflicts                  [######################] 100%
(2/2) checking available disk space                [######################] 100%
(1/2) installing sqlite                            [######################] 100%
(2/2) installing python2                           [######################] 100%
Optional dependencies for python2
    tk: for IDLE
[root@alarm ~]# python2 pagekite.py --signup
[root@alarm ~]# python2 pagekite.py 80 olinuxino.pagekite.me
>>> Hello! This is pagekite.py v0.5.4a.                         [CTRL+C = Stop]
!!! Failed to connect to 178.79.140.143:443
!!! Failed to connect to 69.164.211.158:443
!!! Failed to connect to 93.95.226.149:443
<< pagekite.py [down]     Not connected to any front-ends, will retry...

It seems there is a problem with openssl on the OLinuXino:

[root@alarm ~]# curl -s https://pagekite.net/pk
[root@alarm ~]# curl  http://pagekite.net/pk
exec curl -s -L -k https://pagekite.net/pk/ |bash #
[root@alarm ~]# curl -v -s https://pagekite.net/pk
* About to connect() to pagekite.net port 443 (#0)
*   Trying 69.164.211.158...
* connected
* Connected to pagekite.net (69.164.211.158) port 443 (#0)
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: none
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS alert, Server hello (2):
* SSL certificate problem: certificate is not yet valid
* Closing connection #0
[root@alarm ~]#

And there is the problem: certificate is not yet valid. Not yet?

We nee to set the correct date: https://wiki.archlinux.org/index.php/Network_Time_Protocol_daemon.

Update

It seems Arch Linux ARM installs now openntpd instead of ntp by default, use man ntdp to find the correct commandline option.

[root@alarm ~]# pacman -S ntp
resolving dependencies...
looking for inter-conflicts...

Targets (1): ntp-4.2.6.p5-12

Total Download Size:    0.39 MiB
Total Installed Size:   1.49 MiB

Proceed with installation? [Y/n] y
:: Retrieving packages from extra...
ntp-4.2.6.p5-12-arm      395.2 KiB  7.90K/s 00:50 [######################] 100%
(1/1) checking package integrity                   [######################] 100%
(1/1) loading package files                        [######################] 100%
(1/1) checking for file conflicts                  [######################] 100%
(1/1) checking available disk space                [######################] 100%
(1/1) installing ntp                               [######################] 100%
[root@alarm ~]# systemctl enable ntpd.service
ln -s '/usr/lib/systemd/system/ntpd.service' '/etc/systemd/system/multi-user.target.wants/ntpd.service'
[root@alarm ~]# ntpd -qg
ntpd: time set +1355695847.651911s
[root@alarm ~]# systemctl start ntpd.service
[root@alarm ~]# curl -s https://pagekite.net/pk
exec curl -s -L -k https://pagekite.net/pk/ |bash #
[root@alarm ~]#

Test if the OLinuXino is accessible via web

[root@alarm ~]# mkdir www
[root@alarm ~]# vim www/index.html
[root@alarm ~]# cat www/index.html
Hello World!
[root@alarm ~]# python2 pagekite.py www olinuxino.pagekite.me
>>> Hello! This is pagekite.py v0.5.4a.                         [CTRL+C = Stop]
    Connecting to front-end 69.164.211.158:443 ...
    - Protocols: http http2 http3 https websocket irc finger httpfinger raw
    - Ports: 79 80 443 843 2222 3000 4545 5222 5223 5269 5670 6667 8000 8080
    - Ports: 8081 9292
    - Raw ports: 22 virtual
    Quota: You have 2560.00 MB, 31 days and 5 connections left.
~<> Flying builtin HTTPD as https://olinuxino.pagekite.me/
    - https://olinuxino.pagekite.me/
    46.115.59.235 < http://olinuxino.pagekite.me:80 (builtin)
<< pagekite.py [flying]   Kites are flying and all is well.
PageKite Hello World

PageKite Hello World

SSH into the OLinuXino from everywhere

Warning: Make sure to change the default root password.

[root@alarm ~]# passwd
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
[root@alarm ~]#

Start a simple www server for the www directory and also redirect port 22 for incoming ssh connections:

[root@alarm ~]# python2 pagekite.py www olinuxino.pagekite.me AND 22 ssh:olinuxino.pagekite.me
>>> Hello! This is pagekite.py v0.5.4a.                         [CTRL+C = Stop]
    Connecting to front-end 178.79.140.143:443 ...
    - Protocols: http http2 http3 https websocket irc finger httpfinger raw
    - Ports: 79 80 443 843 2222 3000 4545 5222 5223 5269 5670 6667 8000 8080
    - Ports: 8081 9292
    - Raw ports: 22 virtual
    Quota: You have 2560.00 MB, 31 days and 5 connections left.
~<> Flying localhost:22 as ssh://olinuxino.pagekite.me:22/ (HTTP proxied)
~<> Flying builtin HTTPD as https://olinuxino.pagekite.me/
    - https://olinuxino.pagekite.me/
    Connecting to front-end 69.164.211.158:443 ...
<< pagekite.py [flying]   Kites are flying and all is well.
[root@alarm ~]#
Control GPIO pins over SSH

Control GPIO pins over SSH

If you get the error ssh_exchange_identification: Connection closed by remote host, then the solution as written here and there is to edit your ~/.ssh/config on your PC and add the following:

Host *.pagekite.me
  CheckHostIP no
  ProxyCommand /bin/nc -X connect -x %h:443 %h %p

A simple Python webapp to control some LEDs

Used software

Install everything:

[root@alarm ~]# pacman -S python2-flask python2-pip git
[root@alarm ~]# pip2 install flask-bootstrap
[root@alarm ~]# git clone https://github.com/christianjann/olinuxino-webcontrol-minimal.git webcontrol
Cloning into 'webcontrol'...
remote: Counting objects: 12, done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 12 (delta 2), reused 12 (delta 2)
Unpacking objects: 100% (12/12), done.
[root@alarm ~]#

Test it

Try some functions on the interactive python command prompt:

[root@alarm ~]# cd webcontrol
[root@alarm webcontrol]# python2
Python 2.7.3 (default, Dec 14 2012, 13:39:40)
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from leds import *
>>> ledon()
>>> ledoff()
>>> ledon()
>>> ledheartbeat()
>>> export_pins(32)
>>> setpindirection(32, "out");
>>> writepins(32, 1)
>>> writepins(32, 0)
>>>
[root@alarm webcontrol]#

Start the webapp

With the help of GNU Screen ([root@alarm ~]# man screen) you can open several terminals at once on the OLinuXino:

[chris@thinkpad ~]$ screen /dev/ttyUSB0 115200
[root@alarm ~]# pacman -S screen
[root@alarm ~]# screen

Now you can use the following key combinations to create new virtual terminals and switch between them:

  • “Ctrl-a” “a” “c” for a new virtual terminal
  • “Ctrl-a” “a” “n” for the next window
  • “Ctrl-a” “a” “p” for the previous window
  • “Ctrl-a” “a” “k” Kill, destroy the current window.
  • “Ctrl-a” “d” to detach screen from the current terminal, screen -ls to list terminal sessions and screen -r to attach to one of them.

Virtual terminal 1: wvdial

[root@alarm ~]# wvdial umts
--> WvDial: Internet dialer version 1.61
--> Initializing modem.
--> Sending: ATZ
ATZ
OK
--> Sending: AT+CGDCONT=1,"IP","internet.eplus.de"
AT+CGDCONT=1,"IP","internet.eplus.de"
OK
--> Modem initialized.
--> Sending: ATDT*99#
--> Waiting for carrier.
ATDT*99#
CONNECT 7200000
--> Carrier detected.  Starting PPP immediately.
--> Starting pppd at Wed Dec 31 19:03:37 1969
--> Pid of pppd: 780
--> Using interface ppp0
--> pppd: ?i?[01]
--> pppd: ?i?[01]
--> pppd: ?i?[01]
--> pppd: ?i?[01]
--> pppd: ?i?[01]
--> pppd: ?i?[01]
--> pppd: ?i?[01]
--> local  IP address 10.113.211.24
--> pppd: ?i?[01]
--> remote IP address 10.64.64.64
--> pppd: ?i?[01]
--> primary   DNS address 212.23.115.148
--> pppd: ?i?[01]
--> secondary DNS address 212.23.97.2
--> pppd: ?i?[01]

Virtual terminal 2: app.py

[root@alarm ~]# cd webcontrol
[root@alarm webcontrol]# python2 app.py
The value on the PIN 32 is : 0

The value on the PIN 33 is : 0

* Running on http://127.0.0.1:5000/
* Restarting with reloader
GPIO 32 already Exists, so skipping export gpio
GPIO 33 already Exists, so skipping export gpio
The value on the PIN 32 is : 0

The value on the PIN 33 is : 0

127.0.0.1 - - [17/Dec/2012 04:46:42] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [17/Dec/2012 04:46:50] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [17/Dec/2012 04:47:43] "POST / HTTP/1.1" 200 -
127.0.0.1 - - [17/Dec/2012 04:47:44] "GET /favicon.ico HTTP/1.1" 404 -

Virtual terminal 3: pagekite

[root@alarm ~]# python2 pagekite.py 5000 olinuxino.pagekite.me:80
>>> Hello! This is pagekite.py v0.5.4a.                         [CTRL+C = Stop]
!!! Failed to connect to 178.79.140.143:443
!!! Failed to connect to 93.95.226.149:443
<< pagekite.py [down]     Not connected to any front-ends, will retry...
[root@alarm ~]# ntpd -qg
ntpd: time set +1355736876.515633s
[root@alarm ~]# python2 pagekite.py 5000 olinuxino.pagekite.me:80
>>> Hello! This is pagekite.py v0.5.4a.                         [CTRL+C = Stop]
    Connecting to front-end 178.79.140.143:443 ...
    - Protocols: http http2 http3 https websocket irc finger httpfinger raw
    - Ports: 79 80 443 843 2222 3000 4545 5222 5223 5269 5670 6667 8000 8080
    - Ports: 8081 9292
    - Raw ports: 22 virtual
    Quota: You have 2559.67 MB, 30 days and 5 connections left.
    Connecting to front-end 93.95.226.149:443 ...
~<> Flying localhost:5000 as https://olinuxino.pagekite.me:80/
<< pagekite.py [flying]   Kites are flying and all is well.
Screenshot olinuxino.pagekite.me

Screenshot olinuxino.pagekite.me

Screenshot olinuxino.pagekite.me under Android

Screenshot olinuxino.pagekite.me under Android

The Source Code

app.py:

#!/usr/bin/env python
# coding=utf8

from flask import Flask, render_template, flash, request, redirect, url_for
from flask.ext.bootstrap import Bootstrap
from leds import *

# init gpio's
export_pins(32)
setpindirection(32, "out")
export_pins(33)
setpindirection(33, "out")

app = Flask(__name__)
Bootstrap(app)

app.config['BOOTSTRAP_USE_MINIFIED'] = True
app.config['BOOTSTRAP_USE_CDN'] = True
app.config['BOOTSTRAP_FONTAWESOME'] = True
app.config['SECRET_KEY'] = 'devkey'


@app.route('/')
def index():
    in_out = {'led1': bool(readpins(32)), 'led2': bool(readpins(33))}
    return render_template('example.html', in_out=in_out)


@app.route('/set_leds', methods=('GET', 'POST',))
def set_leds():
    if request.method == 'POST':
        led1 = bool(request.form.get('led1'))
        led2 = bool(request.form.get('led2'))
        print("led1: ", led1)
        print("led2: ", led2)
        flash('LEDs have been updated: LED1="' + str(led1)
              + '", LED2="' + str(led2) + '"', 'success')
        writepins(32, int(led1))
        writepins(33, int(led2))
    return redirect(url_for('index'))

if __name__ == '__main__':
    # '0.0.0.0': listen on all public IPs
    app.run(host='0.0.0.0', port=5000, debug=True)

leds.py:

#!/usr/bin/env python
# coding=utf8


def ledon():
    try:
        value = open("/sys/class/leds/green/brightness", "w")
        value.write(str(1))
        value.close()
    except:
        print("ERR: ledon()")


def ledoff():
    try:
        value = open("/sys/class/leds/green/brightness", "w")
        value.write(str(0))
        value.close()
    except:
        print("ERR: ledoff()")


def ledheartbeat():
    try:
        value = open("/sys/class/leds/green/trigger", "w")
        value.write("heartbeat")
        value.close()
    except:
        print("ERR: ledheartbeat()")


def export_pins(pins):
    try:
        f = open("/sys/class/gpio/export", "w")
        f.write(str(pins))
        f.close()
    except IOError:
        print(
            "GPIO %s already Exists, so skipping export gpio" % (str(pins), ))
    except:
        print("ERR: export_pins()")


def unexport_pins(pins):
    try:
        f = open("/sys/class/gpio/unexport", "w")
        f.write(str(pins))
        f.close()
    except IOError:
        print(
            "GPIO %s is not found, so skipping unexport gpio" % (str(pins), ))
    except:
        print("ERR: unexport_pins()")


def setpindirection(pin_no, pin_direction):
    try:
        gpiopin = "gpio%s" % (str(pin_no), )
        pin = open("/sys/class/gpio/" + gpiopin + "/direction", "w")
        pin.write(pin_direction)
        pin.close()
    except:
        print("ERR: setpindirection()")


def writepins(pin_no, pin_value):
    try:
        gpiopin = "gpio%s" % (str(pin_no), )
        pin = open("/sys/class/gpio/" + gpiopin + "/value", "w")
        if pin_value == 1:
            pin.write("1")
        else:
            pin.write("0")
        pin.close()
    except:
        print("ERR: writepins()")


def readpins(pin_no):
    try:
        gpiopin = "gpio%s" % (str(pin_no), )
        pin = open("/sys/class/gpio/" + gpiopin + "/value", "r")
        value = pin.read()
        print("The value on the PIN %s is : %s" % (str(pin_no), str(value)))
        pin.close()
        return int(value)
    except:
        print("ERR: readpins()")

if __name__ == '__main__':
    from time import sleep
    export_pins(32)
    setpindirection(32, "out")

    while(1):
        ledon()
        writepins(32, 1)
        sleep(1)
        ledoff()
        writepins(32, 0)
        sleep(1)

example.html:

Based on https://github.com/twbs/bootstrap/tree/master/docs/examples/jumbotron and https://github.com/mbr/flask-bootstrap/tree/master/sample_application/templates.

{% extends "bootstrap/base.html" %}

{% macro check_box(name, text, value='', checked=False) -%}
  {% if checked %}
    <label class="checkbox"><input id="{{ name }}"
           name="{{ name }}" type="checkbox"
           value="{{ value|e}}" checked="checked"/>{{ text }}</label>
  {% else %}
    <label class="checkbox"><input id="{{ name }}"
           name="{{ name }}" type="checkbox"
           value="{{ value|e}}" />{{ text }}</label>
  {% endif %}
{%- endmacro %}


{% block content %}
<div class="navbar navbar-inverse navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle"
                  data-toggle="collapse" data-target=".navbar-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="#">Project name</a>
        </div>
        <div class="navbar-collapse collapse">
          <ul class="nav navbar-nav">
            <li class="active"><a href="#">Home</a></li>
            <li><a href="#about">About</a></li>
            <li><a href="#contact">Contact</a></li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </div>

    <!-- Main jumbotron for a primary marketing message or call to action -->
    <div class="jumbotron">
      <div class="container">
        <h1>Hello, world!</h1>
        <p>This is a simple webapp running on the OLinuXino.
        Use it as a starting point to create something more unique.</p>
        <p><a href="http://www.jann.cc/2012/12/16/http://www.jann.cc/2012/12/16/olinuxino_micro_usb_3g_modem_web_control.html" class="btn btn-primary btn-lg" role="button">Learn more &raquo;</a></p>
      </div>
    </div>

    <div class="container">
      <!-- Example row of columns -->
      <div class="row">
        <div class="col-md-4">
          <h2>LEDs</h2>
          <form class="form form-horizontal" method="post"
                action="{{ url_for('set_leds') }}">
            <p>
            {{ check_box('led1', 'LED 1', value='1', checked=in_out['led1']) }}
            {{ check_box('led2', 'LED 2', value='1', checked=in_out['led2']) }}
            </p>
            <p><button name="action_save" type="submit"
                       class="btn btn-primary">Send new LED state</button></p>
          </form>
        </div>
        <div class="col-md-4">
          <h2>Heading</h2>
           <p>Donec id elit non mi porta gravida at eget metus.</p>
           <p><a class="btn btn-default" href="#" role="button">
           View details &raquo;
           </a></p>
       </div>
        <div class="col-md-4">
          <h2>Heading</h2>
          <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in,
          egestas eget quam.</p>
          <p><a class="btn btn-default" href="#" role="button">
          View details &raquo;</a></p>
        </div>
      </div>

       <div class="row">
        <div class="col-md-12">

        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
            {% for category, message in messages %}
                <div class="alert alert-{{ category }} fade in">
                <a href="#" data-dismiss="alert" class="close">×</a>
                {{ message }}
                </div>
            {% endfor %}
            {% endif %}
        {% endwith %}

        </div>
      </div>

      <hr>

      <footer>
      <p>&copy; 2013 <a href="http://www.jann.cc">jann.cc</a></p>
      </footer>
    </div> <!-- /container -->

{% endblock %}

Now it’s up to you, put everything in a script and add it to /etc/rc.local so that it runs automatically at boot.