xexus.dev

xexus.dev
LCD Clock with Buttons

xexus.nexus

The Doll was a practical joke for my wife, though six weeks of work for a five minute joke is far from practical.

First I built the doll, embedded with motors to turn the head and move the arms. Eventually I had to remove the motors from the arms after breaking the arm joints, but I've left the objects in the code for when I make a new version one day.

I places the doll in the living room, telling my wife that I was just putting it there for now until I figured out what to do with it. After a month, she just saw it as yet another trinket on the shelf.

Once activated, it would turn it's head to stare at my wife's chair every midnight, and turn back after five minutes. The servo motor makes a little noise which eventually got her attention, invoking the question, "Did you just turn the Doll's head to look at me?". When it turned it's head back to it's starting position, she said "I don't know what you've done, but I know you've did it". Well she's no dummy, lol.

Parts

The links below go to the sites where I ordered the parts for this project, though you can find these parts at various websites as well.

Project Notes

This code was recently updated via CodeGPT (powered by ChatGPT) for the sake of this page. The doll has been taken apart for now, but I when I revisit this project again, I will add in the the timer function from the Clock with Chime so that it doesn't require the activation command. I'll leave the activation code in there for testing of course.

The head joint was controlled by a micro servo connected to a dowel running up through the body. The duty cycle was fine tuned by hand by trial and error.

The MicroPython code

  • The secrets.py import contains three lines to store my Access Point's SSID, Password, and Static IP:
    SSID = "My_SSID"
    PW = "My_Password"
    StaticIP = "My_Static_IP_Address"

Future Revisions

I'll need a new doll next time around, preferably a little bigger. The problem with the arms began with not having enough room for the rods. There was too much friction in the arm joints, and a slip of the knife led to taking the arm servos out altogether.


import ujson  # Importing the ujson library for JSON parsing
from machine import Pin, PWM  # Importing Pin and PWM for controlling GPIO pins and servos
import network  # Importing network module to handle Wi-Fi connections
import utime  # Importing utime for delays
import secrets  # Importing secrets for storing sensitive data like SSID and passwords
import urequests  # Importing urequests to send HTTP requests
import socket  # Importing socket to handle TCP/IP connections

# Setup LED for feedback
led = Pin("LED", Pin.OUT)
led.on()  # Turn the LED on initially as a feedback mechanism

# Wi-Fi credentials from secrets (external file or module containing SSID and password)
SSID = secrets.SSID
PW = secrets.PW

# Setup WLAN as station (Wi-Fi client mode)
wlan = network.WLAN(network.STA_IF)  # Set WLAN interface to station mode (client)
wlan.active(True)  # Activate the WLAN interface
wlan.connect(SSID, PW)  # Connect to the Wi-Fi network using SSID and password
print('Waiting for connection...')  # Print a status message
busy_meter = 0  # Initialize a counter to track connection attempts

# Wi-Fi connection handling loop
while not wlan.isconnected():  # Loop until the device is connected to the Wi-Fi network
    led.toggle()  # Blink the LED to indicate ongoing connection attempts
    busy_meter += 1  # Increment the connection attempt counter
    utime.sleep(0.5)  # Wait for 0.5 seconds before checking again
    if busy_meter == 16:  # If it reaches 8 seconds (0.5s * 16), assume connection failed
        print("Wi-Fi connection failed, resetting...")  # Print failure message
        machine.reset()  # Reset the microcontroller to try again

# After connection is established, retrieve and display the IP configuration
status = wlan.ifconfig()  # Get the network interface configuration (IP address, subnet, etc.)
print(f'Connection to {SSID} successful!')  # Inform the user of successful connection
print(f'IP Address: {status[0]}')  # Display the assigned IP address
led.on()  # Turn the LED on permanently after connection is successful
utime.sleep(2)  # Wait for 2 seconds before proceeding

# Open a socket to listen for incoming connections
try:
    addr = socket.getaddrinfo(secrets.StaticIP, 8080)[0][-1]  # Resolve static IP address and port
    s = socket.socket()  # Create a socket object for communication
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # Allow reuse of socket address
    s.bind(addr)  # Bind the socket to the specified address and port
    s.listen(1)  # Set the socket to listen for incoming connections (max 1 client)
    print(f"Listening on {addr}")  # Print the socket address it's listening on
except OSError as e:  # Handle socket-related errors
    print(f"Socket error: {e}")  # Print the error if socket setup fails
    machine.reset()  # Reset the device in case of socket setup failure

# Define the class Body_Part for handling servo movement
class Body_Part:
    def __init__(self, name, servo_pin):
        """
        Initialize a body part (such as arm, head) and set up PWM control for the servo motor.
        :param name: Name of the body part
        :param servo_pin: Pin number for controlling the servo
        """
        self.name = name  # Assign the name of the body part
        self.pin = PWM(Pin(servo_pin))  # Initialize PWM on the servo pin
        self.pin.freq(50)  # Set the PWM frequency to 50Hz (standard for servos)
        self.pin.duty_u16(4500)  # Set the initial servo position (duty cycle)

    def turn_head(self):
        """
        Method to control the head movement by adjusting PWM duty cycle.
        Turns the head to one side, waits, and then turns it back.
        """
        print("Turning head")  # Print the action
        # Move the servo from the default position to a specified range (turn the head)
        for turn in range(4500, 2750, -2):  # Decrease duty cycle to turn the head
            self.pin.duty_u16(turn)  # Apply the duty cycle to move the servo
            utime.sleep_ms(1)  # Small delay between each step for smoother movement
        utime.sleep(360)  # Hold the position for a while
        # Move the servo back to the original position
        for turn in range(2750, 4500, 2):  # Increase duty cycle to turn the head back
            self.pin.duty_u16(turn)  # Apply the duty cycle to move the servo back
            utime.sleep_ms(1)  # Small delay between each step for smoother movement
        print("Head turned back")  # Indicate completion of head movement

# Instantiate body parts (example: left arm, head, right arm)
left_arm = Body_Part("left_arm", 13)  # Left arm controlled by GPIO 13
head = Body_Part("head", 14)  # Head controlled by GPIO 14
right_arm = Body_Part("right_arm", 15)  # Right arm controlled by GPIO 15

# Main loop to listen for commands and respond accordingly
while True:
    try:
        cl, addr = s.accept()  # Accept an incoming connection from a client
        request = cl.recv(1024)  # Receive up to 1024 bytes of data from the client
        request_str = request.decode('utf-8')  # Decode the received bytes into a string
        print(f"Request received: {request_str}")  # Print the received request for debugging
        
        # Parsing command from the JSON payload
        try:
            request_json = ujson.loads(request_str)  # Attempt to parse the request as JSON
            command = request_json.get("command", "")  # Extract the "command" value from the JSON
            print(f"Command: {command}")  # Print the command for debugging
        except ValueError:
            print("Failed to parse JSON")  # Print an error message if JSON parsing fails
            cl.close()  # Close the connection and wait for the next request
            continue  # Skip to the next iteration of the loop
        
        # Check if the received command is "Activate"
        if command == "Activate":
            head.turn_head()  # Call the method to turn the head
        
        # Send a response back to the client indicating success
        response = "Command received"  # Prepare the response message
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')  # Send HTTP headers
        cl.send(response)  # Send the response message
        cl.close()  # Close the connection after sending the response

    except OSError as e:  # Catch any socket-related errors
        print(f"OSError: {e}")  # Print the error message for debugging
        cl.close()  # Ensure the connection is closed properly