Inside the Einstein: The Disc, a TatungBytes course: XtalDOS listing the first practice disc image, on a rainbow sunburst
← Back to Modules
module
6

Doing It Again

Introduction

You are about to spend a lot of time looking at bytes. Printing them the way Python does - b'\xe5\xe5\xe5...' - is fine for four of them and hopeless for four hundred. What you want is the view every disc tool gives: sixteen bytes to a row, in hex, with their position on the left and any readable text on the right. Building that needs two more ideas from Python - doing something repeatedly, and wrapping a piece of work up under a name - and this section teaches both by building it.

Doing Something For Each One

A for loop runs the same lines once for each item in a collection:

for b in row:
    ...

means "for each byte in row, call it b, and run the indented lines". The indented lines are the loop's body, and as with with in S5, the indent is how Python knows where the body ends.

range makes a collection of whole numbers to loop over. range(3) is 0, 1, 2 - three numbers, starting from 0, stopping before 3. range(start, stop, step) counts from start up to (not including) stop in jumps of step: range(0, 47, 16) is 0, 16, 32 - the start of each row of sixteen.

Choosing

if runs its indented lines only when a condition is true:

if 32 <= b < 127:
    return chr(b)
return "."

32 <= b < 127 is true when b is 32 or more and less than 127. Those are the codes of the printable characters - letters, digits, punctuation and space - and chr(b) turns such a code into its character: chr(72) is "H". Anything else - new lines, E5, zero - shows as a ., so a dump never prints something that upsets the terminal.

Lists

A list is several values in square brackets: [40, 10, 512]. You read an item by position, from 0, exactly as with bytes: numbers[0] is 40. You have been making lists since S5 - bytes([72, 105, 33, 10]) is made from one.

Functions Of Your Own

print, len and hex are functions: named pieces of work you hand values to. You can make your own with def:

def show_byte(b):
    if 32 <= b < 127:
        return chr(b)
    return "."

This defines a function called show_byte that takes one value, b, and returns an answer: the character, or a dot. Defining it does nothing by itself; it runs when you call it - show_byte(72) - and the call is replaced by what it returned. Once a job has a name you can use it as often as you like, and the rest of the program reads more easily for it.

Formatting Numbers

An f-string is text with an f in front, in which anything inside { } is worked out and put into the text:

f"{b:02X}"

The part after the : says how to show it. 02X means "in hex (X, with capital letters), at least 2 digits, padded with 0". So 229 comes out as E5 and 7 as 07. {offset:06X} does the same with six digits, for positions. {hex_part:<48} pads a piece of text with spaces to 48 characters, keeping it to the left (<), so the text column always starts in the same place - even on a short last row.

The code

Save this as dump.py in edisc.

def show_byte(b):
    if 32 <= b < 127:
        return chr(b)
    return "."


def dump(data, start, length):
    for offset in range(start, start + length, 16):
        row = data[offset:offset + 16]
        hex_part = ""
        text_part = ""
        for b in row:
            hex_part = hex_part + f"{b:02X} "
            text_part = text_part + show_byte(b)
        print(f"{offset:06X}  {hex_part:<48} {text_part}")


message = b"Hello, Einstein! Every byte has a number.\r\n\x00\x01\xE5\xFF"
print(len(message), "bytes")
dump(message, 0, len(message))

for n in range(3):
    print("Round", n)

numbers = [40, 10, 512]
print(numbers[0] * numbers[1] * numbers[2])

dump takes three things: the bytes, where to start, and how many to show. Its outer loop steps through the positions sixteen at a time; for each, it slices out that row, then its inner loop adds each byte's two hex digits to hex_part and its character to text_part, and finally it prints the row. hex_part = hex_part + ... means "make hex_part what it was, with this added on the end" - the usual way to build up text a piece at a time.

Run it

python3 dump.py

What you should see

47 bytes
000000  48 65 6C 6C 6F 2C 20 45 69 6E 73 74 65 69 6E 21  Hello, Einstein!
000010  20 45 76 65 72 79 20 62 79 74 65 20 68 61 73 20   Every byte has 
000020  61 20 6E 75 6D 62 65 72 2E 0D 0A 00 01 E5 FF     a number.......
Round 0
Round 1
Round 2
204800

Read the dump row by row. On the left, the position of the row's first byte, in hex: 0, 0x10 (16), 0x20 (32). Then sixteen bytes: 48 is H, 65 is e, 20 is a space. On the third row, 0D 0A are the carriage-return and new-line codes that end a line of text, and 00 01 E5 FF are bytes that are not characters at all: each shows as . on the right. The last row is short - 47 bytes is two rows of sixteen and fifteen over - and the padding keeps its text lined up.

This is the format every dump in this course is shown in, and dump is the function that makes it. Keep dump.py: S7 builds the course's tool on it.

Change one thing

  • Change dump(message, 0, len(message)) to dump(message, 16, 16). Which row do you get, and what does its position say?
  • Change {b:02X} to {b:02x}. What changes? Then to {b:3d}, which means "decimal, 3 wide". Which do you find easier to read?
  • In show_byte, change 127 to 256. What appears in the text column for E5 and FF now - and why did the dump avoid showing them?
  • Change range(3) to range(1, 10, 4). Predict the lines before you run it.

When it goes wrong

SymptomCause
IndentationError: expected an indented block (3.13 adds after 'for' statement on line and a number)The body of a for, if or def was not indented
The text column wanders on the last rowThe :<48 was left out of {hex_part:<48}

Summary

You can now repeat work with for and range, choose with if, keep values in lists, and package a job as a function with def and return. You can format numbers as hex of a fixed width with f-strings. And you have dump, which turns any bytes into the rows of hex this course reads discs with.

Next

S7, Opening A Real Disc Image - the first version of edisc.py, and the first 256 bytes of a disc image, dumped.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.