Tag Archives: hacking

Reverse engineering Epaper Arduino for own image pusher

To display quotes, changing once per hour.

There is not much to be found for Waveshare 4.2 Epaper.
Except for an Arduino web example.
( see https://www.waveshare.com/wiki/E-Paper_ESP32_Driver_Board )

I reversed engineered the workings, and created a python upload script to push images.

Original workings are a mess.
Per 4 bit of color, high-low switched in a byte.
Black and red separated.
Using a till p encoding over curl commands.

My implementation uses a python script called as:

python3 epaper-pusher.py ~/Downloads/Untitled.png
http://10.1.0.99/EPDI_
30 times something like 
http://10.1.0.99/ppppppppppppppppppppppppppppppppppppppppppppppppppppppaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppiodaLOAD_
http://10.1.0.99/NEXT_
30 times something like
http://10.1.0.99/pbcdefghijjjjjjffffffoooooooaaabbbbbbeeeedddppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppiodaLOAD_
http://10.1.0.99/SHOW_
NOTES:
a = 0000
-
-
-
p = 1111 = 15

30 lines with 1000 bytes ( ending with iodaLOAD_ )

black pixels
first block 1
second block 0

red pixels
first block 0
second block 1

white pixels
first block 1
second block 1

PIXEL Example
RBRB
BWBW

First block 
1010 - letter K
0101 - Letter F - second nibble = white

Second block
0101 - Letter F
1111 - Letter P - second nibble white

Code

from PIL import Image
import numpy
import requests

url="http://10.1.0.99/" 

black_pixels = numpy.zeros((400,300))
red_pixels = numpy.zeros((400,300))



def classify_pixel_color(pixel):
    """
    Classify a pixel as black, white, or red.
    """
    r, g, b = pixel[:3]  # Ignore alpha if present

    # Define thresholds for classification
    if r < 128 and g < 128 and b < 128:
        return 'black'
    elif r > 200 and g > 200 and b > 200:
        return 'white'
    elif r > 128 and g < 100 and b < 100:
        return 'red'
    else:
        return None

def process_image(image_path):
    """
    Process the image and classify its pixels into black, white, or red.
    """
    image = Image.open(image_path)
    image = image.convert("RGB")  # Ensure the image is in RGB mode

    width, height = image.size
    pixel_data = image.load()

    color_counts = {'black': 0, 'white': 0, 'red': 0}

    for y in range (0, 299):
        for x in range (0, 399):
            black_pixels[x][y] = 0
            red_pixels[x][y] = 0

    for y in range(299):
        for x in range(399):
            color = classify_pixel_color(pixel_data[x, y])
            if color:
                color_counts[color] += 1
                if color == 'black':
                    black_pixels[x][y] = 1;
                if color == 'red':
                    red_pixels[x][y] = 1;
                if color == 'white':
                    black_pixels[x][y] = 1;
                    red_pixels[x][y] = 1;

    return color_counts, black_pixels, red_pixels

def number_to_letter(num):
    """
    Translates a number from 0 to 15 into a corresponding letter (a-p).

    Args:
        num (int): The number to translate.

    Returns:
        str: The corresponding letter (a-p).
    """
    if 0 <= num <= 15:
        return chr(ord('a') + num)
    else:
        raise ValueError("Number must be between 0 and 15, inclusive.")

def print_array_in_chunks(array, chunk_size=1001):
    current_chunk = ""
    for item in array:
        # Convert item to string and add to the current chunk
        item_str = str(item)
        if len(current_chunk) + len(item_str) + 1 > chunk_size:
            # Print the current chunk and reset it
            current_chunk += "iodaLOAD_"
            try:
                requests.get(url + current_chunk, verify=False)
                if not response.content:  # Equivalent to expecting an empty reply
                    pass
            except requests.exceptions.RequestException as e:
                # Catch any request-related errors
                pass
            current_chunk = item_str
        else:
            # Append the item to the current chunk
            current_chunk += (item_str)
    current_chunk += "iodaLOAD_"
    # Print any remaining items in the chunk
    if current_chunk:
        try:
            requests.get(url + current_chunk, verify=False)
            if not response.content:  # Equivalent to expecting an empty reply
                pass
        except requests.exceptions.RequestException as e:
            # Catch any request-related errors
            pass
        

def switch_in_pairs(arr):
    # Loop through the array with a step of 2
    for i in range(0, len(arr) - 1, 2):
        # Swap values at index i and i+1
        arr[i], arr[i + 1] = arr[i + 1], arr[i]
    return arr

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("Usage: python3 script.py <image_path>")
        sys.exit(1)

    image_path = sys.argv[1]
    try:
        color_counts, black_pixels, red_pixels = process_image(image_path)
        try:
            requests.get(url + "EPDI_" , verify=False)
            if not response.content:  # Equivalent to expecting an empty reply
                pass
        except requests.exceptions.RequestException as e:
            # Catch any request-related errors
            pass

        
        lines=[]
        for y in range(300):
            for x in range(0,399,4):
                first = red_pixels[x][y]
                second = red_pixels[x+1][y]
                thirth = red_pixels[x+2][y]
                fourth = red_pixels[x+3][y]
                nibble = 0
                if (first ==  1):
                        nibble = nibble + 8
                if (second ==  1):
                        nibble = nibble + 4
                if (thirth ==  1):
                        nibble = nibble + 2
                if (fourth ==  1):
                        nibble = nibble + 1
                lines.append(number_to_letter(nibble))
        switched_array = switch_in_pairs(lines)
        print_array_in_chunks(switched_array)
        try:
            requests.get(url + "NEXT_" , verify=False)
            if not response.content:  # Equivalent to expecting an empty reply
                pass
        except requests.exceptions.RequestException as e:
            # Catch any request-related errors
            pass
        lines=[]
        for y in range(300):
            for x in range(0,399,4):
                first = black_pixels[x][y]
                second = black_pixels[x+1][y]
                thirth = black_pixels[x+2][y]
                fourth = black_pixels[x+3][y]
                nibble = 0
                if (first ==  1):
                        nibble = nibble + 8
                if (second ==  1):
                        nibble = nibble + 4
                if (thirth ==  1):
                        nibble = nibble + 2
                if (fourth ==  1):
                        nibble = nibble + 1
                lines.append(number_to_letter(nibble))
        switched_array = switch_in_pairs(lines)
        print_array_in_chunks(switched_array)

        try:
            requests.get(url + "SHOW_" , verify=False)
            if not response.content:  # Equivalent to expecting an empty reply
                pass
        except requests.exceptions.RequestException as e:
            # Catch any request-related errors
            pass

    except Exception as e:
        pass

Wifi tricks (public and limited networks)

(use your own discretion/risk)

When connecting to public Wi-Fi, watch what you are doing, it can be dangerous.
(Use a VPN whenever you can, like OpenVPN or Zerotier.)

But it also can be fun to have a look on those networks.

Sometimes there are IP camera’s you can find.
Use an App like Android TinyCam to scan for camera’s

I found at least 5 this way. Getting access, is something else.
One had access to RTSP without password. 🙂
But I found IP camera’s in the wild using a default password also.
(Just google for default passwords. Don’t know the brand of the device? Sometimes you can use the OUI (Organizationally Unique Identifier) part of the Mac Address to find the brand)

Other fun things to scan for are devices you can cast to!

At one time I was in Woerden, getting my Car fixed.
I started working on my Laptop using their guest Wi-Fi.
And checking out the network, I saw some TVs with Casting enabled.
Let’s Cast a Youtube video with a fireplace to it.
Next moment, the display behind the desks started playing the video.
The guys behind the desk were not facing the display.

I tried to revert my test, but I could not found/start the original cast stream.
I told them to get this fixed, and the network security.

Another idea is to scan for hidden camera’s in Hotels or B&Bs.
(There are more tricks to find these, like Flir/IR)

Some Hotels or B&B have a paid Wifi or a one device only policy.

Some tricks for that are:

Using a device which acts as an Access Point/Router.

I started using this trick with a Ravpower (RP-WD01)

I used this device to copy my Nikon photos to an external storage device.

I patched the OS on this Linux device.
Now it autocopied files from sdcard to usb-drive when inserted.

But it also could act as an AccessPoint.
Laptops/tablets and phones can connect using this hotspot.

After that I used a WD device in the same way.

After that I made a mini AP using a Raspberry PI.

When connecting with the first device which was a phone, I wanted to switch to an accesspoint.
So I spoofed the MAC address of the my AP, because it was mac-address locked in the B&B’s main access point.

Now it’s even easier, current mobile phone’s have dual Wi-Fi interfaces.
Connecting to an AP and at the same time setting up a hotspot is a breeze.
(Not that this is needed any more, Wi-Fi is not limited to one device any more. And mobile internet is almost everywhere)

Some access points still require payment, or you don’t know the password.

Some tricks below (use wisely):

Access point with a captive portal:
These are not protected initially.
But you have to enter a username/password to gain access to the internet.

  1. Try to start a VPN client (without logging into the captive portal)
    Sometimes those ports are not blocked.
    (Even more change to use UDP instead of TCP, try zerotier)
  2. Sometimes only DNS works though those AP’s.
    Then you could use a DNS tunnel. This is a method to embed your network traffic in DNS packages. (Note: you have to make your own DNS tunnel server!) https://github.com/yarrick/iodine
  3. Copy the Captive Portal website, write some logging code. And start AP using the same SSID you want the credentials for.
    Get close to someone using the real AP, so they try to log into your fake AP, using their credentials.
    (This is also illegal, and I won’t post code to do this.)

Fixing a WD My passport not working in New Zealand

I’ve got a backup disk for my photos.
You can connect it via USB and has a SDCard slot for your camera SDCards.
You can also connect to is using it as a Wifi Access Point.
(You can even install twonky!)

The cardreader should automatically copy a inserted card to its internal harddrive.
I’ve tested this at home, but here in New Zealand it woudn’t work.

So i started investigating.
(I was in the middle of nowhere, so I could not search for answers.)

I’ve got the juice ssh client on my phone.
Connecting to the AP will give you a IP, but whats the IP from de WD?
Just use JuiceSSH to make a local connection (to your android) first.
And type: ip neigh
This will give you the ip from neighbouring devices, thats only one .. the WD.

Connect to the WD with ssh using root@IPNUMMER (password should be same as you configured the device with)

Looking at the logs gave me some idea where the problem was.

It would not mount the sdcard! Why, that one is working.
(In de Nikon Camera and using a cardreader with OTG on my android)

I first wrote my own mounter and copy program. That worked, so i could at least backup my photos.
Now I could search for the problem.

My backup script named “script”

mkdir /tmp/fash ; mount /dev/mmcblk0p1 /tmp/fash mounts okay!

The sdmount.sh has a problem, it uses the timezone in its script.
This will work when in GMT+12 -> GMT-12.
But we are in NZ .. thats GMT+13

Removing some checks in above script (sdmount.sh)
(Adding timeoffset_min=8)
Made the script work for me again.
When I got home from our trip, I flashed a new firmware version on the device, which corrected the problem.

Reverse engineering a alpha ticker led scoller

Cobra from the Icecrew got his hands on some Led scrollers.

But they only had a windows program to controll these, so we started some reverseengineering.

First we had to make a serial cable.
(Now we could easily use a logic analyser with protocol decoding)

Next i made a sniffer .. because we only had a windows program to control the display.
So we used a windows machine to control the display by serial, and using a read line to a linux machine to stiff the serial traffic

After some tinkering .. we got this

So we wrote some software to control it using Linux

Some code

#!/usr/bin/perl
# Get the attention of the sign
print "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
# Tell the sign to print the message
$message = "���������������������    ";
print "\001" . "Z" . "01" . "\002" . "AA" . "\x1B" . " b" . $message . "\004";
#print "\001" . "Z" . "01" . "\002" . "AA" . "\x1B" . " a" . $message . 
#"\004";

::::::::::::::
PERLTESTLED
::::::::::::::
#!/usr/bin/perl
print "\0\0\0\0\0\001" . "Z" . "00" . "\002" . "AA" . "\x1B" . " b" . "Test Message" . "\004";

::::::::::::::
PERLTESTLED2
::::::::::::::
#!/usr/bin/perl
# Get the attention of the sign
print "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
# Tell the sign to print the message
$message = "<<<<<<<<<<<<<<<<";
#print "\001" . "Z" . "01" . "\002" . "AA" . "\x1B" . " b" . $message . "\004";
print "\001" . "Z" . "01" . "\002" . "AA" . "\x1B" . " a" . $message . "\004";

A few days later i wrote a controller using glade. (Could not find more code)

Some time later we found some datasheets which could have helped!