diff --git a/examples/demo-webserver-remote-control/README.md b/examples/demo-webserver-remote-control/README.md new file mode 100644 index 0000000..f0e78a4 --- /dev/null +++ b/examples/demo-webserver-remote-control/README.md @@ -0,0 +1,96 @@ +# arol-alvik-demos + +Demos for [Arduino Alvik](https://www.arduino.cc/education/arduino-alvik/) developed by [AROL Closure Systems](https://www.arol.com/). + +# Instructions + +## 1 - Download IDE + +[Download IDE](https://labs.arduino.cc/en/labs/micropython) + +Arduino labs for Micropython + +Unzip the compressed folder + +Run the executable + +[Either clone or download this repository as zip](https://github.com/arolgroup/arol-alvik-demos/archive/refs/heads/main.zip) + +Unzip this repository + +## 2 - Connect to the Alvik + +Power on the Alvik with its switch + +Connect to the Alvik with a USB C cable + +![](/images/1-connect-usb-c.jpg) + +## 3 - Transfer demo files to the Alvik + +Click on the file icon + +Click on the folder path on the right pane and navigate to the folder with the files in this repository + +![](/images/3-transfer-files.JPG) + +One at a time, select each of the following files and with the left arrow transfer them to the Alvik +- main.py : automatically executed by the robot at startup +- wifi_host.py : demo implementation of a wifi webserver hosted by the Alvik +- page_arrow_control.html : web page to be displayed in the device used to control the Alvik. Can be either a phone or a computer + +## 4 - Rename the robot + +Double click on "wifi_host.py" on the left pane (inside the robot) and edit the name of the wifi network to the desired name of your robot. + +It should be a unique name as not to interfere with other Alvik in the same room or with other networks. I advics calling it "Alvik-myname" for ease + +Save the file, it will take a few seconds. On the black console down it should show repeated a number of times. When it stops, save is complete. + +![](/images/4-rename.JPG) + +## 5 - run applications - reboot the robot + +There is a button near the USB-C port on the alvik. Pressing it will reboot the robot. The robot will always play main.py when rebooted. + +You can also run application by using the play button on an opened file in the IDE. + +The black console will show the "print" strings when connected to the robot. + +You will need to reconnect on the IDE. + +## 6 - Connect to the robot via wifi + +Wait a few seconds after reboot + +In the list of wifi networks now your chosen robot name will show up in the list of networks + +Connect to the wifi network, you should get a prompt for a sign in page a after a few seconds, or be redirected to the html page hosted by the robot + +Pushing the arrow buttons will now move the robot, this does NOT require the USB-C cable plugged in + +![](/images/5-connect-wifi-phone.jpg) + +![](/images/5-connect-wifi-pc.JPG) + +## 7 Enjoy the wifi remote controls wia webpage hosted by the Alvik! + +BUG: Samsung and Apple devices will not redirect automatically. To control the robot, open a web browser with wifi conencted to the Alvik, and go to the web page "192.168.4.1" + +https://github.com/user-attachments/assets/10da174e-641d-4e57-8ab0-77a9fef9af31 + +## 8 - Stop the webserver + +Clicking on the red button in the web page will stop the web server and show a disconnection page. The wifi will disconnect. + +To restart the application you need to restart the robot, or need to play from the IDE. + +This helps you in uploading new code to the robot without needing to reset, as it stops the wifi radio and threads properly. + +## Copyright and license + +Copyright (C) 2024-2025, [AROL Closure Systems](https://www.arol.com/), all rights reserved. + +The source code contained in this repository and the executable distributions are licensed under the terms of the MIT license as detailed in the [LICENSE](LICENSE) file. + + diff --git a/examples/demo-webserver-remote-control/images/1-connect-usb-c.jpg b/examples/demo-webserver-remote-control/images/1-connect-usb-c.jpg new file mode 100644 index 0000000..57f6c65 Binary files /dev/null and b/examples/demo-webserver-remote-control/images/1-connect-usb-c.jpg differ diff --git a/examples/demo-webserver-remote-control/images/2-connect-ide.JPG b/examples/demo-webserver-remote-control/images/2-connect-ide.JPG new file mode 100644 index 0000000..9e6c491 Binary files /dev/null and b/examples/demo-webserver-remote-control/images/2-connect-ide.JPG differ diff --git a/examples/demo-webserver-remote-control/images/3-transfer-files.JPG b/examples/demo-webserver-remote-control/images/3-transfer-files.JPG new file mode 100644 index 0000000..c9eff3e Binary files /dev/null and b/examples/demo-webserver-remote-control/images/3-transfer-files.JPG differ diff --git a/examples/demo-webserver-remote-control/images/4-rename.JPG b/examples/demo-webserver-remote-control/images/4-rename.JPG new file mode 100644 index 0000000..7b5e916 Binary files /dev/null and b/examples/demo-webserver-remote-control/images/4-rename.JPG differ diff --git a/examples/demo-webserver-remote-control/images/5-connect-wifi-pc.JPG b/examples/demo-webserver-remote-control/images/5-connect-wifi-pc.JPG new file mode 100644 index 0000000..44bd764 Binary files /dev/null and b/examples/demo-webserver-remote-control/images/5-connect-wifi-pc.JPG differ diff --git a/examples/demo-webserver-remote-control/images/5-connect-wifi-phone.jpg b/examples/demo-webserver-remote-control/images/5-connect-wifi-phone.jpg new file mode 100644 index 0000000..4efd1b7 Binary files /dev/null and b/examples/demo-webserver-remote-control/images/5-connect-wifi-phone.jpg differ diff --git a/examples/demo-webserver-remote-control/images/6-move-robot.mp4 b/examples/demo-webserver-remote-control/images/6-move-robot.mp4 new file mode 100644 index 0000000..b9199ad Binary files /dev/null and b/examples/demo-webserver-remote-control/images/6-move-robot.mp4 differ diff --git a/examples/demo-webserver-remote-control/main.py b/examples/demo-webserver-remote-control/main.py new file mode 100644 index 0000000..5f58842 --- /dev/null +++ b/examples/demo-webserver-remote-control/main.py @@ -0,0 +1,10 @@ +#import demo + +#import hand_follower.py + +#import line_follower.py + +#this demo will create a wifi network, and connecting to it with a phone will show on 192.168.4.1 a page with four buttons to move the robot +#you should change the name of the wifi network by editing wifi_host.py line 31 the variable gs_ssid with the name of the robot +#multiple robot with the same wifi network name would interfere with each other +import wifi_host.py diff --git a/examples/demo-webserver-remote-control/page_arrow_control.html b/examples/demo-webserver-remote-control/page_arrow_control.html new file mode 100644 index 0000000..18ee656 --- /dev/null +++ b/examples/demo-webserver-remote-control/page_arrow_control.html @@ -0,0 +1,137 @@ + + + + + + ESP32 Arrow Control Portal + + + +
+
+ +
+ +
+ +
+ +
+
+
Waiting for command...
+ + + + diff --git a/examples/demo-webserver-remote-control/wifi_host.py b/examples/demo-webserver-remote-control/wifi_host.py new file mode 100644 index 0000000..4e9c8c5 --- /dev/null +++ b/examples/demo-webserver-remote-control/wifi_host.py @@ -0,0 +1,226 @@ +""" +ESP32-based WiFi and Web Server Control for Arduino-Alvik Robot + +This script sets up an ESP32 as a WiFi access point and runs both a DNS server +(for captive portal functionality) and a web server (to receive remote control commands). + +The web server listens for requests that modify the robot's movement, which +is controlled via the Arduino-Alvik library. + +Features: +- Configures ESP32 as a SoftAP (WiFi hotspot) +- Runs a simple DNS server to handle captive portal requests +- Runs an HTTP web server to process user commands +- Provides a basic control interface for moving an Arduino-Alvik robot +- Uses threading for concurrent execution of DNS and Web server + +Author: Orso Eric +Company: AROL Closure Systems +Date: 2025-05-06 +License: MIT +""" + +import network +import socket +import _thread +from time import sleep_ms + +from arduino_alvik import ArduinoAlvik + +#name of the wifi network +gs_ssid = "Alvik-robot-Arol" + + +def update_speed(forward, right): + _speed_lock.acquire() + try: + _speed_data["forward"] = forward + _speed_data["right"] = right + finally: + _speed_lock.release() + +def get_speed(): + _speed_lock.acquire() + try: + forward = _speed_data["forward"] + right = _speed_data["right"] + finally: + _speed_lock.release() + return forward, right + +def setup_access_point(): + # Configure ESP32 as a WiFi SoftAP. + cl_access_point = network.WLAN(network.AP_IF) + cl_access_point.active(True) + cl_access_point.config(essid=gs_ssid, password="12345678") + print("Access Point started with config:", cl_access_point.ifconfig()) + return cl_access_point + +def build_dns_response(data): + transaction_id = data[0:2] + flags = b'\x81\x80' + qdcount = data[4:6] + ancount = b'\x00\x01' # One answer record. + nscount = b'\x00\x00' + arcount = b'\x00\x00' + header = transaction_id + flags + qdcount + ancount + nscount + arcount + + query = data[12:] + answer = b'\xc0\x0c' # Pointer back to the query name. + answer += b'\x00\x01' # Type A record. + answer += b'\x00\x01' # Class IN. + answer += b'\x00\x00\x00\x3c' # TTL of 60 seconds. + answer += b'\x00\x04' # IPv4 address length. + ip = gcl_access_point.ifconfig()[0] + ip_bytes = bytes([int(x) for x in ip.split('.')]) + answer += ip_bytes + + return header + query + answer + +def dns_server(): + global gb_app_running + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("0.0.0.0", 53)) + sock.settimeout(1) + print("DNS server listening on UDP port 53") + while gb_app_running: + try: + data, addr = sock.recvfrom(512) + print("DNS request from:", addr) + response = build_dns_response(data) + sock.sendto(response, addr) + except OSError: + # Timeout: re-check gb_app_running. + pass + sock.close() + print("DNS server stopped") + except Exception as e: + print("DNS server error:", e) + +def web_server(): + global gb_app_running + try: + addr_info = socket.getaddrinfo("0.0.0.0", 80)[0][-1] + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(addr_info) + sock.listen(5) + sock.settimeout(1) + print("Web server listening on port 80") + + while gb_app_running: + try: + client_sock, client_addr = sock.accept() + except OSError: + continue + + try: + req = client_sock.recv(2048) + req_str = req.decode("utf-8") + print("HTTP request from", client_addr, ":", req_str.splitlines()[0]) + + # Check for captive portal connectivity check (Android) + if "generate_204" in req_str or "connectivity" in req_str or "ncsi.txt" in req_str: + # Redirect connectivity checks to the captive portal page. + ip = gcl_access_point.ifconfig()[0] + response = ("HTTP/1.1 302 Found\r\n" + "Location: http://%s/\r\n" + "Cache-Control: no-cache\r\n" + "\r\n" % ip) + client_sock.send(response.encode("utf-8")) + client_sock.close() + continue + + first_line = req_str.split("\r\n")[0] + parts = first_line.split(" ") + path = parts[1] if len(parts) >= 2 else "/" + + if path.startswith("/update?"): + # Parse query parameters. + query_string = path[len("/update?"):] + params = {} + for param in query_string.split("&"): + if "=" in param: + k, v = param.split("=", 1) + params[k] = v + x_val = float(params.get("x", "0")) + y_val = float(params.get("y", "0")) + update_speed(y_val, x_val) + + evt = params.get("evt", "none") + print("Event:", evt, "| Arrow command - x:", x_val, "y:", y_val) + response_body = "OK" + response = ("HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n\r\n" + response_body) + client_sock.send(response.encode("utf-8")) + + elif path == "/stop": + print("Stop command received. Stopping application...") + gb_app_running = False + response_body = ("Stopped" + "

Application Stopped

") + response = ("HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n\r\n" + response_body) + client_sock.send(response.encode("utf-8")) + + else: + # Serve the captive portal page by loading the external HTML file. + try: + with open("page_arrow_control.html", "r") as f: + html = f.read() + except Exception as e: + print("Error loading page_arrow_control.html:", e) + html = ("Error" + "

Error: Could not load page_arrow_control.html

") + + response = ("HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n\r\n" + html) + client_sock.send(response.encode("utf-8")) + + client_sock.close() + except Exception as e: + print("Error handling client:", e) + sock.close() + print("Web server stopped") + except Exception as e: + print("Web server error:", e) + +# --- Main code --- + + +# Global flag for application running. +gb_app_running = True + +# The access point object (will be set later). +gcl_access_point = None + +# Create a shared dictionary to store speed values, protected by a lock. +_speed_lock = _thread.allocate_lock() +_speed_data = {"forward": 0.0, "right": 0.0} + +gcl_access_point = setup_access_point() + +cl_alvik = ArduinoAlvik() +cl_alvik.begin() + +# Start the DNS and HTTP server threads. +_thread.start_new_thread(dns_server, ()) +_thread.start_new_thread(web_server, ()) + +# Main loop: update wheels based on shared speed data. +while gb_app_running: + n_speed_forward, n_speed_turn = get_speed() + n_speed_left = - n_speed_forward * 100.0 - n_speed_turn * 100.0 + n_speed_right = - n_speed_forward * 100.0 + n_speed_turn * 100.0 + + print(f"Move L{n_speed_left} R{n_speed_right}") + cl_alvik.set_wheels_speed(n_speed_right, n_speed_left) + + sleep_ms(100) + +# On exit, stop the wheels and disable the AP. +cl_alvik.set_wheels_speed(0.0, 0.0) +gcl_access_point.active(False) + +print("Application has been stopped.")