├── .gitignore ├── README.md ├── about.html ├── bigmouth.py ├── billy.py ├── billy_demo1.py ├── billy_demo2.py ├── billyv2.py ├── footer.html ├── header.html ├── index.html └── wdt.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | venv 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Big Mouth Billy Bass & Raspberry Pi Pico W 2 | Control a Big Mouth Billy Bass animatronic fish over wifi using a Raspberry Pi Pico W - which hosts its own a webpage! 3 | 4 | The Raspberry Pi Pico W is a very capable Microcontroller - it can even host simple webpages, and enable you to control things over the internet by accepting commands and then acting upon them. 5 | 6 | I've seen a lot of projects that let you turn an LED on and off, from a webpage hosted on a Pico W, so I thought I'd take this one step further - letting you control a animatronic fish over the Internet. 7 | 8 | Currently there isn't a livestream of the fish, so you'll have to trust me that its working and responding to your requests. 9 | 10 | ## Website 11 | Visit: 12 | to send commands to Big Mouth Billy Bass. 13 | 14 | ## Video 15 | The YouTube video that goes along with this build can be accessed here: 16 | 17 | 18 | 19 | ![Big Mouth Billy Bass](http://www.kevsrobots.com/assets/img/bigmouthbillybass/fish01.jpg) 20 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | {{render_template("header.html")}} 2 | 3 |
4 |
5 |

About this Project

6 |

This website is hosted on a Raspberry Pi Pico W, in the Robotlab, hotglued inside a Big Mouth Billy Bass. You can remotely control Big Mouth Billy by clicking the buttons on the main page.

7 |

The Pico W runs MicroPython code to control the Fish motors, and to receive requests for web pages and to render out the webpages based on templates.

8 | 9 | 10 |
11 |

Phew! Phew!

12 |

Phew! is the web server, logging system, and page templating system use a new MicroPython library from Pimoroni

13 |

Logging

14 |

The logging system enables you to log important information to the Pico W's local file system, noting the date and time, type of message and the message itself. 15 | The types of messages are: debug, info, warn, error.

16 |

The logging system has a function called Truncate which can be used to trim the log file down to a certain number of lines. This ensures the Pico W doesn't run out of space.

17 | 18 | 19 |

NTP-time

20 |

NTP is used to Synchronise the Pico W's realtime clock with a network type protocol server, which it does right after connecting to the local wifi network.

21 |

This ensures the log files record the correct time, and uptime is correctly displayed on the main website.

22 | 23 | 24 |

Code on Github

25 |

You can download this code yourself to have a play with by visiting: https://www.github.com/kevinmcaleer/bigmouth_wifi

26 |
27 |
28 | 29 | {{render_template("footer.html")}} -------------------------------------------------------------------------------- /bigmouth.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | from time import sleep 3 | 4 | # Setup all the pins for each motor 5 | head1 = Pin(0, Pin.OUT) 6 | head2 = Pin(1, Pin.OUT) 7 | tail1 = Pin(2, Pin.OUT) 8 | tail2 = Pin(3, Pin.OUT) 9 | mouth1 = Pin(4, Pin.OUT) 10 | mouth2 = Pin(5, Pin.OUT) 11 | 12 | fishy_parts = [mouth1, mouth2, tail1, tail2, head1, head2] 13 | 14 | def head_out(m1, m2): 15 | m1.value(1) 16 | 17 | def head_in(m1, m2): 18 | m1.value(0) 19 | 20 | def head(m1, m2): 21 | m1.value(1) 22 | sleep(0.25) 23 | m1.value(0) 24 | sleep(0.25) 25 | 26 | # def tail_open(t1,t2,value): 27 | # print(f'tail flip: {value}') 28 | # t2.value(value) 29 | # sleep(0.25) 30 | 31 | # print(f'tail flip: {value}') 32 | # value = 0 33 | # t2.value(value) 34 | # sleep(0.25) 35 | 36 | def tail_out(t1,t2): 37 | print(f'tail flip out') 38 | t2.value(1) 39 | 40 | def tail_in(t1,t2): 41 | print(f'tail flip in') 42 | t2.value(0) 43 | 44 | def mouth_open(motor_1,motor_2): 45 | print(f'mouth open') 46 | motor_1.value(0) 47 | motor_2.value(1) 48 | 49 | def mouth_close(motor_1,motor_2): 50 | print(f'mouth closed') 51 | motor_1.value(1) 52 | motor_2.value(1) 53 | 54 | while True or KeyboardInterrupt: 55 | # head(head1, head2) 56 | head_out(head1, head2) 57 | sleep(0.5) 58 | tail_out(tail1, tail2) 59 | for _ in range (1.10): 60 | mouth_open(mouth1, mouth2) 61 | sleep(0.25) 62 | mouth_close(mouth1, mouth2) 63 | sleep(0.25) 64 | head_in(head1, head2) 65 | tail_in(tail1, tail2) 66 | mouth_close(mouth1, mouth2) 67 | sleep(1) -------------------------------------------------------------------------------- /billy.py: -------------------------------------------------------------------------------- 1 | from machine import Pin, RTC 2 | from time import sleep, gmtime 3 | from phew import logging 4 | import usocket 5 | import struct 6 | 7 | class Billy(): 8 | """ Big Mouth Billy Bass Robot """ 9 | # Setup all the pins for each motor 10 | 11 | head = False 12 | tail = False 13 | mouth = False 14 | 15 | def __init__(self): 16 | self.__head1 = Pin(0, Pin.OUT) 17 | self.__head2 = Pin(1, Pin.OUT) 18 | self.__tail1 = Pin(2, Pin.OUT) 19 | self.__tail2 = Pin(3, Pin.OUT) 20 | self.__mouth1 = Pin(4, Pin.OUT) 21 | self.__mouth2 = Pin(5, Pin.OUT) 22 | 23 | print(' ______ __ ______ __ __ ______ __ __ ______ __ __ ') 24 | print('/\ == \ /\ \ /\ ___\ /\ "-./ \ /\ __ \ /\ \/\ \ /\__ _\ /\ \_\ \ ') 25 | print('\ \ __< \ \ \ \ \ \__ \ \ \ \-./\ \ \ \ \/\ \ \ \ \_\ \ \/_/\ \/ \ \ __ \ ') 26 | print(' \ \_____\ \ \_\ \ \_____\ \ \_\ \ \_\ \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ ') 27 | print(' \/_____/ \/_/ \/_____/ \/_/ \/_/ \/_____/ \/_____/ \/_/ \/_/\/_/ ') 28 | print('') 29 | print(' ______ __ __ __ __ __ ______ ______ ______ ______ ') 30 | print('/\ == \ /\ \ /\ \ /\ \ /\ \_\ \ /\ == \ /\ __ \ /\ ___\ /\ ___\ ') 31 | print('\ \ __< \ \ \ \ \ \____ \ \ \____ \ \____ \ \ \ __< \ \ __ \ \ \___ \ \ \___ \ ') 32 | print(' \ \_____\ \ \_\ \ \_____\ \ \_____\ \/\_____\ \ \_____\ \ \_\ \_\ \/\_____\ \/\_____\ ') 33 | print(' \/_____/ \/_/ \/_____/ \/_____/ \/_____/ \/_____/ \/_/\/_/ \/_____/ \/_____/ ') 34 | print('') 35 | 36 | # truncate log to keep it to at most three blocks on disk) 37 | logging.info("truncating log file") 38 | logging.truncate(8192) 39 | 40 | # returns True if we've used up 90% of the internal filesystem 41 | @staticmethod 42 | def low_disk_space(): 43 | try: 44 | return (os.statvfs(".")[3] / os.statvfs(".")[2]) < 0.1 45 | except: 46 | # os.statvfs doesn't exist on remote mounts but in that case we can 47 | # assume plenty of space 48 | pass 49 | return False 50 | 51 | @staticmethod 52 | def what_time_is_it_mr_wolf()->list: 53 | time_list = gmtime() 54 | current_time = {'year': str(time_list[0]), 55 | 'month': str(time_list[1]), 56 | 'day': str(time_list[2]), 57 | 'hour': str(time_list[3]), 58 | 'minute': str(time_list[4]), 59 | 'second': str(time_list[5])} 60 | return str(current_time['hour'] + ":" + current_time['minute'] + ":" + current_time['second'] + " " + current_time['day'] + "/" + current_time['month'] + "/" + current_time['year'] ) 61 | 62 | @staticmethod 63 | def clock_set(): 64 | return RTC().datetime()[0] > 2022 # year greater than 2020? we're golden! 65 | 66 | @staticmethod 67 | def update_rtc_from_ntp(max_attempts = 5): 68 | logging.info("> fetching date and time from ntp server") 69 | ntp_host = "pool.ntp.org" 70 | attempt = 1 71 | while attempt < max_attempts: 72 | try: 73 | logging.info(" - synching rtc attempt", attempt) 74 | query = bytearray(48) 75 | query[0] = 0x1b 76 | address = usocket.getaddrinfo(ntp_host, 123)[0][-1] 77 | socket = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM) 78 | socket.settimeout(30) 79 | socket.sendto(query, address) 80 | data = socket.recv(48) 81 | socket.close() 82 | local_epoch = 2208988800 # selected by Chris, by experiment. blame him. :-D 83 | timestamp = struct.unpack("!I", data[40:44])[0] - local_epoch 84 | t = gmtime(timestamp) 85 | return t 86 | except Exception as e: 87 | logging.error(e) 88 | 89 | attempt += 1 90 | return False 91 | 92 | # connect to wifi and then attempt to fetch the current time from an ntp server 93 | # once fetch set the onboard rtc and the pico's own rtc 94 | 95 | def sync_clock_from_ntp(self): 96 | t = self.update_rtc_from_ntp() 97 | if not t: 98 | logging.error(" - failed to fetch time from ntp server") 99 | return False 100 | 101 | # set the pico rtc time 102 | RTC().datetime((t[0], t[1], t[2], t[6], t[3], t[4], t[5], 0)) 103 | logging.info(" - rtc synched") 104 | return True 105 | 106 | def head_out(self): 107 | """ Move head out """ 108 | self.head = True 109 | self.__head1.value(1) 110 | self.__head2.value(0) 111 | sleep(0.25) 112 | 113 | def head_in(self): 114 | """ Move head in """ 115 | self.head = False 116 | self.__head1.value(0) 117 | self.__head2.value(0) 118 | sleep(0.25) 119 | 120 | def tail_out(self): 121 | """ Move tail out """ 122 | self.tail = True 123 | self.__tail2.value(1) 124 | sleep(0.25) 125 | 126 | def tail_in(self): 127 | """ Move tail in """ 128 | self.tail = False 129 | self.__tail2.value(0) 130 | sleep(0.25) 131 | 132 | def mouth_open(self): 133 | """ Move mouth open """ 134 | self.mouth = True 135 | self.__mouth1.value(0) 136 | self.__mouth2.value(1) 137 | sleep(0.25) 138 | 139 | def mouth_close(self): 140 | """ Move mouth closed """ 141 | self.mouth = False 142 | self.__mouth1.value(1) 143 | self.__mouth2.value(1) 144 | sleep(0.25) 145 | 146 | def flap_tail(self, times): 147 | for _ in range(1, times): 148 | self.tail_out() 149 | sleep(0.001) 150 | self.tail_in() 151 | 152 | def reset(self): 153 | self.__head1.value(0) 154 | self.__head2.value(0) 155 | self.__tail1.value(0) 156 | self.__tail2.value(0) 157 | self.__mouth1.value(0) 158 | self.__mouth2.value(0) 159 | sleep(0.25) 160 | 161 | @property 162 | def status(self)->str: 163 | message = "Billy's " 164 | if self.head: 165 | message = message + "Head is out, " 166 | else: 167 | message = message + "Head is in, " 168 | 169 | if self.mouth: 170 | message = message + "mouth open, " 171 | else: 172 | message = message + "mouth closed, " 173 | 174 | if self.tail: 175 | message = message + "and the tail is out." 176 | else: 177 | message = message + "and the tail is in." 178 | 179 | return message 180 | 181 | def __str__(self): 182 | state = [0,0,0] 183 | if self.mouth: 184 | state[0] = 1 185 | else: 186 | state[0] = 0 187 | if self.head: 188 | state[1] = 1 189 | else: 190 | state[1] = 0 191 | if self.tail: 192 | state[2] = 1 193 | else: 194 | state[2] = 0 195 | 196 | return ''.join(str(e) for e in state) -------------------------------------------------------------------------------- /billy_demo1.py: -------------------------------------------------------------------------------- 1 | from billy import Billy 2 | from time import sleep 3 | import network 4 | from secret import ssid, password 5 | from machine import Pin, WDT, reset 6 | import uasyncio as asyncio 7 | import socket 8 | from time import gmtime 9 | from phew import server, connect_to_wifi 10 | 11 | connect_to_wifi(ssid, password) 12 | 13 | f = open("index.html","r") 14 | html = f.read() 15 | f.close() 16 | 17 | onboard = Pin("LED", Pin.OUT, value=0) 18 | 19 | billy = Billy() 20 | stateis = "" 21 | page_views = 0 22 | 23 | 24 | def what_time_is_it_mr_wolf()->list: 25 | time_list = gmtime() 26 | current_time = {'year': str(time_list[0]), 27 | 'month': str(time_list[1]), 28 | 'day': str(time_list[2]), 29 | 'hour': str(time_list[3]), 30 | 'minute': str(time_list[4]), 31 | 'second': str(time_list[5])} 32 | return str(current_time['hour'] + ":" + current_time['minute'] + ":" + current_time['second'] + " " + current_time['day'] + "/" + current_time['month'] + "/" + current_time['year'] ) 33 | 34 | async def serve_client(reader, writer): 35 | global billy, stateis, page_views, boot_time 36 | 37 | page_views += 1 38 | print("Client Connected") 39 | request_line = await reader.readline() 40 | print("Request:", request_line) 41 | while await reader.readline() != b"\r\n": 42 | pass 43 | 44 | request = str(request_line) 45 | head_out = request.find('/head/out') 46 | head_in = request.find('/head/in') 47 | mouth_open = request.find('/mouth/open') 48 | mouth_close = request.find('/mouth/close') 49 | tail_out = request.find('/tail/out') 50 | tail_in = request.find('/tail/in') 51 | 52 | 53 | if head_out == 6: billy.head_out() 54 | if head_in == 6: billy.head_in() 55 | if mouth_open == 6: billy.mouth_open() 56 | if mouth_close == 6: billy.mouth_close() 57 | if tail_out == 6: billy.tail_out() 58 | if tail_in == 6: billy.tail_in() 59 | 60 | print(f'billy: "{billy}"') 61 | response = html % (billy, billy.status, str(page_views), boot_time) 62 | writer.write('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n') 63 | writer.write(response) 64 | 65 | await writer.drain() 66 | await writer.wait_closed() 67 | print('Client Disconnected') 68 | 69 | async def main(): 70 | print ("setting up webserver") 71 | asyncio.create_task(asyncio.start_server(serve_client, "0.0.0.0", 80)) 72 | wdt = WDT(timeout=8000) 73 | while True: 74 | # check wifi is connected: 75 | if wlan.isconnected() == False: 76 | print("wifi disconnected") 77 | reset() 78 | onboard.on() 79 | print("heartbeat") 80 | wdt.feed() 81 | await asyncio.sleep(0.25) 82 | onboard.off() 83 | await asyncio.sleep(5) 84 | 85 | boot_time = what_time_is_it_mr_wolf() 86 | billy.reset() 87 | sleep(0.1) 88 | 89 | try: 90 | asyncio.run(main()) 91 | finally: 92 | asyncio.new_event_loop() -------------------------------------------------------------------------------- /billy_demo2.py: -------------------------------------------------------------------------------- 1 | from billy import Billy 2 | from time import sleep 3 | 4 | from machine import Pin 5 | 6 | billy = Billy() 7 | billy.reset() 8 | sleep(1) 9 | 10 | billy.head_out() 11 | billy.tail_out() 12 | sleep(0.5) 13 | for _ in range(1, 1): 14 | billy.mouth_open() 15 | sleep(0.0001) 16 | billy.mouth_close() 17 | sleep(0.0001) 18 | billy.tail_in() 19 | billy.head_in() 20 | 21 | billy.flap_tail(2) 22 | # billy.head_out() 23 | # sleep(2) 24 | # billy.head_in() 25 | 26 | try: 27 | asyncio.run(main()) 28 | finally: 29 | asyncio.new_event_loop() -------------------------------------------------------------------------------- /billyv2.py: -------------------------------------------------------------------------------- 1 | import network 2 | import upip 3 | from secret import ssid, password 4 | from billy import Billy 5 | import uos 6 | from time import sleep, gmtime 7 | from machine import RTC, reset 8 | 9 | wlan = network.WLAN(network.STA_IF) 10 | wlan.active(True) 11 | wlan.connect(ssid, password) 12 | 13 | while wlan.isconnected() == False: 14 | print(".",end="") 15 | sleep(0.5) 16 | 17 | print(f"connected with ip {wlan.config}") 18 | 19 | # check if phew installed 20 | installed_libs = uos.listdir('lib') 21 | if "phew" not in installed_libs: 22 | print("Phew not installed, installing now") 23 | upip.install("micropython-phew") 24 | else: 25 | print("phew already installed") 26 | from phew import logging, server, template 27 | 28 | logging.info("Starting up Billy") 29 | billy = Billy() 30 | 31 | page_views = 1 32 | status = "" 33 | 34 | from phew import server, connect_to_wifi 35 | 36 | connect_to_wifi(ssid, password) 37 | 38 | # if the clock isn't set then we need to fetch the time from an NTP 39 | # server. this requires connecting to WiFi 40 | 41 | # set clock 42 | billy.update_rtc_from_ntp() 43 | 44 | if not billy.clock_set(): 45 | logging.info("> clock not set, synchronise from ntp server") 46 | if not billy.sync_clock_from_ntp(): 47 | # if we failed to synchronise the clock then turn on the warning 48 | # led and go back to sleep for another cycle 49 | logging.error("! failed to synchronise clock") 50 | 51 | uptime = billy.what_time_is_it_mr_wolf() 52 | 53 | if billy.low_disk_space(): 54 | # there is less than 10% of the filesystem available, time to truncate the log file 55 | logging.error("! low disk space") 56 | logging.truncate(8192) 57 | 58 | @server.route("/", methods=["GET"]) 59 | def index(request): 60 | global page_views 61 | response = (template.render_template('index.html',page_views=page_views, uptime=uptime, status=status, fish_image=billy)) 62 | page_views += 1 63 | return response 64 | 65 | @server.route("/about", methods=["GET"]) 66 | def about(request): 67 | global page_views 68 | response = (template.render_template('about.html',page_views=page_views, uptime=uptime, status=status, fish_image=billy)) 69 | page_views += 1 70 | return response 71 | 72 | def redirect_and_respond(request): 73 | global page_views 74 | response = (template.render_template('index.html',page_views=page_views, uptime=uptime, status=billy.status, fish_image=billy)) 75 | page_views += 1 76 | logging.info("redirecting") 77 | return response 78 | 79 | @server.route("/head/out", methods=["GET"]) 80 | def head_out(request): 81 | billy.head_out() 82 | return redirect_and_respond(request) 83 | 84 | @server.route("/head/in", methods=["GET"]) 85 | def head_in(request): 86 | billy.head_in() 87 | return redirect_and_respond(request) 88 | 89 | @server.route("/tail/out", methods=["GET"]) 90 | def tail_out(request): 91 | billy.tail_out() 92 | return redirect_and_respond(request) 93 | 94 | @server.route("/tail/in", methods=["GET"]) 95 | def tail_in(request): 96 | billy.tail_in() 97 | return redirect_and_respond(request) 98 | 99 | @server.route("/mouth/open", methods=["GET"]) 100 | def mouth_open(request): 101 | billy.mouth_open() 102 | return redirect_and_respond(request) 103 | 104 | @server.route("/mouth/closed", methods=["GET"]) 105 | def mouth_close(request): 106 | billy.mouth_close() 107 | return redirect_and_respond(request) 108 | 109 | @server.catchall() 110 | def catchall(request): 111 | return "Not found", 404 112 | 113 | server.run(host="0.0.0.0", port=80) 114 | wdt = WDT(timeout=8000) 115 | while True: 116 | if billy.low_disk_space(): 117 | # there is less than 10% of the filesystem available, time to truncate the log file 118 | logging.error("! low disk space") 119 | logging.truncate(8192) 120 | 121 | # check wifi is connected: 122 | if wlan.isconnected() == False: 123 | print("wifi disconnected") 124 | log.error("wifi disconnected") 125 | log.info("Resetting device") 126 | reset() 127 | onboard.on() 128 | wdt.feed() 129 | sleep(0.25) 130 | onboard.off() 131 | sleep(5) 132 | -------------------------------------------------------------------------------- /footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Billy Bass 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | {{render_template("header.html")}} 2 |
3 |
4 |

Big Mouth Billy Bass

5 |

This website is hosted on a Raspberry Pi Pico W, 6 | in the Robotlab, hotglued inside a Big Mouth Billy Bass. You can remotely control Big Mouth Billy by clicking the buttons below.

7 |

Catch the YouTube video about this project by clicking here.

8 | Move Head Out 9 | Move Head In 10 | 11 | Open Mouth 12 | Closed Mouth 13 | 14 | Move Tail Out 15 | Move Tail In 16 |
17 | 18 | 19 |
20 | 21 |

STATUS: {{status}}

22 |

Page Views: {{page_views}}

23 |

Web server up since: {{uptime}}

24 |
25 |
26 | {{render_template("footer.html")}} -------------------------------------------------------------------------------- /wdt.py: -------------------------------------------------------------------------------- 1 | # Watch Dog Timer 2 | 3 | from machine import WDT 4 | 5 | wdt = WDT(timeout=8000) 6 | 7 | while True: 8 | sleep(1) 9 | wdt.feed() --------------------------------------------------------------------------------