Compare commits

...

2 Commits

Author SHA1 Message Date
Ben V. Brown
bf55d4bb8b fix release 2024-08-22 12:11:15 +10:00
Ben V. Brown
d695535fd4 TS101 rework for Miniware DFU (#44)
* Refactor output to allow turning hex duplication on/off

* Rough out being able to merge image files & firmware files

* Update output_hex.py

* Update TS101 to require merge

* Remove TS101 from CI

* .

* add py3-intelhex

* Update img2logo.py
2024-08-22 12:08:36 +10:00
6 changed files with 229 additions and 144 deletions

View File

@@ -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:

View File

@@ -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 python3-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

View File

@@ -4,14 +4,19 @@ from __future__ import division
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 @@ LCD_PAGE_SIZE = 1024
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]
DFUOutput.writeFile(
output_name + ".dfu",
data,
deviceSettings.IMAGE_ADDRESS,
deviceSettings.DFU_TARGET_NAME,
deviceSettings.DFU_PINECIL_ALT,
deviceSettings.DFU_PINECIL_PRODUCT,
deviceSettings.DFU_PINECIL_VENDOR,
# 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)}"
)
HexOutput.writeFile(output_name + ".hex", data, deviceSettings.IMAGE_ADDRESS)
DFUOutput.writeFile(
output_filename + ".dfu",
binary_blob,
binary_base,
deviceSettings.DFU_TARGET_NAME,
deviceSettings.DFU_ALT,
deviceSettings.DFU_PRODUCT,
deviceSettings.DFU_VENDOR,
)
# 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 parse_commandline():
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 parse_commandline():
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 parse_commandline():
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 @@ if __name__ == "__main__":
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 @@ if __name__ == "__main__":
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 @@ if __name__ == "__main__":
dither=args.dither,
negative=args.negative,
make_erase_image=args.erase,
flip = True,
flip=True,
)

View File

@@ -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 @@ class HexOutput:
) # 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 @@ class HexOutput:
)
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 @@ class HexOutput:
)
)
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, ()))

View File

@@ -1 +0,0 @@

View File

@@ -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/).