546 lines
21 KiB
Zig
546 lines
21 KiB
Zig
const std = @import("std");
|
|
const display = @import("display.zig");
|
|
const chars = @import("images/images.zig").chars;
|
|
|
|
// The package manager will install headers from our dependency in zig's build
|
|
// cache and include the cache directory as a "-I" option on the build command
|
|
// automatically.
|
|
const c = @cImport({
|
|
@cInclude("MagickWand/MagickWand.h");
|
|
@cInclude("i2cdriver.h");
|
|
});
|
|
|
|
var lines: [display.LINES]*[:0]u8 = undefined;
|
|
|
|
fn usage(args: [][]u8) !void {
|
|
const writer = std.io.getStdErr().writer();
|
|
try writer.print("usage: {s} <device> [-bg <image file>] [-<num> text]...\n", .{args[0]});
|
|
try writer.print("\t-<num> text: line number and text to display\n", .{});
|
|
try writer.print("\te.g. \"-1 'hello world'\" will display \"hello world\" on line 1\n", .{});
|
|
std.os.exit(1);
|
|
}
|
|
const Options = struct {
|
|
background_filename: [:0]u8,
|
|
device_file: [:0]u8,
|
|
};
|
|
pub fn main() !void {
|
|
const alloc = std.heap.c_allocator;
|
|
//defer alloc.deinit();
|
|
const args = try std.process.argsAlloc(alloc);
|
|
defer std.process.argsFree(alloc, args);
|
|
|
|
const stdout_file = std.io.getStdOut().writer();
|
|
var bw = std.io.bufferedWriter(stdout_file);
|
|
const stdout = bw.writer();
|
|
defer bw.flush() catch unreachable; // don't forget to flush!
|
|
|
|
// If we have stdin redirected, we want to process that before
|
|
// we take command line options. So cli will overwrite any lines specified
|
|
// from the file
|
|
const stdin_file = std.io.getStdIn();
|
|
var stdin_data: [display.WIDTH * display.HEIGHT + 1]u8 = undefined;
|
|
// Need this to support deallocation of memory
|
|
var line_inx: usize = 0;
|
|
var stdin_lines: [display.LINES][:0]u8 = undefined;
|
|
defer {
|
|
for (0..line_inx) |i| {
|
|
alloc.free(stdin_lines[i]);
|
|
}
|
|
}
|
|
var nothing: [:0]u8 = @constCast("");
|
|
for (lines, 0..) |_, i| {
|
|
lines[i] = ¬hing;
|
|
}
|
|
if (!stdin_file.isTty()) {
|
|
const read = try stdin_file.readAll(&stdin_data);
|
|
if (read == stdin_data.len) {
|
|
try std.io.getStdErr().writer().print("ERROR: data provided exceeds what can be sent to device!\n", .{});
|
|
try usage(args);
|
|
}
|
|
var read_data = stdin_data[0..read];
|
|
var it = std.mem.split(u8, read_data, "\n");
|
|
while (it.next()) |line| {
|
|
if (line.len == 0) continue;
|
|
stdin_lines[line_inx] = try alloc.dupeZ(u8, line);
|
|
lines[line_inx] = &stdin_lines[line_inx];
|
|
line_inx += 1;
|
|
}
|
|
}
|
|
std.debug.print("delme: {s}\n", .{lines[0].*});
|
|
const opts = try processArgs(alloc, args, &lines);
|
|
defer alloc.destroy(opts);
|
|
if (opts.background_filename.len > 0) try stdout.print("Converting {s}\n", .{opts.background_filename});
|
|
var pixels: [display.WIDTH * display.HEIGHT]u8 = undefined;
|
|
try convertImage(opts.background_filename, &pixels, textForLine);
|
|
try bw.flush();
|
|
|
|
// We should take the linux device file here, then inspect for ttyUSB vs
|
|
// i2c whatever and do the right thing from there...
|
|
try sendPixels(&pixels, opts.device_file, 0x3c);
|
|
|
|
// try stdout.print("Run `zig build test` to run the tests.\n", .{});
|
|
}
|
|
|
|
fn processArgs(allocator: std.mem.Allocator, args: [][:0]u8, line_array: *[display.LINES]*const [:0]u8) !*Options {
|
|
if (args.len < 2) try usage(args);
|
|
const prefix = "/dev/ttyUSB";
|
|
var opts = try allocator.create(Options);
|
|
opts.device_file = args[1];
|
|
if (!std.mem.eql(u8, opts.device_file, "-") and !std.mem.startsWith(u8, opts.device_file, prefix)) try usage(args);
|
|
|
|
opts.background_filename = @constCast("");
|
|
var is_filename = false;
|
|
var line_number: ?usize = null;
|
|
for (args[1..], 1..) |arg, i| {
|
|
if (std.mem.eql(u8, "-bg", arg)) {
|
|
is_filename = true;
|
|
continue;
|
|
}
|
|
if (is_filename) {
|
|
opts.background_filename = args[i]; // arg capture changes value...
|
|
break;
|
|
}
|
|
if ((arg[0] == '-' and arg.len > 1) and areDigits(arg[1..])) {
|
|
line_number = try std.fmt.parseInt(usize, arg[1..], 10);
|
|
continue;
|
|
}
|
|
if (line_number) |line| {
|
|
if (arg.len > display.CHARS_PER_LINE) {
|
|
try std.io.getStdErr().writer().print(
|
|
"ERROR: text for line {d} has {d} chars, exceeding maximum length {d}\n",
|
|
.{ line, arg.len, display.CHARS_PER_LINE },
|
|
);
|
|
std.os.exit(1);
|
|
}
|
|
std.log.debug("line {d} text: \"{s}\"\n", .{ line, arg });
|
|
line_array.*[line] = &args[i];
|
|
line_number = null;
|
|
continue;
|
|
}
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
fn areDigits(bytes: []u8) bool {
|
|
for (bytes) |byte| {
|
|
if (!std.ascii.isDigit(byte)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn sendPixels(pixels: []const u8, file: [:0]const u8, device_id: u8) !void {
|
|
if (std.mem.eql(u8, file, "-"))
|
|
return sendPixelsToStdOut(pixels);
|
|
|
|
if (@import("builtin").os.tag != .linux)
|
|
@compileError("Linux only please!");
|
|
|
|
const is_i2cdriver = std.mem.startsWith(u8, file, "/dev/ttyUSB");
|
|
if (is_i2cdriver)
|
|
return sendPixelsThroughI2CDriver(pixels, file, device_id);
|
|
|
|
// Send through linux i2c native
|
|
return error.LinuxNativeNotImplemented;
|
|
}
|
|
|
|
fn sendPixelsToStdOut(pixels: []const u8) !void {
|
|
const stdout_file = std.io.getStdOut().writer();
|
|
var bw = std.io.bufferedWriter(stdout_file);
|
|
const stdout = bw.writer();
|
|
defer bw.flush() catch unreachable; // don't forget to flush!
|
|
for (0..display.HEIGHT) |i| {
|
|
try stdout.print(
|
|
"{d:0>2}: {s}\n",
|
|
.{ i, fmtSliceGreyscaleImage(pixels[(i * display.WIDTH)..((i + 1) * display.WIDTH)]) },
|
|
);
|
|
}
|
|
}
|
|
|
|
fn sendPixelsThroughI2CDriver(pixels: []const u8, file: [*:0]const u8, device_id: u8) !void {
|
|
var pixels_write_command = [_]u8{0x00} ** ((display.WIDTH * display.PAGES) + 1);
|
|
pixels_write_command[0] = 0x40;
|
|
packPixelsToDeviceFormat(pixels, pixels_write_command[1..]);
|
|
var i2c = c.I2CDriver{
|
|
.connected = 0,
|
|
.port = 0,
|
|
.model = [_]u8{0} ** 16,
|
|
.serial = [_]u8{0} ** 9,
|
|
.uptime = 0.0,
|
|
.voltage_v = 0.0,
|
|
.current_ma = 0.0,
|
|
.temp_celsius = 0.0,
|
|
.mode = 0,
|
|
.sda = 0,
|
|
.scl = 0,
|
|
.speed = 0,
|
|
.pullups = 0,
|
|
.ccitt_crc = 0,
|
|
.e_ccitt_crc = 0,
|
|
};
|
|
const c_file = @ptrCast([*c]const u8, file);
|
|
const stdout_file = std.io.getStdOut().writer();
|
|
var bw = std.io.bufferedWriter(stdout_file);
|
|
const stdout = bw.writer();
|
|
defer bw.flush() catch unreachable; // don't forget to flush!
|
|
try stdout.print("Connecting to I2CDriver on {s}. If progress stalls, unplug device and re-insert.\n", .{c_file});
|
|
try bw.flush();
|
|
try stdout.print("Sending pixels to display...", .{});
|
|
c.i2c_connect(&i2c, c_file);
|
|
try stdout.print("Device connected\n", .{});
|
|
if (i2c.connected != 1) return error.I2CConnectionFailed;
|
|
|
|
// Initialize the device
|
|
if (c.i2c_start(&i2c, device_id, 0) != 1) // 0 for write, 1 for read. Seems to be a mask
|
|
return error.I2CStartFailed;
|
|
try i2cWrite(&i2c, &[_]u8{ 0x00, 0x8d, 0x14 }); // Enable charge pump
|
|
// Charge pump takes a few ms so let's do some other stuff in the meantime
|
|
try i2cWrite(&i2c, &[_]u8{ 0x00, 0x20, 0x00 }); // Horizontal addressing mode
|
|
try i2cWrite(&i2c, &[_]u8{ 0x00, 0x21, 0x00, 0x7F }); // Start column 0, end column 127
|
|
try i2cWrite(&i2c, &[_]u8{ 0x00, 0x22, 0x00, 0x07 }); // Start page 0, end page 7
|
|
try i2cWrite(&i2c, &[_]u8{ 0x00, 0xaf }); // Display on (should this be after our image is written?
|
|
|
|
// We stop/start here since otherwise it seems our data goes nowhere. Not
|
|
// sure it's actually our problem but this seems to fix it
|
|
c.i2c_stop(&i2c);
|
|
if (c.i2c_start(&i2c, device_id, 0) != 1) // 0 for write, 1 for read. Seems to be a mask
|
|
return error.I2CStartFailed;
|
|
|
|
// Write data to device
|
|
try i2cWrite(&i2c, &pixels_write_command);
|
|
// Leaving this here because we'll need it later I think
|
|
// for (0..HEIGHT) |i| {
|
|
// std.log.debug("{d:0>2}: {s}\n", .{ i, fmtSliceGreyscaleImage(pixels[(i * WIDTH)..((i + 1) * WIDTH)]) });
|
|
// }
|
|
try stdout.print("done\n", .{});
|
|
}
|
|
|
|
fn packPixelsToDeviceFormat(pixels: []const u8, packed_pixels: []u8) void {
|
|
// Each u8 in pixels is a single bit. We need to pack these bits
|
|
for (packed_pixels, 0..) |*b, i| {
|
|
const column = i % display.WIDTH;
|
|
const page = i / display.WIDTH;
|
|
|
|
// if (column == 0) std.debug.print("{d}: ", .{page});
|
|
// pixel array will be 8x as "high" as the data array we are sending to
|
|
// the device. So the device column above is only our starter
|
|
// Display has 8 pages, which is a set of 8 pixels with LSB at top of page
|
|
//
|
|
// To convert from the pixel array above, we need to:
|
|
// 1. convert from device page to a base "row" in the pixel array
|
|
const row = page * display.PAGES;
|
|
// 2. We will have 8 rows for each base row
|
|
// 3. Multiple each row by the width to get the index of the start of
|
|
// the row
|
|
// 4. Add our current column index for the final pixel location in
|
|
// the pixel array.
|
|
//
|
|
// Now that we have the proper index in the pixel array, we need to
|
|
// convert that into our destination byte. Each index will be a u8, either
|
|
// 0xff for on or 0x00 for off. So...
|
|
//
|
|
// 1. We will take the value and bitwise and with 0x01 so we get one bit
|
|
// per source byte
|
|
// 2. Shift that bit into the proper position in our destination byte
|
|
|
|
b.* = (pixels[(0 + row) * display.WIDTH + column] & 0x01) << 0 |
|
|
(pixels[(1 + row) * display.WIDTH + column] & 0x01) << 1 |
|
|
(pixels[(2 + row) * display.WIDTH + column] & 0x01) << 2 |
|
|
(pixels[(3 + row) * display.WIDTH + column] & 0x01) << 3 |
|
|
(pixels[(4 + row) * display.WIDTH + column] & 0x01) << 4 |
|
|
(pixels[(5 + row) * display.WIDTH + column] & 0x01) << 5 |
|
|
(pixels[(6 + row) * display.WIDTH + column] & 0x01) << 6 |
|
|
(pixels[(7 + row) * display.WIDTH + column] & 0x01) << 7;
|
|
|
|
// std.debug.print("{s}", .{std.fmt.fmtSliceHexLower(&[_]u8{b.*})});
|
|
// if (column == 127) std.debug.print("\n", .{});
|
|
|
|
// Last 2 pages are yellow...16 pixels vertical
|
|
// if (page == 6 or page == 7) b.* = 0xff;
|
|
// b.* = 0xf0;
|
|
}
|
|
}
|
|
fn i2cWrite(i2c: *c.I2CDriver, bytes: []const u8) !void {
|
|
var rc = c.i2c_write(i2c, @ptrCast([*c]const u8, bytes), bytes.len); // nn is size of array
|
|
if (rc != 1)
|
|
return error.BadWrite;
|
|
}
|
|
|
|
fn fmtSliceGreyscaleImage(bytes: []const u8) std.fmt.Formatter(formatSliceGreyscaleImage) {
|
|
return .{ .data = bytes };
|
|
}
|
|
fn formatSliceGreyscaleImage(
|
|
bytes: []const u8,
|
|
comptime fmt: []const u8,
|
|
options: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
_ = fmt;
|
|
_ = options;
|
|
|
|
for (bytes) |b| {
|
|
switch (b) {
|
|
0xff => try writer.writeByte('1'),
|
|
0x00 => try writer.writeByte('0'),
|
|
else => unreachable,
|
|
}
|
|
}
|
|
}
|
|
fn reportMagickError(mw: ?*c.MagickWand) !void {
|
|
var severity: c.ExceptionType = undefined;
|
|
var description = c.MagickGetException(mw, &severity);
|
|
defer description = @ptrCast([*c]u8, c.MagickRelinquishMemory(description));
|
|
try std.io.getStdErr().writer().print("{s}\n", .{description});
|
|
}
|
|
fn textForLine(line: usize) []u8 {
|
|
return lines[line].*;
|
|
}
|
|
fn convertImage(filename: [:0]u8, pixels: *[display.WIDTH * display.HEIGHT]u8, text_fn: *const fn (usize) []u8) !void {
|
|
c.MagickWandGenesis();
|
|
defer c.MagickWandTerminus();
|
|
var mw = c.NewMagickWand();
|
|
defer {
|
|
if (mw) |w| mw = c.DestroyMagickWand(w);
|
|
}
|
|
|
|
// Reading an image into ImageMagick is problematic if it isn't a bmp
|
|
// as the library needs a bunch of dependencies available
|
|
var status: c.MagickBooleanType = undefined;
|
|
if (filename.len > 0) {
|
|
status = c.MagickReadImage(mw, filename);
|
|
} else {
|
|
// TODO: if there is no background image AND
|
|
// we precompute monochrome bit patterns for our font
|
|
// we can completely avoid ImageMagick here. Even with
|
|
// a background we can do the conversion, then do our
|
|
// own text overlay after monochrome conversion.
|
|
// Faster and smaller binary (maybe multi-font support?)
|
|
const blob = @embedFile("images/blank.bmp");
|
|
status = c.MagickReadImageBlob(mw, blob, blob.len);
|
|
}
|
|
if (status == c.MagickFalse) {
|
|
if (!std.mem.eql(u8, filename[filename.len - 3 ..], "bmp"))
|
|
try std.io.getStdErr().writer().print("File is not .bmp. That is probably the problem\n", .{});
|
|
try reportMagickError(mw);
|
|
return error.CouldNotReadImage;
|
|
}
|
|
|
|
// Get height and width of the image
|
|
const w = c.MagickGetImageWidth(mw);
|
|
const h = c.MagickGetImageHeight(mw);
|
|
|
|
std.log.debug("Original dimensions: {d}x{d}\n", .{ w, h });
|
|
// This should be 48x64 with our test
|
|
// Command line resize works differently than this. Here we need to find
|
|
// new width and height based on the input aspect ratio ourselves
|
|
const resize_dimensions = getNewDimensions(w, h, display.WIDTH, display.HEIGHT);
|
|
|
|
std.log.debug("Dimensions for resize: {d}x{d}\n", .{ resize_dimensions.width, resize_dimensions.height });
|
|
|
|
status = c.MagickResizeImage(mw, resize_dimensions.width, resize_dimensions.height, c.UndefinedFilter);
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotResizeImage;
|
|
|
|
var pw = c.NewPixelWand();
|
|
defer {
|
|
if (pw) |pixw| pw = c.DestroyPixelWand(pixw);
|
|
}
|
|
status = c.PixelSetColor(pw, "white");
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotSetColor;
|
|
|
|
status = c.MagickSetImageBackgroundColor(mw, pw);
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotSetBackgroundColor;
|
|
|
|
// This centers the original image on the new canvas.
|
|
// Note that the extent's offset is relative to the
|
|
// top left corner of the *original* image, so adding an extent
|
|
// around it means that the offset will be negative
|
|
status = c.MagickExtentImage(
|
|
mw,
|
|
display.WIDTH,
|
|
display.HEIGHT,
|
|
-@intCast(isize, (display.WIDTH - resize_dimensions.width) / 2),
|
|
-@intCast(isize, (display.HEIGHT - resize_dimensions.height) / 2),
|
|
);
|
|
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotSetExtent;
|
|
|
|
for (0..display.LINES) |i| {
|
|
const text = text_fn(i);
|
|
if (text.len == 0) continue;
|
|
// We have text!
|
|
const y: isize = display.FONT_HEIGHT * @intCast(isize, i);
|
|
var x: isize = display.BORDER_LEFT;
|
|
var left_spaces: isize = 0;
|
|
for (text) |ch| {
|
|
if (ch == ' ') {
|
|
left_spaces += 1;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
x += (display.FONT_WIDTH * left_spaces);
|
|
mw = try drawString(mw, text[@intCast(usize, left_spaces)..], x, y);
|
|
}
|
|
|
|
// We make the image monochrome by quantizing the image with 2 colors in the
|
|
// gray colorspace. See:
|
|
// https://www.imagemagick.org/Usage/quantize/#monochrome
|
|
// and
|
|
// https://stackoverflow.com/questions/18267432/using-the-c-api-for-imagemagick-on-iphone-to-convert-to-monochrome
|
|
//
|
|
// We do this at the end so we have pure black and white. Otherwise the
|
|
// resizing oprations will generate some greyscale that we don't want
|
|
status = c.MagickQuantizeImage(mw, // MagickWand
|
|
2, // Target number colors
|
|
c.GRAYColorspace, // Colorspace
|
|
1, // Optimal depth
|
|
c.MagickTrue, // Dither
|
|
c.MagickFalse // Quantization error
|
|
);
|
|
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotQuantizeImage;
|
|
|
|
status = c.MagickExportImagePixels(mw, 0, 0, display.WIDTH, display.HEIGHT, "I", c.CharPixel, @ptrCast(*anyopaque, pixels));
|
|
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotExportImage;
|
|
|
|
for (0..display.WIDTH * display.HEIGHT) |i| {
|
|
switch (pixels[i]) {
|
|
0x00 => pixels[i] = 0xFF,
|
|
0xFF => pixels[i] = 0x00,
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
fn drawString(mw: ?*c.MagickWand, str: []const u8, x: isize, y: isize) !?*c.MagickWand {
|
|
var rc = mw;
|
|
for (str, 0..) |ch, i| {
|
|
rc = try drawCharacter(
|
|
rc,
|
|
ch,
|
|
-(x + @intCast(isize, display.FONT_WIDTH * i)),
|
|
-y,
|
|
);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
fn drawCharacter(mw: ?*c.MagickWand, char: u8, x: isize, y: isize) !?*c.MagickWand {
|
|
// Create a second wand. Does this need to exist after the block?
|
|
var cw = c.NewMagickWand();
|
|
defer {
|
|
if (cw) |dw| cw = c.DestroyMagickWand(dw);
|
|
// if (merged) |mergeme| {
|
|
// _ = c.DestroyMagickWand(mw);
|
|
// mw = mergeme;
|
|
// }
|
|
}
|
|
const image_char = chars[char];
|
|
if (image_char.len == 0) return error.CharacterNotSupported;
|
|
var status = c.MagickReadImageBlob(cw, @ptrCast(?*const anyopaque, image_char), image_char.len);
|
|
if (status == c.MagickFalse) unreachable; // Something is terribly wrong if this fails
|
|
|
|
// For character placement, we need to set the image to the correct
|
|
// extent, and offset the image as appropriate. When we set the extent,
|
|
// we need the fill background to be transparent so we don't overwrite
|
|
// the background. This also means our font needs a transparent background
|
|
// (maybe?)
|
|
{
|
|
var pwc = c.NewPixelWand();
|
|
defer {
|
|
if (pwc) |pixwc| pwc = c.DestroyPixelWand(pixwc);
|
|
}
|
|
status = c.PixelSetColor(pwc, "transparent");
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotSetColor;
|
|
|
|
status = c.MagickSetImageBackgroundColor(cw, pwc);
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotSetBackgroundColor;
|
|
// I think our characters are offset by 6px in the x and 8 in the y
|
|
status = c.MagickExtentImage(
|
|
cw,
|
|
display.WIDTH,
|
|
display.HEIGHT,
|
|
x,
|
|
y,
|
|
);
|
|
if (status == c.MagickFalse)
|
|
return error.CouldNotSetExtent;
|
|
}
|
|
|
|
// I think I need to add the image, then flatten this
|
|
status = c.MagickAddImage(mw, cw);
|
|
if (status == c.MagickFalse) return error.CouldNotAddImage;
|
|
|
|
// This works, but idk exactly what it's doing. I get the sense
|
|
// I this only works with two images...
|
|
// c.MagickResetIterator(mw);
|
|
c.MagickSetFirstIterator(mw);
|
|
defer {
|
|
if (mw) |w| _ = c.DestroyMagickWand(w);
|
|
}
|
|
return c.MagickMergeImageLayers(mw, c.FlattenLayer);
|
|
}
|
|
|
|
const Dimensions = struct {
|
|
width: usize,
|
|
height: usize,
|
|
};
|
|
|
|
fn getNewDimensions(width: usize, height: usize, desired_width: usize, desired_height: usize) Dimensions {
|
|
// assuming we're shrinking for now.
|
|
// TODO: Handle expansion
|
|
const width_ratio = @intToFloat(f64, width) / @intToFloat(f64, desired_width);
|
|
const height_ratio = @intToFloat(f64, height) / @intToFloat(f64, desired_height);
|
|
const resize_ratio = if (width_ratio > height_ratio) width_ratio else height_ratio;
|
|
|
|
return .{
|
|
.width = @floatToInt(usize, @intToFloat(f64, width) / resize_ratio), // 48,
|
|
.height = @floatToInt(usize, @intToFloat(f64, height) / resize_ratio), // 64,
|
|
};
|
|
}
|
|
|
|
test "gets correct bytes" {
|
|
const bg_file: [:0]u8 = @constCast("logo:");
|
|
const opts = .{ .background_filename = bg_file, .device_file = "-" };
|
|
var empty: [:0]u8 = @constCast("");
|
|
for (&lines) |*line| {
|
|
line.* = ∅
|
|
}
|
|
var line: [:0]u8 = @constCast("Hello\\!");
|
|
lines[5] = &line;
|
|
var pixels: [display.WIDTH * display.HEIGHT]u8 = undefined;
|
|
|
|
var expected_pixels: *const [display.WIDTH * display.HEIGHT]u8 = @embedFile("testExpectedBytes.bin");
|
|
|
|
// [_]u8{..,..,..}
|
|
try convertImage(opts.background_filename, &pixels, textForLine);
|
|
// try writeBytesToFile("testExpectedBytes.bin", &pixels);
|
|
try std.testing.expectEqualSlices(u8, expected_pixels, &pixels);
|
|
}
|
|
fn writeBytesToFile(filename: []const u8, bytes: []u8) !void {
|
|
const file = try std.fs.cwd().createFile(filename, .{
|
|
.read = false,
|
|
.truncate = true,
|
|
.lock = .Exclusive,
|
|
.lock_nonblocking = false,
|
|
.mode = 0o666,
|
|
.intended_io_mode = .blocking,
|
|
});
|
|
defer file.close();
|
|
const writer = file.writer();
|
|
try writer.writeAll(bytes);
|
|
// try writer.print("pub const chars = &[_][]const u8{{\n", .{});
|
|
}
|
|
test "simple test" {
|
|
var list = std.ArrayList(i32).init(std.testing.allocator);
|
|
defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
|
|
try list.append(42);
|
|
try std.testing.expectEqual(@as(i32, 42), list.pop());
|
|
}
|