diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 90e3498..63de1c1 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -14,13 +14,12 @@ jobs: - model: "pinecilv1" - model: "pinecilv2" - model: "mhp30" - - model: "ts101" - model: "s60" fail-fast: true steps: - name: Install dependencies (apk) - run: apk add --no-cache git python3 py3-pip zlib py3-pillow + run: apk add --no-cache git python3 py3-pip zlib py3-pillow py3-intelhex - uses: actions/checkout@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48dd7d5..8f56362 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,69 +1,63 @@ --- - name: "release" - - on: - push: - branches: - - "main" - - jobs: - release: - name: "Release" - runs-on: "ubuntu-22.04" - - steps: - - name: Install dependencies (apk) - run: sudo apt update && sudo apt-get install -y git python3 python3-pillow - - - uses: actions/checkout@v3 - with: - submodules: true - - - name: prep - run: | - mkdir -p /tmp/pinecilv1 && \ - mkdir -p /tmp/pinecilv2 && \ - mkdir -p /tmp/miniware && \ - mkdir -p /tmp/ts101 && \ - mkdir -p /tmp/mhp30 && \ - mkdir -p /tmp/s60 - - - name: build all files for the device - run: | - cd Bootup\ Logos && \ - ./run.sh /tmp/pinecilv1/ -m pinecilv1 && \ - ./run.sh /tmp/pinecilv2/ -m pinecilv2 && \ - ./run.sh /tmp/miniware/ -m miniware && \ - ./run.sh /tmp/ts101/ -m ts101 && \ - ./run.sh /tmp/mhp30/ -m mhp30 && \ - ./run.sh /tmp/s60/ -m s60 - - - name: build logo erase file - run: | - cd Bootup\ Logos && \ - python3 img2logo.py -E erase_stored_image /tmp/pinecilv1/ -m pinecilv1 && \ - python3 img2logo.py -E erase_stored_image /tmp/pinecilv2/ -m pinecilv2 && \ - python3 img2logo.py -E erase_stored_image /tmp/miniware/ -m miniware && \ - python3 img2logo.py -E erase_stored_image /tmp/ts101/ -m ts101 && \ - python3 img2logo.py -E erase_stored_image /tmp/mhp30/ -m mhp30 && \ - python3 img2logo.py -E erase_stored_image /tmp/s60/ -m s60 - - - - name: compress logo files - run: | - zip -rj pinecilv1.zip /tmp/pinecilv1/* && \ - zip -rj miniware.zip /tmp/miniware/* && \ - zip -rj pinecilv2.zip /tmp/pinecilv2/* && \ - zip -rj ts101.zip /tmp/ts101/* && \ - zip -rj mhp30.zip /tmp/mhp30/* && \ - zip -rj s60_s60p.zip /tmp/s60/* - - - uses: "marvinpinto/action-automatic-releases@latest" - with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - automatic_release_tag: "latest" - prerelease: false - title: "Release" - files: | - *.zip - +name: "release" + +on: + push: + branches: + - "main" + +jobs: + release: + name: "Release" + runs-on: "ubuntu-22.04" + + steps: + - name: Install dependencies (apk) + run: sudo apt update && sudo apt-get install -y git python3 python3-pillow py3-intelhex + + - uses: actions/checkout@v3 + with: + submodules: true + + - name: prep + run: | + mkdir -p /tmp/pinecilv1 && \ + mkdir -p /tmp/pinecilv2 && \ + mkdir -p /tmp/miniware && \ + mkdir -p /tmp/mhp30 && \ + mkdir -p /tmp/s60 + + - name: build all files for the device + run: | + cd Bootup\ Logos && \ + ./run.sh /tmp/pinecilv1/ -m pinecilv1 && \ + ./run.sh /tmp/pinecilv2/ -m pinecilv2 && \ + ./run.sh /tmp/miniware/ -m miniware && \ + ./run.sh /tmp/mhp30/ -m mhp30 && \ + ./run.sh /tmp/s60/ -m s60 + + - name: build logo erase file + run: | + cd Bootup\ Logos && \ + python3 img2logo.py -E erase_stored_image /tmp/pinecilv1/ -m pinecilv1 && \ + python3 img2logo.py -E erase_stored_image /tmp/pinecilv2/ -m pinecilv2 && \ + python3 img2logo.py -E erase_stored_image /tmp/miniware/ -m miniware && \ + python3 img2logo.py -E erase_stored_image /tmp/mhp30/ -m mhp30 && \ + python3 img2logo.py -E erase_stored_image /tmp/s60/ -m s60 + + - name: compress logo files + run: | + zip -rj pinecilv1.zip /tmp/pinecilv1/* && \ + zip -rj miniware.zip /tmp/miniware/* && \ + zip -rj pinecilv2.zip /tmp/pinecilv2/* && \ + zip -rj mhp30.zip /tmp/mhp30/* && \ + zip -rj s60_s60p.zip /tmp/s60/* + + - uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "latest" + prerelease: false + title: "Release" + files: | + *.zip diff --git a/Bootup Logos/img2logo.py b/Bootup Logos/img2logo.py index 112dd35..45f777c 100755 --- a/Bootup Logos/img2logo.py +++ b/Bootup Logos/img2logo.py @@ -4,14 +4,19 @@ import argparse import copy import os, sys - +from typing import Optional +from intelhex import IntelHex from output_hex import HexOutput from output_dfu import DFUOutput try: from PIL import Image, ImageOps except ImportError as error: - raise ImportError("{}: {} requres Python Imaging Library (PIL). " "Install with `pip` (pip3 install pillow) or OS-specific package " "management tool.".format(error, sys.argv[0])) + raise ImportError( + "{}: {} requres Python Imaging Library (PIL). " + "Install with `pip` (pip3 install pillow) or OS-specific package " + "management tool.".format(error, sys.argv[0]) + ) VERSION_STRING = "1.0" @@ -22,58 +27,73 @@ DATA_PROGRAMMED_MARKER = 0xAA FULL_FRAME_MARKER = 0xFF -EMPTY_FRAME_MARKER = 0xFE # If this marker is used to start a frame, the frame is a 0-length delta frame +EMPTY_FRAME_MARKER = ( + 0xFE # If this marker is used to start a frame, the frame is a 0-length delta frame +) class MiniwareSettings: IMAGE_ADDRESS = 0x0800F800 DFU_TARGET_NAME = b"IronOS-dfu" - DFU_PINECIL_ALT = 0 - DFU_PINECIL_VENDOR = 0x1209 - DFU_PINECIL_PRODUCT = 0xDB42 + DFU_ALT = 0 + DFU_VENDOR = 0x1209 + DFU_PRODUCT = 0xDB42 + MINIMUM_HEX_SIZE = 4096 + class S60Settings: IMAGE_ADDRESS = 0x08000000 + (62 * 1024) DFU_TARGET_NAME = b"IronOS-dfu" - DFU_PINECIL_ALT = 0 - DFU_PINECIL_VENDOR = 0x1209 - DFU_PINECIL_PRODUCT = 0xDB42 + DFU_ALT = 0 + DFU_VENDOR = 0x1209 + DFU_PRODUCT = 0xDB42 + MINIMUM_HEX_SIZE = 1024 + class TS101Settings: - IMAGE_ADDRESS = 0x08000000 + (126 * 1024) + IMAGE_ADDRESS = 0x08000000 + (99 * 1024) DFU_TARGET_NAME = b"IronOS-dfu" - DFU_PINECIL_ALT = 0 - DFU_PINECIL_VENDOR = 0x1209 - DFU_PINECIL_PRODUCT = 0xDB42 + DFU_ALT = 0 + DFU_VENDOR = 0x1209 + DFU_PRODUCT = 0xDB42 + MINIMUM_HEX_SIZE = 1024 + class MHP30Settings: IMAGE_ADDRESS = 0x08000000 + (126 * 1024) DFU_TARGET_NAME = b"IronOS-dfu" - DFU_PINECIL_ALT = 0 - DFU_PINECIL_VENDOR = 0x1209 - DFU_PINECIL_PRODUCT = 0xDB42 + DFU_ALT = 0 + DFU_VENDOR = 0x1209 + DFU_PRODUCT = 0xDB42 + MINIMUM_HEX_SIZE = 4096 + class PinecilSettings: IMAGE_ADDRESS = 0x0801F800 DFU_TARGET_NAME = b"Pinecil" - DFU_PINECIL_ALT = 0 - DFU_PINECIL_VENDOR = 0x28E9 - DFU_PINECIL_PRODUCT = 0x0189 + DFU_ALT = 0 + DFU_VENDOR = 0x28E9 + DFU_PRODUCT = 0x0189 + MINIMUM_HEX_SIZE = 1024 + -class Pinecilv2Settings: - IMAGE_ADDRESS = (1016 * 1024) # its 2 4k erase pages inset +class Pinecilv2Settings: + IMAGE_ADDRESS = 1016 * 1024 # its 2 4k erase pages inset DFU_TARGET_NAME = b"Pinecilv2" - DFU_PINECIL_ALT = 0 - DFU_PINECIL_VENDOR = 0x28E9 # These are ignored by blisp so doesnt matter what we use - DFU_PINECIL_PRODUCT = 0x0189 # These are ignored by blisp so doesnt matter what we use + DFU_ALT = 0 + DFU_VENDOR = 0x28E9 # These are ignored by blisp so doesnt matter what we use + DFU_PRODUCT = 0x0189 # These are ignored by blisp so doesnt matter what we use + MINIMUM_HEX_SIZE = 1024 -def still_image_to_bytes(image: Image, negative: bool, dither: bool, threshold: int, preview_filename): +def still_image_to_bytes( + image: Image, negative: bool, dither: bool, threshold: int, preview_filename +): # convert to luminance # do even if already black/white because PIL can't invert 1-bit so # can't just pass thru in case --negative flag # also resizing works better in luminance than black/white - # also no information loss converting black/white to grayscale + # also no information loss converting black/white to greyscale if image.mode != "L": image = image.convert("L") # Resize to lcd size using bicubic sampling @@ -144,7 +164,9 @@ def get_screen_blob(previous_frame: bytearray, this_frame: bytearray): return outputData -def animated_image_to_bytes(imageIn: Image, negative: bool, dither: bool, threshold: int, flip_frames): +def animated_image_to_bytes( + imageIn: Image, negative: bool, dither: bool, threshold: int, flip_frames +): """ Convert the gif into our best effort startup animation We are delta-encoding on a byte by byte basis @@ -175,7 +197,9 @@ def animated_image_to_bytes(imageIn: Image, negative: bool, dither: bool, thresh else: delta = frameDuration_ms / frameTiming if delta > 1.05 or delta < 0.95: - print(f"ERROR: You have a frame that is different to the first frame time. Mixed rates are not supported") + print( + f"ERROR: You have a frame that is different to the first frame time. Mixed rates are not supported" + ) sys.exit(-1) print(f"Found {len(frameData)} frames, interval {frameTiming}ms") frameTiming = frameTiming / 5 @@ -183,7 +207,9 @@ def animated_image_to_bytes(imageIn: Image, negative: bool, dither: bool, thresh newTiming = max(frameTiming, 1) newTiming = min(newTiming, 254) - print(f"Inter frame delay {frameTiming} is out of range, and is being adjusted to {newTiming*5}") + print( + f"Inter frame delay {frameTiming} is out of range, and is being adjusted to {newTiming*5}" + ) frameTiming = newTiming # We have now mangled the image into our framebuffers @@ -217,7 +243,8 @@ def animated_image_to_bytes(imageIn: Image, negative: bool, dither: bool, thresh def img2hex( input_filename, - device_model_name:str, + device_model_name: str, + merge_hex_file: Optional[str], preview_filename=None, threshold=128, dither=False, @@ -229,13 +256,13 @@ def img2hex( """ Convert 'input_filename' image file into Intel hex format with data formatted for display on LCD and file object. - Input image is converted from color or grayscale to black-and-white, + Input image is converted from color or greyscale to black-and-white, and resized to fit LCD screen as necessary. Optionally write resized/thresholded/black-and-white preview image to file specified by name. - Optional `threshold' argument 8 bit value; grayscale pixels greater than + Optional `threshold' argument 8 bit value; greyscale pixels greater than this become 1 (white) in output, less than become 0 (black). - Unless optional `dither', in which case PIL grayscale-to-black/white + Unless optional `dither', in which case PIL greyscale-to-black/white dithering algorithm used. Optional `negative' inverts black/white regardless of input image type or other options. @@ -249,13 +276,15 @@ def img2hex( raise IOError('error reading image file "{}": {}'.format(input_filename, e)) if getattr(image, "is_animated", False): - data = animated_image_to_bytes(image, negative, dither, threshold,flip) + data = animated_image_to_bytes(image, negative, dither, threshold, flip) else: if flip: image = image.rotate(180) # magic/required header data = [DATA_PROGRAMMED_MARKER, 0x00] # Timing value of 0 - image_bytes = still_image_to_bytes(image, negative, dither, threshold, preview_filename) + image_bytes = still_image_to_bytes( + image, negative, dither, threshold, preview_filename + ) data.extend(get_screen_blob([0] * LCD_NUM_BYTES, image_bytes)) # Pad up to the full page size @@ -265,7 +294,12 @@ def img2hex( # Set device settings depending on input `-m` argument device_name = device_model_name.lower() - if device_name == "miniware" or device_name == "ts100" or device_name == "ts80" or device_name == "ts80p": + if ( + device_name == "miniware" + or device_name == "ts100" + or device_name == "ts80" + or device_name == "ts80p" + ): deviceSettings = MiniwareSettings elif device_name == "pinecilv1" or device_name == "pinecil": deviceSettings = PinecilSettings @@ -273,6 +307,11 @@ def img2hex( deviceSettings = Pinecilv2Settings elif device_name == "ts101": deviceSettings = TS101Settings + if merge_hex_file is None: + print( + "For the TS101 for compatibility with bugs in the Miniware Loader, you must merge the main firmware with the logo to flash it" + ) + exit(1) elif device_name == "s60": deviceSettings = S60Settings elif device_name == "mhp30": @@ -282,25 +321,75 @@ def img2hex( sys.exit(-1) # Split name from extension so we can mangle in the _L suffix for flipped images - split_name = os.path.splitext( os.path.basename(input_filename)) + split_name = os.path.splitext(os.path.basename(input_filename)) if flip: base = split_name[0] ext = split_name[1] - base =base+"_L" - split_name = [base,ext] - output_name = output_filename_base +split_name[0] +split_name[1] + base = base + "_L" + split_name = [base, ext] + output_name = output_filename_base + split_name[0] + split_name[1] + # If a file has been specified for merging, we want to splice our image data with it + if merge_hex_file is not None: + read_merge_write(merge_hex_file, data, deviceSettings, output_name) + else: + DFUOutput.writeFile( + output_name + ".dfu", + data, + deviceSettings.IMAGE_ADDRESS, + deviceSettings.DFU_TARGET_NAME, + deviceSettings.DFU_ALT, + deviceSettings.DFU_PRODUCT, + deviceSettings.DFU_VENDOR, + ) + + HexOutput.writeFile( + output_name + ".hex", + data, + deviceSettings.IMAGE_ADDRESS, + deviceSettings.MINIMUM_HEX_SIZE, + ) + + +def read_merge_write( + merge_filename: str, image_data: list[int], deviceSettings, output_filename: str +): + """ + Reads in the merge filename as the base object, then inserts the image data. + Then pad-fills the empty space in the binary + """ + base_hex_file = IntelHex(merge_filename) + logo_hex_file = IntelHex() + logo_hex_file.frombytes(image_data, deviceSettings.IMAGE_ADDRESS) + # Merge in the image data, error if collision + base_hex_file.merge(logo_hex_file, overlap="error") + binary_base = base_hex_file.minaddr() + base_hex_file.padding = 0xFF + binary_blob = base_hex_file.tobinarray(start=binary_base) + print( + f"Post-merge output image starts at 0x{binary_base:x}, len {len(binary_blob)}" + ) DFUOutput.writeFile( - output_name + ".dfu", - data, - deviceSettings.IMAGE_ADDRESS, + output_filename + ".dfu", + binary_blob, + binary_base, deviceSettings.DFU_TARGET_NAME, - deviceSettings.DFU_PINECIL_ALT, - deviceSettings.DFU_PINECIL_PRODUCT, - deviceSettings.DFU_PINECIL_VENDOR, + deviceSettings.DFU_ALT, + deviceSettings.DFU_PRODUCT, + deviceSettings.DFU_VENDOR, ) - HexOutput.writeFile(output_name + ".hex", data, deviceSettings.IMAGE_ADDRESS) + # Gap fill any missing segments + # This is required for the TS101 bootloader + segments = base_hex_file.segments() + for seg_pair in zip(segments, segments[1:]): + start = seg_pair[0][1] + end = seg_pair[1][0] + filler = [0xFE] * (end - start) + base_hex_file.frombytes(filler, start) + + with open(output_filename + ".hex", "w") as output: + base_hex_file.write_hex_file(output, eolstyle="CRLF") def parse_commandline(): @@ -325,6 +414,12 @@ def zero_to_255(text): help="filename of image preview", ) + parser.add_argument( + "-M", + "--merge", + help="filename of another hex file to merge with, creating a combined firmware", + ) + parser.add_argument( "-n", "--negative", @@ -332,20 +427,21 @@ def zero_to_255(text): help="photo negative: exchange black and white in output", ) - parser.add_argument( "-t", "--threshold", type=zero_to_255, default=128, - help="0 to 255: gray (or color converted to gray) " "above this becomes white, below becomes black; " "ignored if using --dither", + help="0 to 255: grey (or color converted to grey) " + "above this becomes white, below becomes black; " + "ignored if using --dither", ) parser.add_argument( "-d", "--dither", action="store_true", - help="use dithering (speckling) to convert gray or " "color to black and white", + help="use dithering (speckling) to convert grey or " "color to black and white", ) parser.add_argument( @@ -355,7 +451,7 @@ def zero_to_255(text): help="generate a logo erase file instead of a logo", ) - parser.add_argument("-m", "--model", help="device model name") + parser.add_argument("-m", "--model", help="device model name") parser.add_argument( "-v", "--version", @@ -372,14 +468,16 @@ def zero_to_255(text): args = parse_commandline() if args.preview and os.path.exists(args.preview) and not args.force: - sys.stderr.write('Won\'t overwrite existing file "{}" (use --force ' "option to override)\n".format(args.preview)) + sys.stderr.write( + 'Won\'t overwrite existing file "{}" (use --force ' + "option to override)\n".format(args.preview) + ) sys.exit(1) - print(f"Converting {args.input_filename} => {args.output_filename}") - img2hex( + merge_hex_file=args.merge, input_filename=args.input_filename, output_filename_base=args.output_filename, device_model_name=args.model, @@ -388,10 +486,11 @@ def zero_to_255(text): dither=args.dither, negative=args.negative, make_erase_image=args.erase, - flip = False, + flip=False, ) img2hex( + merge_hex_file=args.merge, input_filename=args.input_filename, output_filename_base=args.output_filename, device_model_name=args.model, @@ -400,5 +499,5 @@ def zero_to_255(text): dither=args.dither, negative=args.negative, make_erase_image=args.erase, - flip = True, + flip=True, ) diff --git a/Bootup Logos/output_hex.py b/Bootup Logos/output_hex.py index b375500..627e205 100644 --- a/Bootup Logos/output_hex.py +++ b/Bootup Logos/output_hex.py @@ -10,7 +10,6 @@ class HexOutput: INTELHEX_END_OF_FILE_RECORD = 0x01 INTELHEX_EXTENDED_LINEAR_ADDRESS_RECORD = 0x04 INTELHEX_BYTES_PER_LINE = 16 - INTELHEX_MINIMUM_SIZE = 4096 @classmethod def split16(cls, word): @@ -53,20 +52,19 @@ def intel_hex_line(cls, record_type, offset, data): ) # low 8 bits @classmethod - def writeFile(cls, file_name: str, data: bytearray, data_address: int): + def writeFile( + cls, + file_name: str, + data: bytearray, + data_address: int, + minimum_hex_file_size: int, + ): """write block of data in Intel hex format""" with open(file_name, "w", newline="\r\n") as output: def write(generator): output.write("".join(generator)) - if len(data) % cls.INTELHEX_BYTES_PER_LINE != 0: - raise ValueError( - "Program error: Size of LCD data is not evenly divisible by {}".format( - cls.INTELHEX_BYTES_PER_LINE - ) - ) - address_lo = data_address & 0xFFFF address_hi = (data_address >> 16) & 0xFFFF @@ -79,7 +77,7 @@ def write(generator): ) size_written = 0 - while size_written < cls.INTELHEX_MINIMUM_SIZE: + while size_written < minimum_hex_file_size: offset = address_lo for line_start in range(0, len(data), cls.INTELHEX_BYTES_PER_LINE): write( @@ -90,8 +88,6 @@ def write(generator): ) ) size_written += cls.INTELHEX_BYTES_PER_LINE - if size_written >= cls.INTELHEX_MINIMUM_SIZE: - break offset += cls.INTELHEX_BYTES_PER_LINE write(cls.intel_hex_line(cls.INTELHEX_END_OF_FILE_RECORD, 0, ())) diff --git a/Documents/Install_Logo b/Documents/Install_Logo deleted file mode 100644 index 8b13789..0000000 --- a/Documents/Install_Logo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/README.md b/README.md index 1b9c24a..491c798 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,10 @@ This includes photographs of hardware, datasheets, schematics, original propriet This repository uses github actions to automagically build the logos for each device. Periodically a "release" will be tagged and pre-compiled logo's will be put there as well to make it easy. - # Boot-Up Logos The IronOS firmware supports a user created bootup logo. -By default, there is _not_ one included in the firmware. This means that once flashed they generally stay. If you want no logo again, you would have to flash a blank image to the bootup logo. +By default, there is _not_ one included in the firmware. This means that once flashed they generally stay. If you want no logo again, you would have to flash a blank image to the bootup logo. - Safe & Fun: will not over write your firmware - Easy install: use dfu tool just like updating firmware (or Pine64 Updater if you have a Pinecil). @@ -22,4 +21,3 @@ There are community logo's already converted and ready to use in [IronOS-Meta/re Download the zip for Pinecil or Miniware and then install using the instructions on the [main IronOS documentation](https://ralim.github.io/IronOS/Logo/). Alternatively if you want to make your own logo files, there is also documentation on how best to do this in the [main IronOS documentation](https://ralim.github.io/IronOS/Logo/). -