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.

main.py
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 )
max6675.py
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.