CHALLENGE ACCEPTED: ROBOT SWEEPER, PART V

Well… things aren’t looking great at this point.

I thought I had everything figured out. Raspberry Pi Zero was going to talk over I2C and a level shifter with two Arduino Pro Micros – one for sensors and one for motors.

Rangefinder and LED/phototransistor pairs were working great and reporting nicely back to the Pi over I2C.

I wasn’t worried about the motors because a couple of days ago I had the drive motors working when sending commands using the USB serial on the Arduino (that’s what I was doing in the video from the other day).

But alas, the USB serial port uses timers differently than I2C on the Arduino, and the servos and I2C are arguing over the same 16-bit timer hardware in the 32U4. There is one other 16-bit timer on the chip (Timer3) but when I tried using it instead of Timer1 for the servos it doesn’t have the same kind of access to the pins that Timer1 has.

I think, anyway. The best I could get out of the servos was an occasional irregular “putt”. And now it’s getting late and I’ve spent way too much time staring at datasheets. Oh, and of course when I went to the Arduino forums I was greeted with this today:

Don’t get me wrong – it’s great they’re doing maintenance. I just wish they weren’t doing it NOW.

I know there’s a solution, but my old worn-out brain can’t think of it right now. I guess I could run the sensors over I2C and the motors over UART, but I was hoping for a cleaner, less confusing setup. A different microcontroller? ESP32? Pico? Ehurrrghhh.

At any rate, I need to get to bed. Lots of stuff I’ve been putting off that I should really do tomorrow before I get back to work on this particular project.

CHALLENGE ACCEPTED: ROBOT SWEEPER, PART IV

I can’t believe I’m saying this, but… IT’S ALIIIIIVEEE!

Doesn’t do much more than drive around with commands sent through a terminal session to the onboard Pi Zero, but I’m pretty sure this is the best run at building a robot I’ve had.

Needs better wheels, though – shiny hard plastic drive wheels aren’t so good on carpet, and a front “wheel” made out of a big blob of masking tape leaves a lot to be desired.

But there is definitely some progress going on. And it already picked up some hair (and there’s no brush or mop or vacuum on it)!

Woohoo little robot!

CHALLENGE ACCEPTED: ROBOT SWEEPER, PART III

Okay, so I’m making some progress. I looked around on the internet to see if I could get some ideas for what I wanted and I came up with this:

I prefer B9 from “Lost In Space” but Robby here has thumbs (and hopefully runs Python).

After some intense pencil & paper work, some printing, and some fumbling with little screws, I’ve got this:

Frankly, it’s pretty disappointing compared to the first picture but at least it’s progress.

My plan at this point is to use a Raspberry Pi Zero as the “brain”, and use an Arduino Pro Micro to handle the motor PWM because the Pi’s software PWM can be jittery. I need to figure out how to get it moving, how to control it, and how to keep itself from throwing itself down the stairs or getting jammed in a corner under the couch. Oh, and power it without an extension cord. Oh, and get it to actually sweep the floor.

No problem – only five things. Should be easy.

CHALLENGE ACCEPTED: ROBOT SWEEPER, PART II

So… last night I made a wheel. It’s not a particularly nice wheel, but it’s wheel-y and should do the job. Not a lot of progress, I’ll admit, but I have lots of time.

Or, at least I thought I did.

Just a few minutes ago I got a text from my wife – she’s at the store and they have a sale on little robot vacuums that ends in seven days.

Seven days!

CHALLENGE ACCEPTED: ROBOT SWEEPER

Spring is here and with it are beautiful sunny clear skies and lots of light. Unfortunately, due to that light and the main floor in our house being mostly hardwood (with some linoleum), every last dust mote, eyelash, and flea turd light up like little signs starting in the late afternoon.

While we’re not disgusting people, we both hate vacuuming and sweeping. It’s a pain, hauling out the vacuum is annoying and fighting with the cords and hoses isn’t fun (not like vacuuming should be, right?)

Anyway, we were just talking about this and, as neither of us volunteered to do the rest of the sweeping and/or vacuuming forever, we started to talk about buying a little robot vacuum. My wife has a friend who has a little Roomba that’s been running pretty solid for almost a decade now, and she thinks it’s the bees knees. So we went through some of the weekly flyers and took a look because vacuums were on sale, and…

GREAT NEPTUNE’S GHOST! TELL ME THAT PRICE IS A TYPO! THAT’S GOTTA BE A TYPO…

It wasn’t a typo. Those things are EXPENSIVE. Yikes.

I’m what you’d call “cheap”, so after we discussed it some more I said, “You know, sweetie, I could just build something to do that stuff, and if it fell down the stairs or over the ledge we wouldn’t be out hundreds and hundreds of dollars. Shouldn’t be too hard, and I’m pretty sure I have everything I need here already.”

She looked at me for a second and I could tell by her slightly narrowed eyes and the set of her jaw that she was thinking pretty hard. Then she said, “Okay, you can tryyyyyyy.”

As soon as she’d replied, I was already beginning to think I’d made a serious mistake, because I’d been pretty sure she was going to say “no”. In all of my electronics hobbyist and career work, 109% of my robot attempts have been utter and complete failures (and I’m pretty sure my wife knows that). But we made a deal: I have until the end of the next vacuum sale at one of the local stores to produce a working floor cleaning robot, or we’re going to buy one.

So I’m not sure what my approach is going to be. I’m not even sure what’s out there, so a bit of research might be in order. Regardless, I’d better go find some paper and a pencil. My pride and a whole bunch of money that would be better spent on potato chips and lasagna is at stake.

I’ll do my best to update my progress here so you can laugh or shake your head at my progress (or lack thereof). Who knows – I may actually build something that works…

Alright, time to get at it!

Raspberry Pi Document Scanning Host

Some years ago we bought a USB flatbed scanner. It got a ton of use, but it took up a lot of room by our computers. As time went on it spent less time hooked up to a computer and more time either in a drawer or resting on (or in) a pile of other barely-used-but-still-useful equipment.

Unfortunately, we still need to scan a lot of bills, receipts, prescription forms, insurance stuff, and all sorts of other things. Since the scanner is now, frankly, a pain in the butt to haul out, blow the dust off, and use, things tend to get left until the last minute. Of course, then there tends to be an enormous backlog of scanning to do.

I tried using a camera for a while. While it did an okay job for the most part, it really depended on lighting and patience to get good enough and consistent enough images. So the old CanoScan LiDE 700F would get put back to work.

This gave me an idea, though – what if it could just sit in a designated spot anywhere in the house and we could scan things without having to make room for it or make room for our computers? I figured that with our fancy electronic computers and the shared electronic cables and radio waves they communicate with each other over, this should be a possibility, right?

I thought about it for a bit and came up with the following requirements:

  1. It had to work without needing a desktop or laptop,
  2. It had to use a USB stick to store scans to prevent SD card wear,
  3. It had to be able to do colour, B&W, and greyscale,
  4. The DPI had to be adjustable, and
  5. There needed to be a way to easily get the images off it.

After a little bit of digging, I stumbled across Scanner Access Now Easy (SANE), and a Python package that worked with SANE – python-sane. Those two pieces of software, a Raspberry Pi, and some stitching would give me everything I needed to do what I wanted.

I’d picked up an inexpensive little touchscreen a couple of years ago that plugs directly into the Pi’s GPIO port. It’s not the fastest, it’s not the prettiest, and it’s certainly not the best, but with a 480×320 LCD with a resistive touchscreen, it’s not too shabby at all. You can find these things (and other similar devices) all over the place. The one I used is 3.5″, 480×320, and is one of the devices that uses the LCD-show drivers.

I started out with a Pi 2 I had kicking around but while CPU load and power didn’t seem to be a problem, it wasn’t able to run the scanner smoothly at higher resolutions, and the scanner would stutter so rapidly that it sounded like grinding gears.

My next attempt was going to be with a Pi 3A+ but it only has one USB port and, as I mentioned earlier, I wanted the scans to be saved to a USB stick instead of the SD card. I could’ve used a USB hub, but in my experience, fewer components means less troubleshooting.

So I ended up going with a Pi 4 B+ (2GB). It really feels like overkill, but in contrast to the Pi 2, it scanned very smoothly.

Anyway, so here’s what you’ll need:

  • A Pi 4 B+ with Raspberry Pi OS installed and updated and booting to the GUI with an account automatically logged in,
  • Your LCD drivers installed and working,
  • A SANE-compatible USB scanner,
  • A USB stick (the one I’m using is 8GB),
  • SANE installed (sudo apt install sane),
  • The python-sane module installed (sudo apt install python3-sane), and
  • Pygame installed

Once you’ve got everything installed and hooked up, start up Python from the command line:

python3

And type the following:

import sane
sane.init()
devices = sane.get_devices()
print(devices)

If, at this point, you see something like:

[('genesys:libusb:001:005', 'Canon', 'LiDE 700F', 'flatbed scanner')]

then SANE has found your scanner and you can continue. If you only see [] then there is a USB connection problem between your Pi and scanner, a power problem, or SANE doesn’t recognize your scanner. Don’t go on unless you can get SANE to find your scanner.

Next, set up your USB stick. Plug it in, remove any existing partitions on it using your favourite GUI or command line tool, then create a single ext4 partition on it.

Now, make a mount point for the USB stick. I made mine at /home/pi/scans.

Try to mount your USB stick:

sudo mount /dev/sda1 /home/pi/scans

If you don’t get an error, you should be good to go. Now edit the /etc/fstab file and add the following line at the end:

/dev/sda1 /home/pi/mnt ext4 defaults,noatime 0 0

Unmount your USB stick (sudo umount /dev/sda1), and then see if your fstab works by typing:

sudo mount -a

If you get no errors, type df and see if /dev/sda1 is listed:

If it is, you’re good to go!

Now go into the mount point you created, create a new directory called scans, and set the pi user and group as owners:

cd /home/pi/scans
mkdir scan_share
chown pi scan_share
chgrp pi scan_share

Now, set up Samba:

sudo apt install samba

Once it’s installed, edit the /etc/samba/smb.conf file. Comment out everything in the “Share Definitions” section at the end of the file and add the following:

[scans]
comment = Scanner Share
guest ok = no
browseable = yes
create mask = 0700
directory mask = 0700
read only = no
valid users = pi
path = /home/pi/scans/scan_share

Now, run the following command to give the pi user (or whichever user you want, I’m using pi) a password to use the Samba share you just created:

sudo smbpasswd -a pi

I strongly suggest not using the same password for Samba as you use for the pi account you use to log into your Pi’s console with.

Now reboot your Pi and once it comes back up, you should be able to see your Samba share on the network:

Go into “scans”, enter your username and the password you created with smbpasswd, and you should be greeted with an empty folder. Make sure you can create something in there – if not, check the permissions on your /home/pi/scans/scan_share folder.

So now you’ve confirmed your Pi can talk to your scanner, you’ve created a filesystem on a USB stick and are mounting it at boot, you’ve set up a Samba file share and have successfully logged into it and made sure you have permissions to the share. Time to glue it all together.

To do this, I used Pygame (with Python 3). I’m a huge fan of it because I’ve used it for a lot of different projects, I’m pretty comfortable with it, and there is a lot of documentation out there with no shortage of examples.

I am, however, NOT a programmer. My formative programming years were spent on a Commodore PET 4032 almost 40 years ago, and I still miss programming with line numbers. It seems to work with my setup, though.

It’s not done by any means (I could probably pass variables to a single function to set up the scan options instead of having each option set by its own function, and I need to play with the font sizes a bit more), but one of the nice things about having a site like this is that I can write down everything that’s on the 1397 pieces of scrap paper and sticky notes that I’ve been using to organize and store the information for this project and then come back to it later. So, without further ado, here’s the Python 3 code:

'''
scan-05.py

05: Partial rewrite to use functions instead of my usual giant list of nested loops

04: Now with graphical menus:
    - Tap to start
    - Select mode (Text, Grey, Colour)
        - icon sizes:
            - text: 0,125 to 155,197
            - grey: 162,125 to 315, 197
            - colour: 322,125 to 479,197
    - Select DPI (75, 150, 300, 600)
    - Select size (letter, full bed length)
    - Select Output (PNG, JPG)
    - "Another scan with these parameters?"

03: Detect touch and scan document with python-sane

02: Making progress with display graphics

User interface for document scanner (CanoScan LIDE 700F, should work for every SANE-supported USB scanner)
Uses 3.5" resistive touch display (480x320) on Pi 4 to show options/menus
Saves to appropriate folder on attached USB stick to save wear on SD card


*** GREY IS SPELLED 'GRAY' IN THE PYTHON-SANE MODULE. COLOUR IS SPELLED 'COLOR' ***

'''

import pygame, os, time, sys, sane


HEIGHT = 320
WIDTH = 480

RED = 255,0,0
GREEN = 0,255,0
BLUE = 0,0,255
YELLOW = 255,255,0
PURPLE = 255,0,255
BLACK = 0,0,0
WHITE = 255,255,255
DK_GREY = 50,50,50
MED_GREY = 127,127,127
LT_GREY = 200,200,200

BOOT_TXT = 'Starting...'
START_TXT = 'TAP TO START'
SCANNER_CHECK_TXT = 'Checking Scanner'
DPI_75_TXT = '75DPI'
DPI_150_TXT = '150DPI'
DPI_300_TXT = '300DPI'
DPI_600_TXT = '600DPI'
MODE_TEXT_TXT = ' Text '
MODE_GRAY_TXT = ' Grey '
MODE_COLOR_TXT= 'Colour'
SIZE_LETTER_TXT = '8.5x11.0'
SIZE_BED_TXT =    '8.5x11.7'
OUTPUT_PNG_TXT = 'Save as PNG (large)'
OUTPUT_JPG_TXT = 'Save as JPG (small)'
SCANNING_TXT = 'Scanning...'
SAVING_TXT = 'Saving...'
ANOTHER_TXT = 'Scan again with these settings?'
ANOTHER_YES_TXT = ' YES '
ANOTHER_NO_TXT = ' NO  '
ERR_NOSCANNER_TXT = 'NO SCANNER FOUND'
ERR_NOFILE_TXT = 'ERROR WRITING TO FILE'

go_on = True
scan_again = True

pygame.init()

pygame.display.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
screen.fill(BLACK)

huge_font = pygame.font.SysFont('freemono', 100)
big_font = pygame.font.SysFont('freemono', 75)
med_font = pygame.font.SysFont('freemono', 45)
small_font = pygame.font.SysFont('freemono', 25)


# *****FUNCTIONS START HERE*****
def SetMode():

    screen.fill(BLACK)
    pygame.display.flip()
    mode_text_obj = med_font.render(MODE_TEXT_TXT, True, BLACK, WHITE)
    mode_text_rect = mode_text_obj.get_rect()
    mode_text_rect.midleft = (0,160)
    screen.blit(mode_text_obj, mode_text_rect)

    mode_gray_obj = med_font.render(MODE_GRAY_TXT, True, LT_GREY, DK_GREY)
    mode_gray_rect = mode_gray_obj.get_rect()
    mode_gray_rect.center = (240,160)
    screen.blit(mode_gray_obj, mode_gray_rect)

    mode_color_obj = med_font.render(MODE_COLOR_TXT, True, RED, BLUE)
    mode_color_rect = mode_color_obj.get_rect()
    mode_color_rect.midright = (480,160)
    screen.blit(mode_color_obj, mode_color_rect)

    pygame.display.flip()
    print("E")
    pygame.event.clear()

    go_on = False


    while go_on == False:

        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.unicode == 'z':
                    pygame.quit()
                    sys.exit
            if event.type == pygame.MOUSEBUTTONUP:
                pos = pygame.mouse.get_pos()
                pygame.event.clear()
                if (pos[0] > 0 and pos[0] < 155 and pos[1] > 125 and pos[1] < 197):
                    print("TEXT")
                    scanner.mode = 'Lineart'
                    go_on = True
                elif (pos[0] > 162 and pos[0] < 315 and pos[1] > 125 and pos[1] < 197):
                    print("GREY")
                    scanner.mode = 'Gray'
                    go_on = True
                elif (pos[0] > 322 and pos[0] < 479 and pos[1] > 125 and pos[1] < 197):
                    print("COLOUR")
                    scanner.mode = 'Color'
                    go_on = True
                else:
                    print("AREA OUTSIDE MODE BUTTONS PRESSED")
                    go_on = False

    go_on = True

    return()


def SetDPI():

    # Options are 75, 150, 300, 600, put them at top left, bottom left, top right, bottom right
    screen.fill(BLACK)
    pygame.display.flip()
    DPI_75_obj = med_font.render(DPI_75_TXT, True, WHITE, BLACK)
    DPI_75_rect = DPI_75_obj.get_rect()
    DPI_75_rect.topleft = (0,0)
    screen.blit(DPI_75_obj, DPI_75_rect)

    DPI_150_obj = med_font.render(DPI_150_TXT, True, WHITE, BLACK)
    DPI_150_rect = DPI_150_obj.get_rect()
    DPI_150_rect.bottomleft = (0,320)
    screen.blit(DPI_150_obj, DPI_150_rect)

    DPI_300_obj = med_font.render(DPI_300_TXT, True, WHITE, BLACK)
    DPI_300_rect = DPI_300_obj.get_rect()
    DPI_300_rect.topright = (480,0)
    screen.blit(DPI_300_obj, DPI_300_rect)

    DPI_600_obj = med_font.render(DPI_600_TXT, True, WHITE, BLACK)
    DPI_600_rect = DPI_600_obj.get_rect()
    DPI_600_rect.bottomright = (480,320)
    screen.blit(DPI_600_obj, DPI_600_rect)

    pygame.display.flip()
    print("E")
    pygame.event.clear()

    go_on = False

    while go_on == False:

        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.unicode == 'z':
                    pygame.quit()
                    sys.exit
            if event.type == pygame.MOUSEBUTTONUP:
                pos = pygame.mouse.get_pos()
                pygame.event.clear()
                if (pos[0] > 0 and pos[0] < 240 and pos[1] > 0 and pos[1] < 160):
                    print("75DPI")
                    scanner.resolution = 75
                    go_on = True
                elif (pos[0] > 0 and pos[0] < 240 and pos[1] > 161 and pos[1] < 320):
                    print("150DPI")
                    scanner.resolution = 150
                    go_on = True
                elif (pos[0] > 241 and pos[0] < 479 and pos[1] > 0 and pos[1] < 160):
                    print("300DPI")
                    scanner.resolution = 300
                    go_on = True
                elif (pos[0] > 241 and pos[0] < 479 and pos[1] > 161 and pos[1] < 320):
                    print("600DPI")
                    scanner.resolution = 600
                    go_on = True

                else:
                    print("AREA OUTSIDE MODE BUTTONS PRESSED")
                    go_on = False

    time.sleep(1)
    go_on = True

    return()


def SetSize():
    # Options are Letter (8.5x11") and full bed (8.5x11.7)
    # br_x = 216.0699920654297 for both
    # br_y = 279.4 for Letter and 297 for full bed
    screen.fill(BLACK)
    pygame.display.flip()


    SIZE_LETTER_obj = med_font.render(SIZE_LETTER_TXT, True, WHITE, BLACK)
    SIZE_LETTER_rect = SIZE_LETTER_obj.get_rect()
    SIZE_LETTER_rect.midleft = (0,160)
    screen.blit(SIZE_LETTER_obj, SIZE_LETTER_rect)

    SIZE_BED_obj = med_font.render(SIZE_BED_TXT, True, WHITE, BLACK)
    SIZE_BED_rect = SIZE_BED_obj.get_rect()
    SIZE_BED_rect.midright = (480,160)
    screen.blit(SIZE_BED_obj, SIZE_BED_rect)

    pygame.display.flip()
    pygame.event.clear()

    go_on = False

    while go_on == False:

        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.unicode == 'z':
                    pygame.quit()
                    sys.exit
            if event.type == pygame.MOUSEBUTTONUP:
                pos = pygame.mouse.get_pos()
                pygame.event.clear()
                if (pos[0] > 0 and pos[0] < 240 and pos[1] > 0 and pos[1] < 320):
                    print("8.5x11")
                    scanner.br_x = 216.0699920654297
                    scanner.br_y = 279.4
                    go_on = True
                elif (pos[0] > 240 and pos[0] < 480 and pos[1] > 0 and pos[1] < 320):
                    print("8.5x11.7")
                    scanner.br_x = 216.0699920654297
                    scanner.br_y = 297.0
                    go_on = True

                else:
                    print("AREA OUTSIDE MODE BUTTONS PRESSED")
                    go_on = False

    time.sleep(1)
    go_on = True

    return()


def SetFileType():

    screen.fill(BLACK)
    pygame.display.flip()


    OUTPUT_PNG_obj = med_font.render(OUTPUT_PNG_TXT, True, WHITE, BLACK)
    OUTPUT_PNG_rect = OUTPUT_PNG_obj.get_rect()
    OUTPUT_PNG_rect.topleft = (0,0)
    screen.blit(OUTPUT_PNG_obj, OUTPUT_PNG_rect)

    OUTPUT_JPG_obj = med_font.render(OUTPUT_JPG_TXT, True, WHITE, BLACK)
    OUTPUT_JPG_rect = OUTPUT_JPG_obj.get_rect()
    OUTPUT_JPG_rect.bottomleft = (0,320)
    screen.blit(OUTPUT_JPG_obj, OUTPUT_JPG_rect)


    pygame.display.flip()
    pygame.event.clear()

    go_on = False

    while go_on == False:

        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.unicode == 'z':
                    pygame.quit()
                    sys.exit
            if event.type == pygame.MOUSEBUTTONUP:
                pos = pygame.mouse.get_pos()
                pygame.event.clear()
                if (pos[0] > 0 and pos[0] < 480 and pos[1] > 0 and pos[1] < 160):
                    print("PNG SCAN")
                    screen.fill(BLACK)

                    SCANNING_obj = med_font.render(SCANNING_TXT, True, WHITE, BLACK)
                    SCANNING_rect = SCANNING_obj.get_rect()
                    SCANNING_rect.center = (240,160)
                    screen.blit(SCANNING_obj, SCANNING_rect)
                    pygame.display.flip()
                    scanner.start()
                    im = scanner.snap()
                    # Now save the scanned image
                    screen.fill(BLACK)
                    SAVING_obj = med_font.render(SAVING_TXT, True, WHITE, BLACK)
                    SAVING_rect = SAVING_obj.get_rect()
                    SAVING_rect.center = (240,160)
                    screen.blit(SAVING_obj, SAVING_rect)
                    pygame.display.flip()
                    filename = '/home/pi/scans/scan_share/' + time.strftime('%Y-%m-%d_%H%M-%S') + '.png'
                    im.save(filename)
                    time.sleep(0.5)

                    go_on = True
                elif (pos[0] > 0 and pos[0] < 480 and pos[1] > 160 and pos[1] < 320):
                    print("JPG SCAN")
                    screen.fill(BLACK)

                    SCANNING_obj = med_font.render(SCANNING_TXT, True, WHITE, BLACK)
                    SCANNING_rect = SCANNING_obj.get_rect()
                    SCANNING_rect.center = (240,160)
                    screen.blit(SCANNING_obj, SCANNING_rect)
                    pygame.display.flip()
                    scanner.start()
                    im = scanner.snap()
                    # Now save the scanned image
                    screen.fill(BLACK)
                    SAVING_obj = med_font.render(SAVING_TXT, True, WHITE, BLACK)
                    SAVING_rect = SAVING_obj.get_rect()
                    SAVING_rect.center = (240,160)
                    screen.blit(SAVING_obj, SAVING_rect)
                    pygame.display.flip()
                    filename = '/home/pi/scans/scan_share/' + time.strftime('%Y-%m-%d_%H%M-%S') + '.jpg'
                    im.save(filename)
                    print(filename)

                    go_on = True

                else:
                    print("AREA OUTSIDE MODE BUTTONS PRESSED")
                    go_on = False

    time.sleep(1)
    go_on = True

    return()


# *****FUNCTIONS END HERE*****


boot_text_obj = med_font.render(BOOT_TXT, True, WHITE, BLACK)
boot_text_rect = boot_text_obj.get_rect()
boot_text_rect.center = (240, 160)
screen.blit(boot_text_obj, boot_text_rect)
pygame.display.flip()
time.sleep(2)


# Main loop here

while True:

    screen.fill(BLACK)
    start_text_obj = med_font.render('TAP TO START', True, MED_GREY, BLACK)
    start_text_rect = start_text_obj.get_rect()
    start_text_rect.center = (240, 160)

    screen.blit(start_text_obj, start_text_rect)

    pygame.display.flip()

    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:
            if event.unicode == 'z':
                pygame.quit()
                sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            screen.fill(BLACK)
            scanner_check_text_obj = med_font.render(SCANNER_CHECK_TXT, True, LT_GREY, BLACK)
            scanner_check_text_rect = scanner_check_text_obj.get_rect()
            scanner_check_text_rect.center = (240,160)
            screen.blit(scanner_check_text_obj, scanner_check_text_rect)
            pygame.display.flip()
            pygame.event.clear()
            sane.init()
            devices = sane.get_devices()
            print("A")
            if (devices == []):
                go_on = False
                screen.fill(BLACK)
                err_text_obj = small_font.render('SCANNER NOT FOUND', True, RED, BLACK)
                err_text_rect = err_text_obj.get_rect()
                err_text_rect.center = (240,160)
                screen.blit(err_text_obj, err_text_rect)
                pygame.display.flip()
                time.sleep(5)
                print("B")
            else:
                go_on = True
                print("C")

            if (go_on == True):
                print("D")
                # If we're here, open the scanner ask for and set the mode ('Lineart', 'Gray', 'Color')
                scanner = sane.open(devices[0][0])
                SetMode()

                print(scanner.mode)

                # If we're here, ask for and set the DPI (75, 150, 300, 600)    
                SetDPI()
                print("E")
                print(scanner.resolution)

                # If we're here, ask for and set the scan size (Letter, Full Bed)
                SetSize()
                print("F")
                print(scanner.area)

                # If we're here, ask for and set the output format (JPG, PNG), do the scan, and
                # save the file.
                SetFileType()
                print("G")
                #print(filename)


                #If we're here, check to see if another scan should be done with same settings
                # ANOTHER_TXT, ANOTHER_YES_TXT, ANOTHER_NO_TXT
                while scan_again == True:
                    screen.fill(BLACK)
                    ANOTHER_obj = small_font.render(ANOTHER_TXT, True, WHITE, BLACK)
                    ANOTHER_rect = ANOTHER_obj.get_rect()
                    ANOTHER_rect.midtop = (240,0)
                    screen.blit(ANOTHER_obj, ANOTHER_rect)

                    ANOTHER_YES_obj = big_font.render(ANOTHER_YES_TXT, True, BLACK, WHITE)
                    ANOTHER_YES_rect = ANOTHER_YES_obj.get_rect()
                    ANOTHER_YES_rect.bottomleft = (0,320)
                    screen.blit(ANOTHER_YES_obj, ANOTHER_YES_rect)

                    ANOTHER_NO_obj = big_font.render(ANOTHER_NO_TXT, True, BLACK, WHITE)
                    ANOTHER_NO_rect = ANOTHER_NO_obj.get_rect()
                    ANOTHER_NO_rect.bottomright = (480,320)
                    screen.blit(ANOTHER_NO_obj, ANOTHER_NO_rect)

                    pygame.display.flip()

                    #pygame.event.clear()

                    for event in pygame.event.get():
                        if event.type == pygame.KEYDOWN:
                            if event.unicode == 'z':
                                print("H")
                                pygame.quit()
                                sys.exit
                        if event.type == pygame.MOUSEBUTTONUP:
                            print("I")
                            pos = pygame.mouse.get_pos()
                            pygame.event.clear()
                            if (pos[0] > 0 and pos[0] < 240 and pos[1] > 160 and pos[1] < 320):
                                print("Scanning again with same settings")
                                SetFileType()
                                scan_again = True
                            elif (pos[0] > 240 and pos[0] < 480 and pos[1] > 160 and pos[1] < 320):
                                print("Don't scan again, close scanner and go back to start")
                                scanner.close()
                                scan_again = False

                            else:
                                print("AREA OUTSIDE MODE BUTTONS PRESSED")
                                scan_again = True

All of the print statements are there to leave info on the console for troubleshooting purposes. You can also exit out of the program at any screen by hitting the ‘z’ key (if you have a keyboard connected).

Here’s how it looks while it’s running (note that you have to be booted into the GUI for it to work):

And that’s pretty much what it does at this point. You can probably tell I’m not much of a UI designer, either.

I also needed a different case for this to work, because the case that came with the display only fit the Pi B+/2/3 and not the Pi 4. I came up with a pretty simple case that I’m pleased with, and the display socket fits onto the GPIO pretty much perfectly. I will probably change it again at some point but for now the display is shielded from the heat the Pi throws off, and the Pi case is vented so it doesn’t get too hot in the first place now, either. You can find the case STL files on Thingiverse.

Here it is with the Pi fastened to the lid of the scanner with part of a sticky silicone pad (the tape will be replaced with Command hooks shortly):

To get it to run automatically when the GUI loads, you need to do two things. First, make sure that you configure the Pi to boot to the GUI and log in automatically (you can set that with sudo raspi-config). Second, edit /etc/xdg/lxsession/LXDE-pi/autostart

and add the following line at the end:

@python3 /home/pi/scan-05.py
(or whatever you called your file)

Save, reboot, and the program should start automatically when the Pi loads and logs into the GUI.

I haven’t played with overlay filesystems much, but I enabled it on the Pi with the idea that the whole thing can be turned on and off with just the switch on the power cord (or by pulling the plug) and not damage the filesystem on the SD card. So far it seems to be doing the trick but I haven’t really put it through the gears yet.

So yeah… that’s pretty much it. Still a work in progress, not the prettiest thing, but it seems to work. And now anyone in the house can scan whenever they want to, and we don’t need to worry about finding the scanner and hoping it works again at the end of November.

Thanks to the following people and/or resources:

  • Running something when the GUI boots: Arnold Chan, https://raspberry-projects.com/pi/pi-operating-systems/raspbian/auto-running-programs-gui
  • Setting the SANE scan resolution: various, https://python.hotexamples.com/examples/sane/-/get_devices/python-get_devices-function-examples.html

If you’ve read this far, you’re either one of those bots that scrapes content, or a human that’s pretty bored. This was a fun project, though, and I hope there’s something in here you can use!

Contact Form Problems

Hey folks,

I’ve had a couple of people recently try to use the contact form but it’s been problematic. Unfortunately, I don’t seem to be able to replicate the problem.

If you try to send a note using the contact form and it doesn’t work (and if you have an extra minute or so), please let me know via comment below what error you’re getting and any other details that might help (like which browser you’re using).

Sorry for the inconvenience!

10YQM / NT64 COB LEDs

Some months ago a local store had a whole pile of these AAA battery powered LED lamps that you could mount pretty much anywhere:

LED lamp that looks like a light switch
Back of LED lamp that looks like a light switch

They weren’t all the same – some had contacts for three AAA batteries, some had contacts for four, and there were a couple of different brands. They all looked pretty much the same, though, and were surprisingly bright (some had cheap batteries in them from the factory I guess) and really, REALLY inexpensive.

I bought a few of them, took them home, and puttered around with them. Some had a resistor in them and some didn’t. I tried running one off my bench supply set to the right voltage and they started to smoke. Even with a set of AA batteries instead of AAA they got pretty hot. So… not only were they cheap, they were counting on folks using AAA batteries with a particular current capacity/internal resistance. Not sure if that’s clever engineering or just super cheap.

I was really impressed with those Chip on Board (COB) LEDs, though. Even with the batteries from the store in them they were crazy bright. I went back to the store and bought out the rest they had, thinking I’d take them apart and use the LEDs for other purposes.

And then they sat.

Fast-forward to a couple of days ago. I have this snazzy little gooseneck-mounted magnifier and wanted to turn it into an illuminated one. I tried a couple of different approaches and wasn’t happy with them. Then I remembered those switch lamp things.

Taking them apart is easy, it’s just six screws on the back (two are under the sticker), and then the whole thing pretty much falls apart until you’re left with this:

LED lamp disassembly showing internal parts

A slide switch, some wires, two COB LEDs mounted to a plastic reflector, and sometimes a resistor (this one didn’t have one).

Snip the wires and the plastic “rivets” and the LEDs come off easily:

LED lamp showing removed switch and LEDs

Notice how there is one red wire, one blue wire, and two white wires? There is a positive and negative terminal on both ends of the LEDs. Both positive terminals are connected, as are both negative terminals, so you can chain the LEDs together however you want. In this lamp’s case, they were wired in parallel.

There are two markings that I could find on the LEDs in this lamp and on several others that I tried – “10YQM” and “NT64”:

COB LED 10YQM
LED COB NT64

Each COB has ten LEDs in it and the back of the COB is a nice little aluminum strip that will carry away some heat. Unfortunately, I had no idea how much heat, or how much current would generate how much heat, because I couldn’t find any datasheets (or even information) on these devices.

So, bench time! I set up an LM317 as a constant current source and, starting from 50mA, slowly increased the current until the voltage across the device stabilized, then increased it some more until the aluminum got just slightly warm to the touch. What I ended up with were the following numbers (tested on six of these devices):

10YQM/NT64 COB LED
Forward voltage: 2.7-2.8V @ 150mA
Forward current (sustained, no external cooling required): ~150mA
Maximum current tested: ~350mA for less than 10 seconds (got hot pretty quick)

I’ve had them running for five hours at a time at 125mA with no problem, and at that power, four of them are plenty bright enough for a desk lamp:

Desk lamp made with 4x LED COBs
It’s too bright to look at it with my eyes but I like this picture because the short exposure shows the individual LEDs in the COB.

They’re wired in series and held on with some masking tape (you can see it in the picture) but I’m printing a proper holder for them right now. Maybe it’ll be the subject of another post.

Anyway, if you have any of those LED switch lamps or some of those 10YQM/NT64 COB LEDs, good news – they’re nice and bright and easy to use!

Removing IR Filter From ESP32-Cam, Part III

Okay, so it’s been a while but I finally got a chance to tinker with the ESP32-Cam in the dark again. 🙂

I’m using the same kind of IR illuminator that I used last time. You can get them all over the place. These little guys:

IR Illuminator for Raspberry Pi Camera
Image taken from https://www.amazon.ca/s?k=raspberry+pi+ir+illuminator

I went back over what I’d done last time and looked at my notes that I’d made after. The last time, I thought I’d play it safe and use a constant-current source to power the LED. I’ve had good results in the past using an LM317 to do this quickly and cheaply, so that’s the way I went, limiting the current to 125mA.

Turns out, though, that I was underpowering the illuminator because not only is it capable of more than 125mA, it was only getting around 2.4V because I was powering the LM317 off a 5V supply instead of the 12V I should’ve used.

This time around, I changed how I powered it. After a bit of poking around, it seems the illuminator runs on a 3.3V supply. I wanted to run it off a 5V USB pack because it’s so convenient, so I cheated and just put two 1N4001 diodes in series between +5V and the illuminator to get down to around 3.3V:

Two diodes in series on a breadboard

I also ended up adding a 1uF tantalum capacitor between the GND and +5V pins of the USB connector to cut down on some of the noise.

There is a tiny potentiometer on the module that supposedly controls at which point the module will turn on the LED, but it seems that it controls the current through the LED and has very little (if anything) to do with the amount of light on the photoresistor. Turning it all the way in one direction (clockwise, I think) gives a current of about 65mA through the LED, while all the way the other direction gives a current of around 420mA.

Unless you attach a heat sink, DO NOT RUN THESE IR ILLUMINATORS AT FULL POWER. It didn’t take too long before I could smell that all too familiar burning electronics smell, and the module itself failed shortly after.

I took another one, covered the photoresistor, and dialed it in to about 300mA. In this case, with unrestricted natural convection, it was able to run for over five hours before I turned it off. It was hot to the touch, but not unbearably so. Yours will probably be different, so keep an eye on it!

Here’s what happened with the current as the time went on:

Current reading through USB port
Current reading through USB port
Current reading through USB port
Current reading through USB port

So in the first 20 minutes or so the current climbed from 295mA to 325mA and then stayed there for the nest four hours and 50 minutes. For the entire time, the voltage across the module was 3.2V:

Voltage across IR illuminator

So, 325mA at 3.2V works out to 1.04W. That’s a lot better than the 0.3W from the previous setup. The advertising for these modules claims 3W, but I think it’s assuming you’re using both at the same time (they all come in 2-packs) and are running them with the pot dialed to full power.

Yes, there are problems with how I did this. Using two diodes isn’t a great way to “set” the voltage. Those USB meters are notorious for being inaccurate, too. So again, if you decide to try this, keep an eye on your stuff!

So the big question then, is: does running the LED at 3.5x the power make much of a difference? I think it does. Here’s a look from the dining room table with the LED covered up…

Living room, no illumination

… and uncovered:

Living room with IR illumination

Note that surface C is about six feet from the camera. A is ten feet, and B is about 22 feet. This is noticeably better than the last set of tests – the pictures I took inside the hallway with the closed doors last time confined the IR and kept it bouncing around. In this picture, the room is wide open and the pattern on the wall over 20 feet away is still easy to make out.

Here we are back in the hallway like last time. Here it is without IR:

Dark hallway, no IR

… and with IR:

Dark hallway with IR illumination

For reference, here’s what the camera saw when I did this test back in May:

ESP32-Cam

The picture from today with the illuminator running 3.2V@300mA is noticeably brighter and less grainy. This is a good thing!

And I braved a near-solid wall of mosquitoes to try the ESP32-Cam again outside on the deck steps. Here’s a picture with no IR (it’s a little blurry because I was waving my arms trying to keep from being exsanguinated:

Yard at night, no IR

Unfortunately, the bird feeder has moved around the yard a few times since May, so I wasn’t able to use it to compare with last time. However…

Yard at night with IR on

… this time I can see THE FENCE. And the garden. And the solar panel in the garden. And the little weather station thing. Even the eavestrough on the neighbour’s house! Here’s what it looked like last time:

ESP32-Cam

Quite the difference! I’m also only using one of the illuminators – they come in pairs. Twice the output won’t correspond to twice the brightness in the camera, but it will certainly lighten things up even more.

The ESP32-Cam with the IR filter removed and this particular illuminator do not put on a stellar performance, but I’m comfortable saying that they works well indoors, and they’re decent outside, too. Not good enough to, say, catch a license plate of a car driving by, but just fine within six to ten feet.

I still plan to put one of these outside, but getting a wifi signal out there is pretty tricky. I’ll either have to move the router or add something else to the network here. That might end up being a whole other project.

Thanks to everyone who commented and sent messages – I appreciate the feedback!