Skip to content

Commit c7edfa6

Browse files
authored
Merge pull request #3 from gurgleapps/esp
ESP support and new features like file list and ip blinking. Minimise memory usage.
2 parents 2f447c9 + f4bdd6a commit c7edfa6

File tree

7 files changed

+272
-673
lines changed

7 files changed

+272
-673
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@ This repository contains code to control Raspberry Pi Pico, ESP8266, ESP32 or ot
66

77
## Features
88

9-
- Serve static web pages from your Raspberry Pi Pico
9+
- Serve static and dynamic web pages from your Raspberry Pi Pico
1010
- Run Python functions on your microcontroller device from a web browser
1111
- Create dynamic web pages with live data from your Pico or other Microcontroller
12+
- Blink the IP address using the built-in LED, handy when you're out in the field with no screen or computer
13+
- Display a file and folder list of your root directory with an attractive and responsive user interface
14+
- End-to-end examples showcasing various functionalities
15+
- Easily customizable codebase
1216

1317
## Setup
1418

1519
1. Make sure you have MicroPython on your Pico
1620
2. Clone this repository
1721
3. Copy the code to your Pico
18-
4. Edit `config.py` with your Wi-Fi details
22+
4. Edit `config.py` with your Wi-Fi details and IP blink options
1923
5. Run `main.py` and look for the IP address of your web server
24+
6. Point your browsers to http://<YOUR_IP>
2025

2126
## Documentation
2227

config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
WIFI_SSID = "your_wifi_ssid"
2-
WIFI_PASSWORD = "your_wifi_password"
2+
WIFI_PASSWORD = "your_wifi_password"
3+
BLINK_IP = False
4+
BLINK_LAST_ONLY = False

gurgleapps_webserver.py

Lines changed: 140 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class GurgleAppsWebserver:
2121

2222
def __init__(self, wifi_ssid, wifi_password, port=80, timeout=20, doc_root="/www", log_level=0):
2323
print("GurgleApps.com Webserver")
24+
self.ip_address = '1.1.1.1'
2425
self.port = port
2526
self.timeout = timeout
2627
self.wifi_ssid = wifi_ssid
@@ -50,16 +51,17 @@ def __init__(self, wifi_ssid, wifi_password, port=80, timeout=20, doc_root="/www
5051
print('waiting for connection...')
5152
time.sleep(1)
5253

53-
#if self.wlan.status() != 3:
54+
# if self.wlan.status() != 3:
5455
if self.wlan.isconnected() == False:
5556
raise RuntimeError('network connection failed')
5657
else:
5758
print('connected')
5859
status = self.wlan.ifconfig()
5960
print('ip = ' + status[0])
6061
self.serving = True
62+
self.ip_address = status[0]
6163
print('point your browser to http://', status[0])
62-
#asyncio.new_event_loop()
64+
# asyncio.new_event_loop()
6365
print("exit constructor")
6466

6567
# async def start_server(self):
@@ -96,7 +98,7 @@ async def serve_request(self, reader, writer):
9698
post_data = None
9799
while True:
98100
line = await reader.readline()
99-
#print("line: "+str(line))
101+
# print("line: "+str(line))
100102
line = line.decode('utf-8').strip()
101103
if line == "":
102104
break
@@ -109,7 +111,7 @@ async def serve_request(self, reader, writer):
109111
method = match.group(1)
110112
url = match.group(2)
111113
print(method, url)
112-
else: # regex didn't match, try splitting the request line
114+
else: # regex didn't match, try splitting the request line
113115
request_parts = request_raw.split(" ")
114116
if len(request_parts) > 1:
115117
method = request_parts[0]
@@ -144,14 +146,22 @@ async def serve_request(self, reader, writer):
144146
file_path = self.doc_root + url
145147
if self.log_level > 0:
146148
print("file_path: "+str(file_path))
147-
#if uos.stat(file_path)[6] > 0:
149+
# if uos.stat(file_path)[6] > 0:
148150
if self.file_exists(file_path):
149151
content_type = self.get_content_type(url)
150152
if self.log_level > 1:
151153
print("content_type: "+str(content_type))
152154
await response.send_file(file_path, content_type=content_type)
153155
return
154-
print("file not found")
156+
if url == "/":
157+
print("root")
158+
files_and_folders = self.list_files_and_folders(self.doc_root)
159+
await response.send_iterator(self.generate_root_page_html(files_and_folders))
160+
return
161+
html = self.generate_root_page_html(files_and_folders)
162+
await response.send(html)
163+
return
164+
print("file not found "+url)
155165
await response.send(self.html % "page not found "+url, status_code=404)
156166
if (url == "/shutdown"):
157167
self.serving = False
@@ -163,7 +173,7 @@ def dir_exists(self, filename):
163173
return (os.stat(filename)[0] & 0x4000) != 0
164174
except OSError:
165175
return False
166-
176+
167177
def file_exists(self, filename):
168178
try:
169179
return (os.stat(filename)[0] & 0x4000) == 0
@@ -220,8 +230,7 @@ def get_file_extension(self, file_path):
220230
return file_parts[-1]
221231
return ''
222232

223-
224-
def get_content_type(self,file_path):
233+
def get_content_type(self, file_path):
225234
extension = self.get_file_extension(file_path)
226235
content_type_map = {
227236
'html': 'text/html',
@@ -231,6 +240,127 @@ def get_content_type(self,file_path):
231240
'jpeg': 'image/jpeg',
232241
'png': 'image/png',
233242
'gif': 'image/gif',
234-
'ico': 'image/x-icon'
243+
'webp': 'image/webp',
244+
'ico': 'image/x-icon',
245+
'svg': 'image/svg+xml',
246+
'json': 'application/json',
247+
'xml': 'application/xml',
248+
'pdf': 'application/pdf',
249+
'zip': 'application/zip',
250+
'txt': 'text/plain',
251+
'csv': 'text/csv',
252+
'mp3': 'audio/mpeg',
253+
'mp4': 'video/mp4',
254+
'wav': 'audio/wav',
255+
'ogg': 'audio/ogg',
256+
'webm': 'video/webm',
235257
}
236258
return content_type_map.get(extension, 'text/plain')
259+
260+
# long pause for dots 4 quick blinks for zero 2 quick for a dot
261+
async def blink_ip(self, led_pin, ip=None, repeat=2, delay_between_digits=0.9, last_only=False):
262+
delay_between_repititions = 2
263+
if ip == None:
264+
ip = self.ip_address
265+
print("blink_ip: " + str(ip))
266+
267+
def blink_element(element, pin, duration=0.27):
268+
if element == '-':
269+
blinks = 9
270+
duration = 0.1
271+
elif element == '.':
272+
blinks = 2
273+
duration = 0.1
274+
elif element == 0:
275+
blinks = 4
276+
duration = 0.1
277+
else:
278+
blinks = element
279+
280+
for _ in range(blinks):
281+
pin.on()
282+
time.sleep(duration)
283+
pin.off()
284+
time.sleep(duration)
285+
286+
ip_digits_and_dots = []
287+
ip_parts = ip.split('.')
288+
if last_only:
289+
# Only blink the last part of the IP address
290+
ip_parts = [ip_parts[-1]]
291+
292+
for part in ip_parts:
293+
for digit in part:
294+
ip_digits_and_dots.append(int(digit))
295+
# Add a dot to the list to represent the separator
296+
ip_digits_and_dots.append('.')
297+
ip_digits_and_dots.pop() # Remove the last dot
298+
# Add a dash to the list to represent the end of the IP address
299+
ip_digits_and_dots.append('-')
300+
301+
for _ in range(repeat):
302+
for element in ip_digits_and_dots:
303+
blink_element(element, led_pin)
304+
await asyncio.sleep(delay_between_digits if element != '.' else 2 * delay_between_digits)
305+
await asyncio.sleep(delay_between_repititions)
306+
307+
def list_files_and_folders(self, path):
308+
entries = uos.ilistdir(path)
309+
files_and_folders = []
310+
for entry in entries:
311+
name = entry[0]
312+
mode = entry[1]
313+
if mode & 0o170000 == 0o040000: # Check if it's a directory
314+
files_and_folders.append({"name": name, "type": "directory"})
315+
elif mode & 0o170000 == 0o100000: # Check if it's a file
316+
files_and_folders.append({"name": name, "type": "file"})
317+
return files_and_folders
318+
319+
def generate_root_page_html(self, files_and_folders):
320+
yield """
321+
<!DOCTYPE html>
322+
<html>
323+
<head>
324+
<title>GurgleApps.com Webserver</title>
325+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
326+
<link href="/styles.css" rel="stylesheet">
327+
</head>
328+
<body class="bg-gray-100">
329+
"""
330+
yield """
331+
<div class="relative flex min-h-screen flex-col justify-center overflow-hidden bg-gray-50 py-6 sm:py-12">
332+
<div class="relative bg-white px-6 pb-8 pt-10 shadow-xl ring-1 ring-gray-900/5 sm:mx-auto sm:max-w-lg sm:rounded-lg sm:px-10">
333+
<div class="mx-auto max-w-md">
334+
<img src="/img/logo.svg" class="h-12 w-auto" alt="GurgleApps.com">
335+
"""
336+
yield """
337+
<div class="divide-y divide-gray-300/50">
338+
<div class="space-y-6 py-8 text-base leading-7 text-gray-600">
339+
<h1 class="text-lg font-semibold">Welcome to GurgleApps.com Webserver</h1>
340+
<h12 class="text-base font-semibold">File List:</h2>
341+
<ul class="space-y-2 mt-3">
342+
"""
343+
folder_icon_svg = """
344+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 fill-indigo-800">
345+
<path d="M19.5 21a3 3 0 003-3v-4.5a3 3 0 00-3-3h-15a3 3 0 00-3 3V18a3 3 0 003 3h15zM1.5 10.146V6a3 3 0 013-3h5.379a2.25 2.25 0 011.59.659l2.122 2.121c.14.141.331.22.53.22H19.5a3 3 0 013 3v1.146A4.483 4.483 0 0019.5 9h-15a4.483 4.483 0 00-3 1.146z" />
346+
</svg>
347+
"""
348+
file_icon_svg = """
349+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 fill-indigo-800">
350+
<path d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0016.5 9h-1.875a1.875 1.875 0 01-1.875-1.875V5.25A3.75 3.75 0 009 1.5H5.625z" />
351+
<path d="M12.971 1.816A5.23 5.23 0 0114.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 013.434 1.279 9.768 9.768 0 00-6.963-6.963z" />
352+
</svg>
353+
"""
354+
for file_or_folder in files_and_folders:
355+
icon = folder_icon_svg if file_or_folder['type'] == 'directory' else file_icon_svg
356+
yield f"<li class='border-t pt-1'><a href='/{file_or_folder['name']}' class='flex items-center font-semibold text-slate-800 hover:text-indigo-800'>{icon}<p class='ml-2'>{file_or_folder['name']}</p></a></li>"
357+
yield "</ul>"
358+
# Closing tags for the body and container div
359+
yield """
360+
</div>
361+
<div class="pt-3 text-base font-semibold leading-7">
362+
<p class="text-gray-900">More information</p><p><a href="https://gurgleapps.com/learn/projects/micropython-web-server-control-raspberry-pi-pico-projects" class="text-indigo-500 hover:text-sky-600">Project Home &rarr;</a>
363+
</p></div></div></div></div></div></body></html>
364+
"""
365+
366+

main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,23 @@ async def background_task():
8787

8888

8989
async def run():
90+
#await(server.blink_ip(led_pin=led))
91+
if config.BLINK_IP:
92+
await(server.blink_ip(led_pin = led, last_only = config.BLINK_LAST_ONLY))
93+
9094
await asyncio.gather(main(), background_task())
9195

9296
server = GurgleAppsWebserver(config.WIFI_SSID, config.WIFI_PASSWORD, port=80, timeout=20, doc_root="/www", log_level=2)
9397
server.add_function_route("/set-delay/<delay>", set_delay)
94-
server.add_function_route("/set-blink-pattern/<on_time>/<off_time>", set_blink_pattern)
98+
server.add_function_route(
99+
"/set-blink-pattern/<on_time>/<off_time>",
100+
set_blink_pattern
101+
)
95102
server.add_function_route("/stop", stop_flashing)
96103
server.add_function_route("/start", start_flashing)
97104
server.add_function_route("/status", send_status)
98105
server.add_function_route("/example/func/<param1>/<param2>", example_func)
99106
server.add_function_route("/hello/<name>", say_hello)
100107

108+
101109
asyncio.run(run())

response.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ async def send_file(self, file_path, status_code=200, content_type="text/html"):
2929
file_size = os.stat(file_path)[6]
3030
await self.send_headers(status_code, content_type, content_length=file_size)
3131

32-
chunk_size = 1024 # Adjust the chunk size as needed
32+
#chunk_size = 1024 # Adjust the chunk size as needed
33+
chunk_size = 512 # Adjust the chunk size as needed
3334
with open(file_path, "rb") as f:
3435
while True:
3536
chunk = f.read(chunk_size)
@@ -43,3 +44,11 @@ async def send_file(self, file_path, status_code=200, content_type="text/html"):
4344
except Exception as e:
4445
print("Error sending file:", e)
4546
await self.send('', status_code=404)
47+
48+
async def send_iterator(self, iterator, status_code=200, content_type="text/html"):
49+
await self.send_headers(status_code=status_code, content_type=content_type)
50+
for chunk in iterator:
51+
self.writer.write(chunk)
52+
await self.writer.drain()
53+
await self.writer.wait_closed()
54+

0 commit comments

Comments
 (0)