Laser Portraits

I had a chance to run 2 Pewtybots at a public event where people could have their picture taken to be turned into line art to be rendered by lasers. I should have taken better pictures and videos, but I was too busy manning the station and talking to people. It’s unfortunate because it’s hard to convey the experience with words.

Ever since the first successful prototype, I kind of knew I wanted to do something for this event. And so I built 2 machines, refined the software, the math… well maybe that’ll be another post… I’m not sure I have it right just yet, I might. I have it right enough at least, let’s just say I refined the math. Finally, I spent time developing and practicing a pipeline where I can take someone’s picture and send it to either Pewtybot happens to be idle.

Esther helped man the station so we practiced at home with all her toys pretending to be the various personalities she’d encounter at such an event. From the overly curious bear to the llama in a hurry. And so the pipeline is as such: first we take your picture. A monitor is facing you to see what it is.

Then we turn that into lines to be drawn (or lasered in this case). This is supposed to be a first taste of eye candy as these algorithms are cool to see at work.

Then you go in a dark room, and see it all get zapped on the wall (I don’t have a picture of the lasered dog plush).

Because this was a first on many fronts, I was pretty anxious some things would go wrong (they did). I also didn’t know how to present it, or how people would react. So the first couple of “customers” helped me figure out how to guide them through the pipeline. And when the time comes to go in the dark room, I purposefully kind of dump people in there and vaguely tell them to wait for the wall to light up. I have the laser write their name and count down from 3 to 1, and then the laser moves much faster through their portrait.

Nicole quickly realized adding chairs in the dark room would invite people to take in the experience more. And I realized I was silly to tune my setting for single portraits, I almost exclusively had families and groups of friends in the same picture. The reactions were great, although I didn’t get any from inside the room, people coming out were full of questions and kids were smiling. As always with my silly projects, there’s also a smaller fraction of people with whom they resonate more deeply.

Overall it was a big success and pretty smooth for a first. I want to do more for sure. There’s something fireworksy about when the laser really starts moving and light shows up everywhere.

Evaporator Regulator

This isn’t the most involved project but I might as well document it. I’ve been trying to automate some of the more boring tasks of running the evaporator, I’ve got some nice stainless steel float valves to regulate the sap going in now for example. One of the things that kept requiring constant attention is the air intake to adjust the strength of the fire. I’d have to sit with a foot on it to be able to regulate it almost constantly, to make sure the fire wasn’t burning too hard or too weak. And so naturally I thought I could do something with a Pi.

This proved quite successful even with very loose wiring and fastening just to see how it would work.

All of a sudden I barely need to pay attention to the fire’s strength, with a few refinements I won’t have to at all.

The circuit is quite simple:

Wifi barely reaches the sugarhouse so I made sure this could run independent of connectivity. Which involves coding threads on a Pi Pico, which is supported but not as one would expect.

import machine
import time
import network
import socket
from max6675 import MAX6675
import _thread
    

html = """{\"evaporator_temperature\":<TEMPERATURE>}"""

# LED
led = machine.Pin( "LED", machine.Pin.OUT )

# temperature
sck = machine.Pin( 2, machine.Pin.OUT )
cs = machine.Pin( 3, machine.Pin.OUT )
so = machine.Pin( 4, machine.Pin.IN )
sensor = MAX6675( sck, cs , so )
temperature_min = 25
temperature_max = 30
temperature = -1337.0

# servo
servo = machine.PWM( machine.Pin(0) )
servo.freq( 50 )
servo_min = 1000
servo_max = 8000
servo_at = 0


def temp_to_servo( temp ):
    if (temperature_max - temperature_min)==0:
        # right in the middle
        return int( (servo_max-servo_min)/2 )
    result = (temp - temperature_min) * (servo_max - servo_min) / (temperature_max - temperature_min) + servo_min
    if result>servo_max:
        result = servo_max
    if result<servo_min:
        result = servo_min
    return int( result )


def blink_number( number ):
    number = str( int(number) )
    for char in number:
        for i in range(int(char)):
            led.value( 1 )
            time.sleep( 0.2 )
            led.value( 0 )
            time.sleep( 0.2 )
        time.sleep( 0.3 )
        led.value( 1 )
        time.sleep( 0.1 )
        led.value( 0 )
        time.sleep( 0.3 )
            


keep_going = False
def servo_thread():
    global temperature, servo_at, servo
    
    while keep_going:
        time.sleep( 5 )
        print( "# measuring average temperature over 1 seconds..." )
        temperature_total = 0.0
        for i in range(10):
            temperature_total += sensor.read()
            time.sleep( 0.1 )
        temperature = temperature_total / 10
        blink_number( temperature )
        print( "# " + str(temperature) )
        new_servo_position = temp_to_servo( temperature )
        print( "# new_servo_position: " + str(new_servo_position) )
        step = 1
        if new_servo_position<servo_at:
            step = -1
        for i in range(servo_at, new_servo_position, step):
            time.sleep( 0.001 )
            servo_at = i
            servo.duty_u16( i )
    print( "# servo tread finishing" )

# main thread
servo.duty_u16( servo_min )
for i in range(servo_min, servo_max):
    time.sleep( 0.001 )
    servo_at = i
    servo.duty_u16( i )

try:
    ssid = "<wifi_ssid>"
    password = "<wifi_password>"
    wlan = network.WLAN( network.STA_IF )
    wlan.active( True )
    wlan.connect( ssid, password )

    # wait for connect or fail
    max_wait = 20
    while max_wait>0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        max_wait -= 1
        print( "> waiting for connection..." )
        time.sleep( 1 )

    # handle connection error
    if wlan.status()!=3:
        print( "> network connection failed, will launch servo thread in 1 minute" )
        time.sleep( 60 )
        print( "> launching" )
        keep_going = True
        _thread.start_new_thread(servo_thread, ())
        while True:
            time.sleep( 1 )
    else:
        print( "> connected" )
        status = wlan.ifconfig()
        print( "ip = " + status[0] )

        # open socket
        addr = socket.getaddrinfo( "0.0.0.0", 80)[0][-1]
        s = socket.socket()
        s.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
        #s.settimeout(1)
        s.bind( addr )
        s.listen( 1 )
        print( "> web server listening on", addr )

        # listen for connections
        while True:
            print( ">" )
            try:
                cl, addr = s.accept()
                print( "client connected from", addr)

                request = cl.recv( 1024 )
               
                request = request.decode( "utf-8" ).strip()
                print( request )

                if request.startswith( "GET / " ):
                    print( "get data" )
                    response = html.replace( "<TEMPERATURE>", str(temperature) )
                    cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" )
                    cl.send( response )
                elif request.startswith( "GET /min_temperature " ):
                     print( "get min_temperature" )
                     cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" )
                     cl.send( str(temperature_min) )
                elif request.startswith( "GET /max_temperature " ):
                     print( "get max_temperature" )
                     cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" )
                     cl.send( str(temperature_max) )
                elif request.startswith( "PUT /min_temperature " ):
                    print( "put min_temperature" )
                    new_min_temperature = int(request.split( "\r\n\r\n" )[1])
                    print( new_min_temperature )
                    temperature_min = new_min_temperature
                    cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" )
                    cl.send( "\"ok\"" )
                elif request.startswith( "PUT /max_temperature " ):
                    print( "put max_temperature" )
                    new_max_temperature = int(request.split( "\r\n\r\n" )[1])
                    print( new_max_temperature )
                    temperature_max = new_max_temperature
                    cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" )
                    cl.send( "\"ok\"" )
                elif request.startswith( "PUT /start " ):
                    print( "put start" )
                    cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" )
                    if keep_going:
                        print( "  already started" )
                        cl.send( "\"already started\"" )
                    else:
                        keep_going = True
                        _thread.start_new_thread(servo_thread, ())
                        cl.send( "\"started\"" )
                        for i in range(5000, 6000):
                            time.sleep( 0.001 )
                            servo_at = i
                            servo.duty_u16( i )
                elif request.startswith( "PUT /stop " ):
                    print( "put stop" )
                    #cl.send( "HTTP/1.0 501 OK\r\nContent-type: application/json\r\n\r\n" )
                    #cl.send( "\"not implemented\"" )
                    # crash on stop, thread support is that bad
                    cl.send( "HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n" )
                    if keep_going:
                        keep_going = False
                        cl.send( "\"stopped\"" )
                    else:
                        print( "  already stopped" )
                        cl.send( "\"already stopped\"" )

                cl.close()
         
            except OSError as e:
                cl.close()
                print( "> connection closed" )
                keep_going = False
                time.sleep( 2 )
    

except KeyboardInterrupt:
    print( "> ctrl+c, wrapping up..." )
    keep_going = False
    time.sleep( 10 )
except Exception as e:
    print( e )
    print( "> unexpected exception, wrapping up..." )
    keep_going = False
    time.sleep( 10 )

import time
class MAX6675:
    MEASUREMENT_PERIOD_MS = 220

    def __init__(self, sck, cs, so):
        """
        Creates new object for controlling MAX6675
        :param sck: SCK (clock) pin, must be configured as Pin.OUT
        :param cs: CS (select) pin, must be configured as Pin.OUT
        :param so: SO (data) pin, must be configured as Pin.IN
        """
        # Thermocouple
        self._sck = sck
        self._sck.low()

        self._cs = cs
        self._cs.high()

        self._so = so
        self._so.low()

        self._last_measurement_start = 0
        self._last_read_temp = 0
        self._error = 0

    def _cycle_sck(self):
        self._sck.high()
        time.sleep_us(1)
        self._sck.low()
        time.sleep_us(1)

    def refresh(self):
        """
        Start a new measurement.
        """
        self._cs.low()
        time.sleep_us(10)
        self._cs.high()
        self._last_measurement_start = time.ticks_ms()

    def ready(self):
        """
        Signals if measurement is finished.
        :return: True if measurement is ready for reading.
        """
        return time.ticks_ms() - self._last_measurement_start > MAX6675.MEASUREMENT_PERIOD_MS

    def error(self):
        """
        Returns error bit of last reading. If this bit is set (=1), there's problem with the
        thermocouple - it can be damaged or loosely connected
        :return: Error bit value
        """
        return self._error

    def read(self):
        """
        Reads last measurement and starts a new one. If new measurement is not ready yet, returns last value.
        Note: The last measurement can be quite old (e.g. since last call to `read`).
        To refresh measurement, call `refresh` and wait for `ready` to become True before reading.
        :return: Measured temperature
        """
        # Check if new reading is available
        if self.ready():
            # Bring CS pin low to start protocol for reading result of
            # the conversion process. Forcing the pin down outputs
            # first (dummy) sign bit 15.
            self._cs.low()
            time.sleep_us(10)

            # Read temperature bits 14-3 from MAX6675.
            value = 0
            for i in range(12):
                # SCK should resemble clock signal and new SO value
                # is presented at falling edge
                self._cycle_sck()
                value += self._so.value() << (11 - i)

            # Read the TC Input pin to check if the input is open
            self._cycle_sck()
            self._error = self._so.value()

            # Read the last two bits to complete protocol
            for i in range(2):
                self._cycle_sck()

            # Finish protocol and start new measurement
            self._cs.high()
            self._last_measurement_start = time.ticks_ms()

            self._last_read_temp = value * 0.25

        return self._last_read_temp

Web requests collection.