epdiy icon indicating copy to clipboard operation
epdiy copied to clipboard

Let's parse those waveforms

Open aceisace opened this issue 1 year ago • 49 comments

One of the bottlenecks of this library is the ability to parse vendor wavesforms correctly. The current approach is to convert a .wbf file to a json-like format with the mod from fried-ink, then to use the converter from this repo to convert the json to a header file.

As discussed with @martinberlin and @mcer12 and @vroland, a better approach to convert the waveforms is required. The idea is to have a single parser (preferrably in python) that is able to convert the waveforms from .wbf directly to the header format required by this lib.

After extensive hours of digging into waveforms, I have finally made a python-based waveform parser, that is able to parse the different modes and waveforms for each mode for each temperature-range. The data parsed seems to be correct. I want to share this parser as soon as it's possible to use vendor waveforms directly with this lib.

For that, however, I need help understanding the header file format. While I do consider myself an expert in python, I am by no means a C++ developer. Understanding complex code and c/cpp specific structures is a bit difficult. I need help understanding how to convert a mode and temperature-range specific waveform into the needed header file. So far, the help I have gotten was not sufficient, hence this issue on Github.

So far, my parser can extract the following data from a .wbf file:

{range(0, 3):
{'waveform_hex': ['55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 92},
range(3, 6): 
{'waveform_hex': ['55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 84}, 
range(6, 9): {
'waveform_hex': ['55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
'length': 76}, range(9, 12): {
'waveform_hex': ['55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'],
 'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 72}, 
range(12, 15): {
'waveform_hex': ['55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 64}, 
range(15, 18): {
'waveform_hex': ['55', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 98}, 
range(18, 21): {
'waveform_hex': ['55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 90}, 
range(21, 24): {
'waveform_hex': ['55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 74}, 
range(24, 27): {
'waveform_hex': ['55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 74}, 
range(27, 30): {
'waveform_hex': ['55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 66},
range(30, 33): {
'waveform_hex': ['55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 58}, 
range(33, 38): {
'waveform_hex': ['55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '55', '0', 'aa', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 58}, 
range(38, 43): {
'waveform_hex': ['55', '55', '55', '0', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '0', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 50}, 
range(43, 48): {
'waveform_hex': ['55', '55', '55', '0', 'aa', 'aa', 'aa', '0', '0', '55', '55', '55', '0', 'aa', 'aa', 'aa', '0', 'ff', '1', 'b0', 'b0', '70', '40', '0', '0'], 
'phases': [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2], [0, 0, 0, 0], [3, 3, 3, 3], [1, 0, 0, 0], [0, 0, 3, 2], [0, 0, 3, 2], [0, 0, 3, 1], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], 'length': 50}}

The range specifies for which temperature range these waveforms were made for. These waveforms are for Mode 0 (init). The hex array is basically the exact data of the waveform. The phases are the binary representation of each hex.

My question is, how to get from this format into the required header file?

aceisace avatar Mar 26 '23 14:03 aceisace

Waveform in json as returned by the inkwave mod: sample waveform in json

aceisace avatar Mar 28 '23 22:03 aceisace

Sharing the current version of my waveform parser based on python: Python-based waveform parser

aceisace avatar Mar 30 '23 14:03 aceisace

Referencing #132 as it seems related. I can write an article about waveforms, but in a nutshell, it corresponds to this:

  • header file containing some data about the waveform
  • temperature ranges supported by the display
  • each temperature range has a separate waveform for each mode, e.g. update without refreshing, partial update etc.

Waveforms themselves contain phases, which contain the information about which voltage pattern needs to be applied to a given pixel to convert it from a known grayscale i to a different grayscale j.

In this case '0x55', '0x00', '0xaa' (section of a waveform) The phases are [1, 1, 1, 1], [0, 0, 0, 0], [2, 2, 2, 2], which is basically the binary representation of the above mentioned hex numbers.

However, converting these phases into a format supported by epdiy is something not quite clear to me yet.

aceisace avatar Apr 06 '23 01:04 aceisace

Interesting work here about waveforms. I was reading about this and stumbled on this project NekoInk that describes IWF interchangeable Waveform Format maybe interesting to understand also?

martinberlin avatar May 09 '23 21:05 martinberlin

There's quite a few articles about waveforms, each not being extensive and containing some differences, @martinberlin . What's clear is that the waveforms used to be embedded on chips of the flex-cable in older e-ink displays while now they are provided as .wbf files. These files are only available via signed NDAs with E-ink (or other hacks) so sharing them is a problem by itself. What's more is that the older waveforms had 3-bit lut, while most now have 4-bit luts and the latest ones (incl. the ones supporting colour use 5-bit waveforms). Previous versions had timings, newer ones have hex values with 0,1,2 (3). What makes this parser a bit special is that it's in python, so more easy to maintain, easy to adapt, easy to read and doesn't contain too many lines (you should check the number of lines in inkwave..). If I can understand how to convert one waveform in the epdiy-format, we'll be able to use 4-bit vendor waveforms with ease directly from waveform files (effectively bypassing the .wrf format) . Any help with that would be much appreciated @vroland .

Assuming the most common use-case, a common 4-bit waveform, the waveform parser should work without an issue. In a nutshell, the waveform boils down to the following format:

HEADER...
TEMP-RANGE-1
    ...waveform-for-mode-1...
    ...waveform-for-mode-2...
    ...waveform-for-mode-3...
    ...waveform-for-mode-4...
TEMP-RANGE-2
    ....waveform-for-mode-1...
    ...waveform-for-mode-2...
    ...waveform-for-mode-3...
    ...waveform-for-mode-4...
TEMP-RANGE-3
    ....waveform-for-mode-1...
    ...waveform-for-mode-2...
    ...waveform-for-mode-3...
    ...waveform-for-mode-4...
TEMP-RANGE-4
    ....waveform-for-mode-1...
    ...waveform-for-mode-2...
    ...waveform-for-mode-3...
    ...waveform-for-mode-4...
....

and so on until the last temperature range.

@mcer12 Have you yet had a chance to test the parser with your extensive range of waveforms? 😄

aceisace avatar May 09 '23 23:05 aceisace

Thank you for sharing this code. It not only helps with the current project, but also provides great convenience for DIY e-ink projects. Your code can assist me in parsing wbf waveforms and using the results to drive an e-ink screen with an FPGA.

Thank you for continuing to improve the code despite your busy schedule. It would be great if you could create a new open-source project that implements similar functionality to this link: https://github.com/zephray/NekoInk/tree/master/waveform/gdew101_gd. ↗ This would allow us to convert wbf waveforms into CSV format for easier reading and modification. Thanks again!

Hanley-Yao avatar Jul 17 '23 15:07 Hanley-Yao

@Hanley-Yao , you're welcome. This parser was written by me and as such, does not belong to epdiy or it's core contributors, but may be used for development as long as you publish any progress you have made back to me.

I am aware of zephray's nekoink and nekocal project, but there is a difference in the input waveform format. To be precise, nekoink's parser requires .fw or .iwf format files containing the actual waveforms, but most of the parallel display's recent waveforms are in .wbf format, which is pretty different and pretty much no source-code is (and likely won't be) available. As such, compatibility between different waveform formats are still difficult to achieve. Once I have some better understanding of how to convert the raw data containing the waveform into a meaningful (and tested) format, I can adapt the code to output the waveform in a format like zephray's. Any help with understanding how to use the parsed data from my parser would be much appreciated.

aceisace avatar Jul 17 '23 18:07 aceisace

Hello, the analysis of the wbf waveform is confidential, but we can still obtain its analysis results through it8951. By directly burning the wbf waveform into it8951 and then refreshing a special image, we can use a logic analyzer to collect the corresponding waveform data. This process is somewhat troublesome. I will attempt to collect these waveforms and share both the wbf source file and the collected waveforms.

It seems that we can learn from the NXP forum (https://community.nxp.com/t5/i-MX-Processors/How-to-convert-wbf-waveform-file-to-wf-file/m-p/467926/highlight/true) ↗) that the wbf file contains raw waveform data, which may be compressed or encrypted. With a specific program, it can be converted into a wf format, which will increase memory usage. We cannot obtain the source code for this decompression or decryption method, but we may be able to reverse engineer it from the binary machine code in the it8951 firmware... However, the CPU instruction set of it8951 may be non-public, but I will share the wbf file and the firmware of it8951!

I am designing a PCB suitable for collecting the output signals of it8951. After the data collection and sorting, I will share it. Perhaps we can establish some mapping relationship between the wbf file and the collected data, so that we can parse these wbf files without going through it8951 (It's a pity that it seems that it8951 can only parse 4bit waveforms).

Thank you for your efforts! We can work together to tackle this challenge. Please pay attention https://github.com/Hanley-Yao/WaveHack

Hanley-Yao avatar Jul 18 '23 04:07 Hanley-Yao

Thanks for your reply, @Hanley-Yao . You're right, most of the work related to waveforms in NDA, but there are is a little bit of info available from the net. Having a logic analyser is pretty handy when trying to figure out how waveforms work. I do not have these tools myself, just a simple oscilloscope and a few other tools.

But the results from my parser look promising, as the checksums within the waveforms seem valid. To make a usable format form my parser, it's just a matter of converting this format:

[0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0xFF, 0x01, 0xB0, 0xB0, 0x70, 0x40, 0x00, 0x00]

into something meaningful, e.g. the format suggested by zephray, with 0,1,2,3 (e.g. 0 -> no op, 1-> make lighter etc.)

I am looking forward to see the progress of your pcb to help analyse the waveforms as well as having some open-source way to convert the .wbf files into .wf files and then to csv.

I have a few vendor waveforms available I use for testing purposes. So far, my parser can work with both, 4 bit and 5-bit waveforms. If we know the (4-bit) input waveform in the it8951 chip, we can use the analysed data from your pcb to convert the current format into something similar to .wf or even directly to csv.

aceisace avatar Jul 18 '23 10:07 aceisace

Thank you for your reply, @aceisace .Zephray is an expert in this field. I also have some knowledge about waveform. Normally, if you need to execute the A2 waveform, you need to index the data through 1: current color, 2: target color, 3: frame number, and 4: temperature. We will get 2 bits of data, and generally, the interface width of the ink screen is 8 or 16 bits, where every 2 bits control one pixel. That is to say, 4 or 8 pixels of driving data can be sent in every effective clock cycle. The screen will determine whether the pixel is powered on according to the following action table:

Bit 1 Bit 0 Action
0 0 No action
0 1 Draw black
1 0 Draw white
1 1 No action

According to experience, when the temperature is set to 25 degrees Celsius, the A2 waveform is used. Whether the pixel is brushed from white to black or from black to white, or not brushed at all, it will take 10 waveform cycles. Assuming the pixel is initially white and the target is black, the "01" will be found from the waveform in the first frame to the tenth frame.

However, the A2 waveform only has two colors, while the GC16 has 16 colors. The data decoded by your program to some extent is available, so we should analyze the [0x55] obtained and convert it into binary [01,01,01,01]. But I cannot index the position of the corresponding color and target color. I have entrusted the factory to manufacture the PCB, and I hope to establish a mapping relationship between the data collected from IT8951 and the data decoded by your program, which may help me find the pattern and make your program more perfect!

Thank you for your efforts!

Hanley-Yao avatar Jul 18 '23 11:07 Hanley-Yao

Thanks for your reply @Hanley-Yao . Thanks for the info about the waveform. You are right that the A2 waveform only uses two colours, while the GC16 can effectively display 16 grayscales, hence the lut for GC16 is essentially a 16x16xn matrix (excl. temperature), as we can go from any of the supported 16 grayscales to any other of the 16 grayscales. n is probably the phases needed to apply to get the desired target grayscale.

After checking a few 4-bit waveforms for mode gc16, I do not know how to further process the data. I'm looking forward to the pcb and it's results with the hope that the parser is then able to parse the extracted waveform data 💯

aceisace avatar Jul 18 '23 13:07 aceisace

Hello, @aceisace sorry for the wait. After relentless effort, I was able to refresh two special images [https://github.com/Hanley-Yao/WaveHack/tree/main/imghack] using the default waveform file in mode 2 (GC16) on it8951. I also used a logic analyzer to sample the output signal of it8951, including 14 temperature segments [https://github.com/Hanley-Yao/WaveHack/tree/main/analysis/waveshare_ed097tc2].

I am preparing to write code that can parse the csv file exported from the logic analyzer, which can convert csv into something similar to [https://github.com/zephray/NekoInk/tree/master/waveform] for human reading and modification.

Thank you for your attention!

Hanley-Yao avatar Jul 27 '23 02:07 Hanley-Yao

@Hanley-Yao Thanks for the update! Wow, being able to analyse the code this way is quite convenient. I was a little too busy last week fixing some code, but I managed to my hands on a .fw file generated from an original waveform. The only issue is that the generated csv files still require some pre-processing before they can be used at all with epdiy. Hence, any code to parse these csv files would be much appreciated!

I'll share the csv files so you can also use them to help you with the waveforms 👍

aceisace avatar Aug 02 '23 13:08 aceisace

Created a new repo for my python-waveform parser: Python waveform parser

aceisace avatar Aug 27 '23 22:08 aceisace

Could not make it the 16 grayscakes work correctly in the 13.3 inch display. If someone has the right waveform for ED133UT2 would like to try this and see if I can get the grayscales rendered correctly.

martinberlin avatar Aug 28 '23 16:08 martinberlin

In regards to the 13.3", this one is pretty weird compared to all other displays. None of the other waveforms are working on this one as they should. Furthermore, as the vendor waveforms for this one are in .wbf only and 4-bit, vroland's parser cannot parse them as it's designed to parse 5-bit only. On top of that, the difference between waveforms of this display within different batches can be significant. The result is a disorted image, lines caused by non-suitable waveforms and missing gray-levels.

Your best bet is to probably use the OC4-waveform. Perhaps you could ask NXP to convert the .wbf file into a .fw file, which can be converted to csv. But then again, yet another parser is required to convert it from csv to epdiy-format. This is what I'm working on. Since vroland is staying under the radar for quite some time now, even understanding the epdiy-waveform will take quite some time and effort.

aceisace avatar Aug 28 '23 18:08 aceisace

Hey, sorry, I was indeed away for a bit in summer and also focused on the V7 firmware. I can at least help you understand the epdiy waveforms :) Do you need help understanding the JSON intermediate format or the header file? Regarding the waveform timings: This is a clutch for epdiy V1-V6 to use less cycles to draw an image. Normally, each frame that is sent to the display has exactly the same timing and only the direction of the voltage applied is different. To save some cycles when going directly from white or black, my idea was to modify the timing so one frame brings the particles exactly to the next gray level. The time is the high time of the CKV time in 10s of microseconds, which controls for how long the line driver is active. The timings do not come from a waveform file, I just made them up through experimentation. Hence in the parsed waveforms they are NULL.

vroland avatar Aug 29 '23 19:08 vroland

Welcome back! Glad to have you back! Yes, please, I need help understanding the header file itself first. Thanks for the explanation about the timings too. To be more precise, to generate a usable header file, two steps are required:

  1. Decoding a waveform as it is found in the .wbf file into a more usable format which includes the phases for all 16 grayscales. This is something inkwave is able to do, but even after going through the code several times, I cannot figure out, how to parse the data inside a waveform for a given mode and temperature range. As I am planning to support not just epdiy-format, json will work just fine as a middle-step.

  2. Converting the parsed waveform from 1) into a suitable header file. This requires understanding the waveform header. What I could figure out is that the waveform header for a mode m and temperature range t consists of 16 columns with each 4 phases and a number of rows, which are fixed for a given waveform. I believe the 16 columns represent the 16 grayscales, but two questions arise: How does epdiy figure out which of the 4 voltages to apply by using the phases as the info in the phases only has hex-numbers, e.g. 0x45? As parallel e-paper displays heavily rely phases to change pixel values, the information of the current as well as the desired grayscale is required. So in the header file, how is a transition made, e.g. if my pixel has grayscale 0 (fully dark), how can I reach grayscale 4 on this pixel using the header?

aceisace avatar Aug 30 '23 15:08 aceisace

Just a small short note about this @aceisace

How does epdiy figure out which of the 4 voltages

to drive the pixels only +15 / -15 is used. So they are just 2 voltages not 4 if I got this correctly. Here a nice explanation from the creative genius maker that drove this originally http://essentialscrap.com/eink/electronics.html

This is what the +-15 V voltages are used for: they are connected to the electrodes on the screen through some thin-film-transistor FETs. Then the +22 V and -20 V voltages are used for driving these FETs. The transistors made using TFT technology are hardly ideal, and require quite a bit of voltage on the gate to fully switch. Therefore the gate driver requires these voltages which are larger than the voltages that will be applied to the FET's source.

martinberlin avatar Aug 31 '23 02:08 martinberlin

Thanks for the info @martinberlin . Indeed, I did not know this information, but it makes sense. While I was testing, I hooked up an oscilloscope in amp-meter mode and found only a small current draw from the +20V/-22V lines, while the +15V/-15V were drawing much more current. That would also explain how it's possible to make a pixel darker or lighter. From the CSV generated by Zephray's tool, there are only 3 valid operations, no-op, lighter, darker. With two voltages instead of 4, it would explain how this can be achieved. Thanks for solving this mistery!

What's left is to basically decode the waveform from the bytes found in the waveform into the epdiy header file, using json as an intermediate.

aceisace avatar Aug 31 '23 10:08 aceisace

@aceisace Maybe what got you confused is that to save space, I use 2 bits per table entry, not a whole byte. So a 16x16 table for one phase becomes 16 x 4 bytes, because each of the 4 bytes has 4 sets of two bits, indicating a lighten, darken or no-op, see https://github.com/vroland/epdiy/blob/221f068d781ef0e6df7db3cd7cf9bca4b83233e9/scripts/waveform_hdrgen.py#L21. So for a given phase / frame, if we want to go from gray level 5 to gray level 7, we have to look up in the 7th set of 4 bytes the least significant 2 bits of the 2nd byte. (Imagine the binary numbers written from left to right, then the 2 least significant are the leftmost bits).

vroland avatar Aug 31 '23 21:08 vroland

Thank you very much for explaining @vroland ! So a phase (row) always consists of 16 "slots", where each slot has 4 bytes. These four bytes actually represent tuples of 2 bits each, where these tuples represent what to do. This is just assumption, but 00 could be no-op, 01 is probably to make lighter and 10 is probably to make darker, while 11 is probably ignored or no-op again. So basically, a row from zephray's csv format, which looks like this: 0,15,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,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,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0 represents the operations needed to go from white to black are actually encoded in a column(?) in the epdiy header. e.g. 2,2,0,0 would become 10, 10, 00, 00 in binary (10100000) which equals to 0xA0 in hex. Hence, 16 operations can be packed into 4 bytes. Do the remaining bytes get added in the next row, below the same column?

Now there is just two things left to do until I can finish my parser:

  1. I'm still not quite sure about the transitions from one grayscale to another. Could you explain it more concretely with this line: {{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x55},{0x15,0x55,0x55,0x54}}

  2. How does inkwave parse the waveform as it is found in the waveform file?

aceisace avatar Aug 31 '23 22:08 aceisace

Exactly!

For 1.: For each pixel, we first choose the inner chunk according to the target grayscale. E.g., if we want to go from 2 to 9 (zero-based), we first choose the 3rd 4-byte array from the outer array. Then, we look up the origin grayscale in the inner array: Since the target is 9, we need the second operation tuple of the third byte, in this case 01. So this is the operation to push to the display for this particular pixel. We nee to do this for every pixel for every phase of the waveform. This is why it's so hard to make it fast on an ESP :D

  1. I can't really give you more insights on this, as I only extended the existing parser. I don't know much about the wbf format.

vroland avatar Sep 02 '23 09:09 vroland

Thanks for the info, @vroland So, a single outer array contains a single operation on how to go from any grayscale to any other grayscale (16*16 = 256 combinations). The next outer array contains the next operation to apply to this pixel. So if I format the outer arrays in a way they are exactly on top of each other, I just need to go down one line right? e.g. {0xC9, 0x55, 0xAA, 0xAA} {0x93, 0x00, 0xAA, 0xAA} Assuming the operation in question is the 1 byte's 4th operation (01), does that mean the next operation to apply is 11?

That leaves one more question though. For simplicities sake, let's assume the old grayscale value is always 0 (black). How do I find out the position of the hex-value is responsible for the desired target grayscale?

aceisace avatar Sep 04 '23 18:09 aceisace

I don't quite understand the question, what value is the "hex value" you want? You mean the 2-bit operation?

vroland avatar Sep 04 '23 19:09 vroland

Yes, the two-bit operation within a byte. How can I change some values to improve the waveform, if I am always starting at black? Every row should contain the info about which operation needs to be applied (darker, lighter or no-op). Where exactly are the two-bit operation codes for going from a fully black pixel to any other grayscale within the outer arrays and how can I find this out myself?

For example, let's say I'm using GC-16 and all pixels are currently black. If the built-in waveform is not suitable due to a different lot, you could potentially modify the waveform to get more reasonable results. The question is finding out which of the hex values in the outer array I need to modify for this

aceisace avatar Sep 04 '23 19:09 aceisace

In that case you always look at the most significant two bits of the first inner array for every phase (If I remember correctly the "to" is the outer array, "from" the inner array"). So these you'd need to modify or add additional phases.

vroland avatar Sep 07 '23 21:09 vroland

Mind if we can make a call sometime? I think this is a little too complicated to discuss in a few comments of a github issue.

Also, one more thing I realised (at least in the main branch). When the waveform does use timings, the first two inner arrays of every outer array are apparently ignored. When omitting timings, the first two inner arrays of every phase seem to make a difference. If this is how it should be, it's fine. Just making sure it's not a bug, as this is one of the reasons some (not all) 13.3" have rendering issues with the provided waveforms. Differences can be quite large between batches. What may work for one does not mean it'll work for others, sometimes even within the same batch and having the same LOT.

aceisace avatar Sep 13 '23 19:09 aceisace

We can probably find the time for a call, just send me an email.

I don't think that should be happening, not sure what you mean. Maybe it's easier to explain synchronously.

vroland avatar Sep 14 '23 20:09 vroland

Sure, I'll get in contact via mail to schedule the call 👍

aceisace avatar Sep 15 '23 10:09 aceisace