# Quick and dirty web image optimization

Given a large pile of images that nominally live on a web server, I want to make them smaller and more friendly to serve to clients. This is hardly novel: for example, Google offer detailed advice on reducing the size of images for the web. I have mostly JPEG and PNG images, so, jpegtran and optipng are the tools of choice for bulk lossless compression.

To locate and compress images, I’ll use GNU find and parallel to invoke those tools. For JPEGs I take a simple approach, preserving comment tags and creating a progressive JPEG (which can be displayed at a reduced resolution before the entire image has been downloaded).

find -iname '*.jpg' -printf '%P,%s\n' \
-exec jpegtran -o -progressive -copy comments -outfile {} {} \; \
> delta.csv


Where things get a little more interesting is when I output the name and size of each located file (-printf ...) and store those in a file (> delta.csv).1 This is so I can collect more information about the changes that were made.

## Check the JPEGs

Loading up a Jupyter notebook for quick computations driven with Python, I load the file names and original sizes, adding the current size of each listed file to my dataset.

import os
deltas = []

with open('delta.csv', 'r') as f:
for line in f:
name, _, size = line.partition(',')
size = int(size)
newsize = os.stat(name).st_size
deltas.append((name, size, newsize))

From there, it’s quick work to do some list comprehensions and generate some overall statistics:

original_sizes = [orig for (_, orig, _) in deltas]
final_sizes = [new for (_, _, new) in deltas]
shrinkage = [orig - new for (_, orig, new) in deltas]

pct_total_change = 100 * (sum(original_sizes) -
sum(final_sizes)) / sum(original_sizes)

pct_change = [shrinkage /
orig for (shrinkage, orig) in zip(shrinkage, original_sizes)]
avg_pct_change = 100 * sum(pct_change) / len(pct_change)

print('Total size reduction:', sum(shrinkage),
'bytes ({}%)'.format(round(pct_total_change, 2)))
avg = sum(shrinkage) / len(shrinkage)
print('Average reduction per file:', avg,
'bytes ({}%)'.format(round(avg_pct_change, 2)))

This is by no means good code, but that’s why this short write-up is “quick and dirty”. Over the JPEGs alone, I rewrote 2162 files, saving 8820474 bytes overall; about 8.4 MiB, 8.02% of the original size of all of the files- a respectable savings for exactly zero loss in quality.

## PNGs

I processed the PNGs in a similar fashion, having optipng emit interlaced PNGs which can be displayed at reduced resolution without complete data and try more combinations of compression parameters than it usually would, trading CPU time for possible gains. There was no similar tradeoff with the JPEGs, since apparently jpegtran’s optimization of encoding is entirely deterministic whereas optipng relies on experimental compression of image data with varying parameters to find a minimum size.

Since I observed that many of the PNGs were already optimally-sized and interlacing made them significantly larger (by more than 10% in many cases), I also considered only files that were more than 256 KiB in size. To speed up processing overall, I used parallel to run multiple instances of optipng at once (since it’s a single-threaded program) to better utilize the multicore processors at my disposal.

find -iname '*.png' -size +256k -printf '%P,%s\n' \
-exec parallel optipng -i 1 -o 9 ::: {} + \
> delta.csv


Running the same analysis over this output, I found that interlacing had a significant negative effect on image size. There were 68 inputs larger than 256 KiB, and the size of all of the files increased by 2316732 bytes; 2.2 MiB, nearly 10% of the original size.

A few files had significant size reductions (about 300 KiB in the best case), but the largest increase in size had similar magnitude. Given the overall size increase, the distribution of changes must be skewed towards net increases.

### Try again

Assuming most of the original images were not interlaced (most programs that write PNG don’t interlace unless specifically told to) and recognizing that interlaced PNGs tend to compress worse, I ran this again but without interlacing (-i 0) and selecting all files regardless of size.

The results of this second run were much better: over 3102 files, save 12719421 bytes (12.1 MiB), 15.9% of the original combined size. One file saw a whopping 98% reduction in size, from 234 KB to only 2914 bytes- inspecting that one myself, the original was inefficiently coded 32 bits per pixel (8-bit RGBA), and it was reduced to two bits per pixel. I expect a number of other files had similar but less dramatic transformations. Some were not shrunk at all, but optipng is smart enough to skip rewriting those so it will never make a file larger- the first run was an exception because I asked it to make interlaced images.

## That’s all

I saved about 20 MiB for around 5000 files- not bad. A copy of the notebook I used to do measurements is available (check out nbviewer for an online viewer), useful if you want to do something similar with your own images. I would not recommend doing it to ones that are not meant purely for web viewing, since the optimization process may strip metadata that is useful to preserve.

1. Assumption: none of the file names contain commas, since I’m calling this a CSV (comma-separated values) file. It’s true in this instance, but may not be in others.

[return]

# HodorCSE

Localization of software, while not trivial, is not a particularly novel problem. Where it gets more interesting is in resource-constrained systems, where your ability to display strings is limited by display resolution and memory limitations may make it difficult to include multiple localized copies of any given string in a single binary. All of this is then on top of the usual (admittedly slight in well-designed systems) difficulty in selecting a language at runtime and maintaining reasonably readable code.

This all comes to mind following discussion of providing translations of Doors CSE, a piece of software for the TI-84+ Color Silver Edition1 that falls squarely into the “embedded software” category. The simple approach (and the one taken in previous versions of Doors CS) to localizing it is just replacing the hard-coded strings and rebuilding.

As something of a joke, it was proposed to make additional “joke” translations, for languages such as Klingon or pirate. I proposed a Hodor translation, along the lines of the Hodor UI patch2 for Android. After making that suggestion, I decided to exercise my skills a bit and actually make one.

## Hodor (Implementation)3

Since I don’t have access to the source code of Doors CSE, I had to modify the binary to rewrite the strings. Referring the to file format guide, we are aware that TI-8x applications are mostly Intel hex, with a short header. Additionally, I know that these applications are cryptographically signed which implies I will need to resign the application when I have made my changes.

### Dumping contents

I installed the IntelHex module in a Python virtualenv to process the file into a format easier to modify, though I ended up not needing much capability from there. I simply used a hex editor to remove the header from the 8ck file (the first 0x4D bytes).

Simply trying to convert the 8ck payload to binary without further processing doesn’t work in this case, because Doors CSE is a multipage application. On these calculators Flash applications are split into 16-kilobyte pages which get swapped into the memory bank at 0x4000. Thus the logical address of the beginning of each page is 0x4000, and programs that are not aware of the special delimiters used in the TI format (to delimit pages) handle this poorly. The raw hex file (after removing the 8ck header) looks like this:

:020000020000FC
:20400000800F00007B578012010F8021088031018048446F6F727343534580908081020382
:2040200022090002008070C39D40C39A6DC3236FC30E70C3106EC3CA7DC3FD7DC3677EC370
:20404000A97EC3FF7EC35D40C35D40C33D78C34E78C36A78C37778C35D40C3A851C9C940F3
:2040600001634001067001C36D00CA7D00BC6E00024900097A00E17200487500985800BDF8
[snip]
:020000020001FB
:20402000EF7D4721B98411AE84010900EDB0EFAA4AC302723A9B87B7CA4940FE01CA4340B9
:20404000C30272CD4F40C30272CDB540C30272EF67452100002275FE3EA03273FECD63405B


Lines 1 and 7 here are the TI-specific page markers, indicating the beginning of pages 0 and 1, respectively. The lines following each of those contain 32 (20 hex) bytes of data starting at address 0x40000 (4000). I extracted the data from each page out to its own file with a text editor, minus the page delimiter. From there, I was able to use the hex2bin.py script provided with the IntelHex module to create two binary files, one for each page.

### Modifying strings

With two binary files, I was ready to modify some strings. The calculator’s character set mostly coincides with ASCII, so I used the strings program packaged with GNU binutils to examine the strings in the image.

$strings page00.bin HDoorsCSE ##6M#60> oJ:T Uo& dQ:T [snip] xImprove BASIC editor Display clock Enable lowercase Always launch Doors CSE Launch Doors CSE with PRGM]  With some knowledge of the strings in there, it was reasonably short work to find them with a hex editor (in this case I used HxD) and replace them with variants on the string “Hodor”. I also found that page 1 of the application contains no meaningful strings, so I ended up only needing to examine page 0. Some of the reported strings require care in modification, because they refer to system-invariant strings. For example, “OFFSCRPT” appears in there, which I know from experience is the magic name which may be given to an AppVar to make the calculator execute its contents when turned off. Thus I did not modify that string, in addition to a few others (names of authors, URLs, etc). ### Repacking I ran bin2hex.py to convert the modified page 0 binary back into hex, and pasted the contents of that file back into the whole-app hex file (replacing the original contents of page 0). From there, I had to re-sign the binary.4 WikiTI points out how easy that process is, so I installed rabbitsign and went on my merry way: $ rabbitsign -g -r -o HodorCSE.8ck HodorCSE.hex


### Testing

I loaded the app up in an emulator to give it a quick test, and was met by complete nonsense, as intended.

I’m providing the final modified 8ck here, for the amusement of my readers. I don’t suggest that anybody use it seriously, not for the least reason that I didn’t test it at all thoroughly to be sure I didn’t inadvertently break something.

## Extending the concept

It’s relatively easy to extend this concept to the calculator’s OS as well (and in fact similar string replacements have been done before) with the OS signing keys in hand. I lack the inclination to do so, but surely somebody else would be able to do something fun with it using the process I outlined here.

1. That name sounds stupider every time I write it out. Henceforth, it’s just “the CSE.”

[return]
2. The programmer of that one took is surprisingly far, such that all of the code that feasibly can be is also Hodor-filled.

[return]
3. Hodor hodor hodor hodor. Hodor hodor hodor. [return]
4. This signature doesn’t identify the author, as you might assume. Once upon a time TI provided the ability for application authors to pay some amount of money to get a signing key associated with them personally, but that system never saw wide use. Nowadays everybody signs their applications with the public “freeware” keys, just because the calculator requires that all apps be signed and the public keys must be stored on the calculator (of which the freeware keys are preinstalled on all of them).

[return]

# Reverse-engineering Ren'py packages

Some time ago (September 3, 2013, apparently), I had just finished reading Analogue: A Hate Story (which I highly recommend, by the way) and was particularly taken with the art. At that point it seems my engineer’s instincts kicked in and it seemed reasonable to reverse-engineer the resource archives to extract the art for my own nefarious purposes.

A little examination of the game files revealed a convenient truth: it was built with Ren’Py, a (open-source) visual novel engine written in Python. Python is a language I’m quite familiar with, so the actual task promised to be well within my expertise.

## Code

Long story short, I’ve build some rudimentary tools for working with compiled Ren’py data. You can get it from my repository on BitBucket. Technically-inclined readers might also want to follow along in the code while reading.

## Background

There are a large number of games designed with Ren’py. It’s an easy tool to get started with and hack on, since the script language is fairly simple and because it’s open-source, more sophisticated users are free to bend it to their will. A few examples of (in my opinion) high-quality things built with the engine:

Since visual novels tend to live or die on the combination of art and writing, the ability to examine the assets outside the game environment offers interesting possibilities.

Since it was handy, I started my experimentation with Analogue.

## RPA resource archives

The largest files distributed with the game were .rpa files, so I investigated those first for finding art. As it turned out, this was exactly the place I needed to look. Start by examining the raw data:

$cd "Analogue A Hate Story/game"$ ls
bytecode.rpyb
data.rpa
dlc1.rpa
nd.rpa
$less data.rpa RPA-3.0 00000000035f5c75 414154bb <F0><AA><D6>^MީZ<A0><90><FB>^M6<B9><B7>^X<A3><82><F3>F<B0><DF>k8(<BF>ߦx<9C><D5>T [snip] There’s an obvious file identifier (RPA-3.0), followed by a couple numbers and a lot of compressed-looking data. The first number turns out to be very close to the total file size, so it’s probably some size or offset field, while the other one looks like some kind of signature. $ python -c 'print(0x35f5c75)'
56581237
\$ stat -c %s data.rpa
56592058

At this point I simply referred to the Ren’Py source code, rather than waste time experimenting on the data itself. Turns out the first number is the file offset of the index, and the second one is a key used for simple obfuscation of elements of the index (numbers are bitwise exclusive-or’d with the key). The archive index itself is a DEFLATE-compressed block of pickled Python objects. The index maps file names to tuples of offset and block length specifying where within the archive file the data can be found.

With that knowledge in hand, it’s short work to build a decoder for the index data and dump it all to files. This is rpa.py in my tools. Extracting the archives pulls out plenty of images and other media, as well as a number of interesting-looking .rpyb files, which we’ll discuss shortly.

### Cosplay

For a bit of amusement, I exercised my web-programming chops a little and built a standalone web page for playing with the extracted costumes and expressions of *Hyun-ae and *Mute, which I’ve included below. Here’s a link to the bare page for standalone amusement as well.

## Script guts

The basic format of compiled scripts (.rpyb files) is similar to that of resource packages. The entire thing is a tuple of (data, statements), where data is a dictionary of basic metadata and statements is a list of objects representing the script’s code.

The statements in this are just the Ren’py abstract syntax tree, so all the objects come from the renpy.ast module. Unfortunately and as I’ll discuss later, the pickle format makes this representation hard to work with.

The structure of AST members is designed such that each object can have attached bytecode. In practice this appears to never happen in archives. In my investigations of the source, it appears that Ren’py only writes Python bytecode as a performance enhancement, and most of it ends up in bytecode.rpyb. That file appears to provide some sort of bytecode cache that overrides script files in certain situations. For the purposes of reverse-engineering this is fortunate– Python bytecode is documented, but rather more difficult to translate into something human-readable than the source code that is actually present in RPYB archives.

Here’s some of the Act 1 script from Analogue run through the current version of my script decompiler:

## BEGIN Label 'dont_understand', params=None, hide=False
dont_understand:
## <class 'renpy.ast.Show'> -- don't know how to dump this!
## <class 'renpy.ast.Say'> -- don't know how to dump this!
## <class 'renpy.ast.Jump'> -- don't know how to dump this!
## BEGIN Label 'nothing', params=None, hide=False
nothing:
python:
shown_message = None

for block in store.blocks:
for message in block.contents:
if message == _message:
shown_message = str(store.blocks.index(block)+1) + "-" + str(block.contents.index(message)+1)

if shown_message:
gray_out(shown_message)
## <class 'renpy.ast.With'> -- don't know how to dump this!
## <class 'renpy.ast.Show'> -- don't know how to dump this!
## <class 'renpy.ast.With'> -- don't know how to dump this!
## <class 'renpy.ast.Say'> -- don't know how to dump this!


Clearly there are a few things my decompiler needs to learn about. It does, however, handle the more common block elements such as If statements. In any case, the Python code embedded in these scripts tends to be more interesting than the rest (which are mostly just dialogue and display manipulation) for the purposes of reverse-engineering. If you’re more interested in spoiling the game for yourself, it’s not as useful.

A few telling bits of logic from options.rpy:

init -3:
## BEGIN Python
python hide:
## BEGIN PyCode, exec mode, from game/options.rpy
renpy.demo_mode = True
init -1:
## BEGIN Python
python hide:
## BEGIN PyCode, exec mode, from game/options.rpy
config.developer = True

config.screen_width = 1024
if persistent.resolution == None:
persistent.resolution = 2
if persistent.old_resolution == None:
persistent.old_resolution = persistent.resolution

if not persistent.resolution:
config.screen_height = 600
else:
config.screen_height = 640

if renpy.demo_mode:
config.window_title = u"Analogue trial"
else:
config.window_title = u"Analogue: A Hate Story"

config.name = "Analogue"
config.version = "0.0"


The spacing here is interesting; I suspect (but haven’t attempted to verify) that the Ren’py script compiler strips comments since there haven’t been any in all of the scripts I’ve examined, so it’s likely that the unusual empty blocks in the code were comment blocks in a former life.

I’ve yet to dig much into what determines when demo_mode is set, but I doubt it would be difficult to forcibly set (or clear) if one were so inclined. Not that I condone such an action..

A little bit of interesting game-critical logic, also from Analogue (caution: minor spoilers)

store.radiation = 0
store.reactor_enabled = True
def interact_cb():

if (any_read("7-*") or (store.current_character == "mute" and get_m("6-9").enabled)) and store.radiation_levels < 0.65:

config.interact_callbacks.append(interact_cb)

You can get some idea of how specialized Ren’py’s execution environment is from this code. Particularly, store is a magic value injected into the locals of Python script blocks which maps to the RPY persistent variable store, which stores most of the game state. config is a similar magic value providing a handle to the engine configuration.

In this instance, radiation refers to a sort of hidden timer which forces the player to solve a puzzle on expiration (assuming the preconditions have been met), then make a decision which causes the plot to fork depending on that decision. Elsewhere in the code, I found a few developer switches which allow one to display the value of this countdown and reset or force it.

## Conclusions

As the official documentation notes, the process of resource compilation is not very secure but is enough to deter casual copying. I’ve shown here that such a claim is entirely correct, though script decompilation may be somewhat harder than the developers envisioned due to the choice of pickle as a serialization format.

It’s nothing particularly new to me, but a reminder to designers of software: if it runs on your attacker’s system, it can be hacked. It’s not a question of “if”, but instead “how fast”. I was mostly interested in extracting resources with this project, which was quite easy. In that matter, I think the designers of Ren’Py made a good design decision. The compiled archives and scripts are much more robust against accidental modification in the face of curious users than not compiling anything, but the developers do not expend undue effort building something harder to break which would eventually be broken anyway by a sufficiently determined attacker.

### Portability

As I alluded to earlier, the pickle representation makes the Ren’Py AST hard to work with. This is because many of the objects contain references to the engine state, which in turn implies most of the engine needs to be initialized when unpickling the AST. To say the least, this is not easy- engine initialization is not easily separated from game startup.

To illustrate the problem, observe that the Ren’Py developer kit is simply the engine itself packaged with a game of sorts that provides help in getting a new project set up by modifying the included scripts. There simply seems to be no part of the engine that is designed to run without the rest of it running as well.

In experimenting with different products built with Ren’Py, I’ve had to make changes to some combination of the engine itself and my code in order to bootstrap the engine state to a point where the AST can be successfully unpickled. Suffice to say, this has hampered my progress somewhat, and led me to consider slightly different avenues of attack.

The most promising of these would involve a semi-custom unpickler which avoids instantiating actual Ren’Py objects; the only data that need be preserved is the structural information, rather than the many hooks into engine state that are also included in the pickle serialization. Further continuation of this project is likely to take this approach to deserialization.

# Newlib's git repository

Because I had quite the time finding it when I wanted to submit a patch to newlib, there’s a git mirror of the canonical CVS repository for newlib, which all the patches I saw on the mailing list were based off of. Maybe somebody else looking for it will find this note useful:

git clone git://sourceware.org/git/newlib.git


# MAX5214 Eval Board

I caught on to a promotion from AVNet last week, in which one may get a free MAX5214 eval board (available through August 31), so hopped on it because really, why wouldn’t I turn down free hardware? I promptly forgot about it until today, when a box arrived from AVNet.

## What’s on the board

The board features four Maxim ICs:

• MAX8510- small low-power LDO.  Not terribly interesting.
• MAXQ622- 16-bit microcontroller with USB.  I didn’t even know Maxim make microcontrollers!
• MAX5214- 14-bit SPI DAC. The most interesting part.
• MAX6133- precision 3V LDO (provides supply for the DAC)

The MAXQ622 micro (U2) is connected to a USB mini-B port for data, and USB also supplies power for the 5V rail.  The MAX8510 (U4) supplies power for the microcontroller and also the MAX6133 (U3).  The microcontroller acts as a USB bridge to the MAX5214 DAC (U1), which it communicates with over SPI.  The SPI signals are also broken out to a 4-pin header (J6).

## Software

The software included with the board is fairly straightforward, providing a small variety of waveforms that can be generated. It’s best demonstrated photographically, as below. Those familiar with National Instruments’ LabView environment will probably recognize that this interface is actually just a LabView VI (Virtual Instrument).

## Hacking

Rather more interesting than the stock software is the possibility of reprogramming the microcontroller. Looking at the board photos, we can see that there’s a header that breaks out the JTAG signals. With the right tools, it shouldn’t be very difficult to write a custom firmware to open up a communication protocol to the device (perhaps change its device class to a USB CDC for easier interfacing). Reprogramming the device requires some sort of JTAG adapter, but I can probably make a Bus Pirate do the job.

With some custom software, this could become a handy little function generator- its precision is good and it has a handy USB connection. On the downside, the slew rate on the DAC is not anything special (0.5V/µs, -3dB bandwidth is 100 kHz), and its output current rating is pretty pathetic (5 mA typical). With a unity-gain amplifier on the output though, it could easily drive decent loads and act as a handy low-cost waveform generator. Let’s get hacking?

# Divergence meter: high-voltage supply and FET drivers

I got some time to work on the divergence meter project more, now that the new board revision is in.  I assembled the boost converter portion of the circuit and plugged in a signal generator to see what sort of performance I can get out of it.  The bad news: I was rather dumb in choosing a FET, so the one I have is fast, but can’t be driven fully on with my 3.3V MSP430.  Good news is that with 5V PWM input to the FET, I was able to handily get 190V on the Nixie supply rail.

Looking at possible FET replacements, I discovered that my choice of part, the IRFD220, appears to be the only MOSFET that Mouser sell that’s available in a 4-pin DIP package.  Since it seems incredibly wasteful to create another board revision at this point, I went ahead with designing a daughterboard to plug in where the FET currently does.

I got some ICL7667 FET driver samples from Maxim and have assembled this unit onto some perfboard, but have not yet tested it.  Given I was driving the FET with a 9V square wave while testing, it’s possible that I blew out the timer output to the FET on my microcontroller while testing.  Next time I get to work on this, I’ll be exercising that output to see if I blew it with high voltages, and connecting up the perfboard driver to try the high voltage supply all driven on-board.

# Rewriting SPD

I recently pulled a few SDR (133 MHz) SO-DIMMs out of an old computer.  They sat on my desk for a few days until I came up with a silly idea for something to do with them: rewrite the SPD information to make them only semi-functional- with incorrect timing information, the memory might work intermittently or not at all.

## Background

Most reasonably modern memory modules have a small amount of onboard persistent memory to allow the host (eg your PC) to automatically configure it.  This information is the Serial Presence Detect, or SPD, and it includes information on the type of memory, the timings it requires for correct operation, and some information about the manufacturer.  (I’ve got a copy of the exact specification mirrored here: SPDSDRAM1.2a.)  If I could rewrite the SPD on one of these DIMMs, I could find values that make it work intermittently or not at all, or even report a different size (by modifying the row and column address width parameters).

The SPD memory communicates with the host via SMBus, which is compatible with I2C for my purposes.

## The job

The hardest part of this quest was simply connecting wires to the DIMM in order to communicate with the SPD ROM.  I gutted a PATA ribbon cable for its narrow-gauge wire and carefully soldered them onto the pads on the DIMM.  Per information at pinouts.ru, I knew I needed four connections, given in the table to the right.

Note that the pads are labeled on this DIMM, with pad 1 on the left side, and 143 on the right (the label for 143 is visible in the above photo), so the visible side of the board in this photo contains all the odd-numbered pads.  The opposite side of the board has the even-numbered ones, 2-144.  With the tight-pitch soldering done, I put a few globs of hot glue on to keep the wires from coming off again.

With good electrical connections to the I2C lines on the DIMM, it became a simple matter of powering it up and trying to communicate.  I connected everything to my Bus Pirate and scanned for devices:

I2C>(1)
Found devices at:
0x60(0x30 W) 0xA0(0x50 W) 0xA1(0x50 R)
I2C>


The bus scan returns two devices, with addresses 0x30 (write-only) and 0x50 (read-write).  The presence of a device with address 0x50 is expected, as SPD memories (per the specification) are always assigned addresses in the range 0x50-0x57.  The low three bits of the address are set by the AS0, AS1 and AS2 connections on the DIMM, with the intention that the host assign different values to these lines for each DIMM slot it has.  Since I left those unconnected, it is reasonable that they are all low, yielding an address of 0x50.

A device with address 0x30 is interesting, and indicates that this memory may be writable.  As a first test, however, I read some data out to verify everything was working:

I2C>[0xa0 0][0xa1 rrr]
I2C START CONDITION
WRITE: 0xA0 ACK
WRITE: 0 ACK
I2C STOP CONDITION
I2C START CONDITION
WRITE: 0XA1 ACK


I write 0 to address 0xA0 to set the memory’s address pointer, and read out the first three bytes.  The values (0x80 0x08 0x04) agree with what I expect, indicating the memory has 128 bytes written, is 256 bytes in total, and is type 4 (SDRAM).

Unfortunately, I could only read data out, not write anything, so the ultimate goal of this experiment was not reached.  Attempts to write anywhere in the SPD regions were NACKed (the device returned failure):

I2C>[0xA0 0 0]
I2C START CONDITION
WRITE: 0xA0 ACK
WRITE: 0 ACK
WRITE: 0 NACK
I2C STOP CONDITION
I2C>[0x50 0 0]
I2C START CONDITION
WRITE: 0x50
WRITE: 0 ACK
WRITE: 0 NACK
I2C STOP CONDITION


In the above block, I attempted to write zero to the first byte in memory, which was NACKed.  Since that failed, I tried the same commands on address 0x30, with the same effect.

With that, I admitted failure on the original goal of rewriting the SPD.  A possible further attempt to at least program unusual values to a DIMM could involve replacing the EEPROM with a new one which I know is programmable.  Suitable devices are plentiful- one possible part is Atmel’s AT24C02C, which is available in several packages (PDIP being most useful for silly hacks like this project, simply because it’s easy to work with), and costs only 30 cents per unit in small quantities.