Featured

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!

CHALLENGE ACCEPTED: ROBOT SWEEPER, PART VI

So… here we are. It’s been a little over a week since I started this project. Lots of ups, downs, laughs, tears, fist-shaking, face-palming, thumbs-upping, head hung in defeat, and arms raised in victory. I gave up on the Raspberry Pi Zero/Arduino combination (curse you, Timer3!!!) and switched to a Raspberry Pi Pico microcontroller (which, aside from making an LED blink by copy-pasting an example a few months ago, I knew nothing about) and started the electronics from scratch. By yesterday evening, things were moving along again. And in the end…

I failed.

It was pretty close, though. By this morning, my little robot could move around on its own and successfully detect and turn to avoid (some) obstacles using an HC-SR04 ultrasonic rangefinder. I was working on LED rangefinding and cliff sensing when I ran out of time.

Head bowed and properly ashamed, I told my wife that my robot sweeper wasn’t ready. She assured me that it was “cute” and thanked me for not setting the house on fire. Then, a twist: she’s been doing a ton of research into the little robots and based on the layout of our house and the stuff we have kicking around, she’s not sure which one would be a good fit. We even talked about getting a (gasp) cordless stick vacuum instead. Personally, I’d still rather have a robot because neither of us enjoy vacuuming or sweeping (does anyone?) and it’s more likely to get done if a computer does it.

Regardless, the sale is now over and we didn’t buy anything. Which means… I have more time. It’s not really a challenge anymore since we don’t know when the next good sale will be (or even what we want) but as it’s been quite the learning experience and we both agree that having a cheap little sweeper robot can’t hurt, I’m going to keep working on this project.

I have also “discovered” the Pi Pico. I’ve had a couple of them for a while now but since most of the stuff I do is with an Arduino, they’d been sitting around until I had time to tinker with them. Well, in the last 48 hours or so I’ve tinkered with them until my eyes would no longer focus and I’ve got to say that I’m quite pretty extremely impressed. Setting up MicroPython was easy, it’s easy to program with Python, and it has so many peripherals built in (not to mention the PIO stuff which I haven’t tried but looks great) that it’s… well… pretty amazing. And I’ve barely scratched the surface. I’ve got a million other project ideas for it, and it could well become my go-to when I need a microcontroller for something.

With the time constraint relaxed (at least for now), I’m very interested in seeing where this project goes. But now it’s late again and I really, REALLY need to clean my piles of components, wires, and papers off the dining room table before I go to bed.

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!

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!