diff --git a/Makefile b/Makefile index bd8c809..690118f 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,24 @@ -CC=gcc -CFLAGS=-I ./ -Wall -Werror -LDFLAGS=-static -OBJS=main.o ssd1306.o linux_i2c.o -BIN=ssd1306_bin +CC = gcc +CFLAGS = -I ./ -Wall -Werror -fPIC +LDFLAGS = -L. -lssd1306 +SO_FLAGS = -shared +BIN = ssd1306_cli +LIB = libssd1306.so + +SRC_LIB = ssd1306.c linux_i2c.c +OBJ_LIB = $(SRC_LIB:.c=.o) + +SRC_BIN = main.c +OBJ_BIN = $(SRC_BIN:.c=.o) default: $(BIN) -.PHONY: default clean -# Adapted from scottmcpeak.com/autodepend/autodepend.html --include $(OBJS:.o=.d) +.PHONY: default clean install + +# Autodependency generation +-include $(OBJ_LIB:.o=.d) +-include $(OBJ_BIN:.o=.d) + %.o: %.c $(CC) -c $(CFLAGS) $< -o $*.o $(CC) -MM $(CFLAGS) $< > $*.d @@ -17,9 +27,21 @@ default: $(BIN) sed -e 's/^ *//' -e 's/$$/:/' >> $*.d @rm -f $*.d.tmp -$(BIN):$(OBJS) - $(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS) +# Build the shared library +$(LIB): $(OBJ_LIB) + $(CC) $(SO_FLAGS) -o $@ $(OBJ_LIB) -clean: - rm -f *.o *.d $(BIN) +# Build the CLI tool, linking to the shared lib +$(BIN): $(OBJ_BIN) $(LIB) + $(CC) $(CFLAGS) -o $@ $(OBJ_BIN) $(LDFLAGS) + +# Install CLI and shared library +install: $(BIN) $(LIB) + install -Dm755 $(BIN) /usr/local/bin/$(BIN) + install -Dm755 $(LIB) /usr/local/lib/$(LIB) + install -Dm644 ssd1306.h /usr/local/include/ssd1306.h + install -Dm644 linux_i2c.h /usr/local/include/linux_i2c.h + ldconfig +clean: + rm -f *.o *.d $(BIN) $(LIB) diff --git a/Readme.md b/Readme.md index 5a5b3b8..9aa28b7 100644 --- a/Readme.md +++ b/Readme.md @@ -13,7 +13,8 @@ Please make sure the linux has run "modprobe i2c-dev". **Readme.md** this readme file. ## How to compile Require make and gcc. If you use cross compile, please self define $(CC). -Type "make" to build binary "ssd1306_bin". +Type "make" to build the library and example binaries. +Type "sudo make install" to install the library to your system. Type "make clean" to clean the project. ## How to use - always init oled module ONCE when power up. @@ -26,93 +27,99 @@ Type "make clean" to clean the project. - make sure the XY cursor setting in correct location before printing text ### Params ```sh --I init oled (128x32 or 128x64 or 64x48) --c clear (line number or all) --d 0/display off 1/display on +-I init oled (128x32 or 128x64 or 64x48) +-c clear (line number or all) +-d 0/display off 1/display on -f 0/small font 5x7 1/normal font 8x8 (default small font) --h help message --i 0/normal oled 1/invert oled --l put your line to display --m put your strings to oled +-h help message +-i 0/normal oled 1/invert oled +-b read and display a bmp file (128x64 or 128x32 @ 1bpp) +-l put your line to display +-m put your strings to oled -n I2C device node address (0,1,2..., default 0) --r 0/normal 180/rotate --x x position --y y position +-r 0/normal 180/rotate +-x x position +-y y position ``` ## Example ### init the OLED once - resolution 128x64 ```sh -$ ./ssd1306_bin -I 128x64 +$ sudo ssd1306_cli -I 128x64 ``` - resolution 128x32 ```sh -$ ./ssd1306_bin -I 128x32 +$ sudo ssd1306_cli -I 128x32 ``` - resolution 64x48 ```sh -$ ./ssd1306_bin -I 64x48 +$ sudo ssd1306_cli -I 64x48 ``` ### clear display - clear 1st line ```sh -./ssd1306_bin -c0 +$ sudo ssd1306_cli -c0 ``` - clear 2nd line ```sh -$ ./ssd1306_bin -c1 +$ sudo ssd1306_cli -c1 ``` - clear 4th line ```sh -$ ./ssd1306_bin -c3 +$ sudo ssd1306_cli -c3 ``` - clear whole screen ```sh -$ ./ssd1306_bin -c +$ sudo ssd1306_cli -c ``` ### display on/off - turn off display ```sh -$ ./ssd1306_bin -d 0 +$ sudo ssd1306_cli -d 0 ``` - turn on display ```sh -$ ./ssd1306_bin -d 1 +$ sudo ssd1306_cli -d 1 ``` ### inverting display - normal oled (0 is off, 1 is on) ```sh -$ ./ssd1306_bin -i 0 +$ sudo ssd1306_cli -i 0 ``` - invert oled (0 is on, 1 is off) ```sh -$ ./ssd1306_bin -i 1 +$ sudo ssd1306_cli -i 1 +``` +### displaying a bitmap +- display a 128x64 or 128x32 1bpp bitmap file on the oled +```sh +$ sudo ssd1306_cli -b ``` ### print words - write line "Hello World" ```sh -$ ./ssd1306_bin -l "Hello World" +$ sudo ssd1306_cli -l "Hello World" ``` - write message "alpha\nbravo\ncharlie\ndelta" (please place \n for next line) ```sh -$ ./ssd1306_bin -m "alpha\nbravo\ncharlie\ndelta" +$ sudo ssd1306_cli -m "alpha\nbravo\ncharlie\ndelta" ``` ### I2C device address (default is /dev/i2c-0) - using /dev/i2c-1 ```sh -$ ./ssd1306_bin -n 1 +$ sudo ssd1306_cli -n 1 ``` ### rotate display - normal orientation ```sh -$ ./ssd1306_bin -r 0 +$ sudo ssd1306_cli -r 0 ``` - turn 180 orientation ```sh -$ ./ssd1306_bin -r 180 +$ sudo ssd1306_cli -r 180 ``` ### set cursor location - set XY cursor 8,1(x is column, 8 columns skipping, y is row, 2nd line) ```sh -$ ./ssd1306_bin -x 8 -y 1 +$ sudo ssd1306_cli -x 8 -y 1 ``` diff --git a/examples/Makefile b/examples/Makefile new file mode 100644 index 0000000..bdcec3d --- /dev/null +++ b/examples/Makefile @@ -0,0 +1,13 @@ +CC = gcc +CFLAGS = -Wall -O2 +LDFLAGS = -lssd1306 +TARGET = ssd1306_fmv +SRC = ssd1306_fmv.c + +all: fmv + +fmv: $(SRC) + $(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS) + +clean: + rm -f $(TARGET) diff --git a/examples/Readme.md b/examples/Readme.md new file mode 100644 index 0000000..903606f --- /dev/null +++ b/examples/Readme.md @@ -0,0 +1,21 @@ +## Examples + +### FMV Player +Plays a full motion video (bad apple is included for testing) on the OLED, uses an animated bitmap file. + +Bitmap file was generated using +``` +$ mkdir frames && cd frames +$ ffmpeg -i /path/to/input.mp4 -vf "scale=128:64,format=gray" -pix_fmt monow -vsync 0 frame_%04d.bmp +$ cat frame_* >> ../output.abmp +``` + +Usage: +``` +$ sudo ./ssd1306_fmv [-I 128x64] -n -r <0/128> -a -d +``` + +Example: +``` +$ sudo ./ssd1306_fmv -I 128x64 -n 1 -r 0 -a bad_apple.abmp -d 33 +``` diff --git a/examples/bad_apple.abmp b/examples/bad_apple.abmp new file mode 100644 index 0000000..7d9c6ce Binary files /dev/null and b/examples/bad_apple.abmp differ diff --git a/examples/ssd1306_fmv.c b/examples/ssd1306_fmv.c new file mode 100644 index 0000000..9476566 --- /dev/null +++ b/examples/ssd1306_fmv.c @@ -0,0 +1,254 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ssd1306.h" + +void print_help() +{ + printf("help message\n\n"); + printf("ssd1306_fmv [-I 128x64] -n -r <0/128> -a -d \n"); + printf("-I\t\tinit oled (128x32 or 128x64 or 64x48)\n"); + printf("-h\t\thelp message\n"); + printf("-n\t\tI2C device node address (0,1,2..., default 0)\n"); + printf("-r\t\t0/normal 180/rotate\n"); + printf("-a\t\tanimation file\n"); + printf("-d\t\tframe delay in ms\n"); +} + +#define MAX_WIDTH 128 +#define MAX_HEIGHT 64 +bool bitmap[MAX_HEIGHT][MAX_WIDTH]; +char anim_path[256] = {0}; + +int load_bitmap_to_bool_array(const char *filepath, bool output[MAX_HEIGHT][MAX_WIDTH], int *out_width, int *out_height) { + FILE *fp = fopen(filepath, "rb"); + if (!fp) { + perror("fopen"); + return -1; + } + + uint8_t header[54]; + if (fread(header, 1, 54, fp) != 54 || header[0] != 'B' || header[1] != 'M') { + fclose(fp); + fprintf(stderr, "Invalid BMP file\n"); + return -1; + } + + uint32_t offset = *(uint32_t *)&header[10]; + int width = *(int32_t *)&header[18]; + int height = *(int32_t *)&header[22]; + uint16_t bpp = *(uint16_t *)&header[28]; + + if (bpp != 1 || width > MAX_WIDTH || (height != 32 && height != 64)) { + fclose(fp); + fprintf(stderr, "Only 1bpp 128x32 or 128x64 BMPs supported.\n"); + return -1; + } + + fseek(fp, offset, SEEK_SET); + size_t row_bytes = ((width + 31) / 32) * 4; + + memset(output, 0, sizeof(bool) * MAX_WIDTH * MAX_HEIGHT); + + // BMP rows are bottom-up + for (int row = height - 1; row >= 0; row--) { + uint8_t row_data[row_bytes]; + fread(row_data, 1, row_bytes, fp); + + for (int col = 0; col < width; col++) { + int byte_index = col / 8; + int bit_index = 7 - (col % 8); + uint8_t byte = row_data[byte_index]; + output[row][col] = (byte >> bit_index) & 0x01; + } + } + + fclose(fp); + *out_width = width; + *out_height = height; + return 0; +} + +int main(int argc, char **argv) +{ + uint8_t i2c_node_address = 1; + char oled_type[10] = {0}; + int orientation = -1; + int y = 0; + int cmd_opt = 0; + + while(cmd_opt != -1) + { + cmd_opt = getopt(argc, argv, "I:n:r:a:d:h:"); + + /* Lets parse */ + switch (cmd_opt) { + case 'I': + snprintf(oled_type, sizeof (oled_type) - 1, "%s", optarg); + break; + case 'h': + print_help(); + return 0; + case 'n': + i2c_node_address = (uint8_t)atoi(optarg); + break; + case 'r': + orientation = atoi(optarg); + if (orientation != 0 && orientation != 180) + { + printf("orientation value must be 0 or 180\n"); + return 1; + } + break; + case 'a': + strncpy(anim_path, optarg, sizeof(anim_path)); + break; + case 'd': + y = atoi(optarg); + break; + case -1: + // just ignore + break; + /* Error handle: Mainly missing arg or illegal option */ + case '?': + if (optopt == 'I') + { + printf("prams -%c missing oled type (128x64/128x32/64x48)\n", optopt); + return 1; + } + else if (optopt == 'n') + { + printf("prams -%c missing 0,1,2... I2C device node number\n", optopt); + return 1; + } + else if (optopt == 'r') + { + printf("prams -%c missing 0 or 180 fields\n", optopt); + return 1; + } + else if (optopt == 'd') + { + printf("prams -%c missing delay values\n", optopt); + return 1; + } + else if (optopt == 'a') + { + printf("prams -%c missing file name\n", optopt); + return 1; + } + break; + default: + print_help(); + return 1; + } + } + + uint8_t rc = 0; + + // open the I2C device node + rc = ssd1306_init(i2c_node_address); + + if (rc != 0) + { + printf("no oled attached to /dev/i2c-%d\n", i2c_node_address); + return 1; + } + + // init oled module + if (oled_type[0] != 0) + { + if (strcmp(oled_type, "128x64") == 0) + rc += ssd1306_oled_default_config(64, 128); + else if (strcmp(oled_type, "128x32") == 0) + rc += ssd1306_oled_default_config(32, 128); + else if (strcmp(oled_type, "64x48") == 0) + rc += ssd1306_oled_default_config(48, 64); + } + else if (ssd1306_oled_load_resolution() != 0) + { + printf("please do init oled module with correction resolution first!\n"); + return 1; + } + + rc += ssd1306_oled_clear_screen(); + + // set rotate orientation + if (orientation > -1) + { + rc += ssd1306_oled_set_rotate(orientation); + } + + // display animation +if (anim_path[0]) { + FILE *fp = fopen(anim_path, "rb"); + if (!fp) { + perror("fopen anim_path"); + return 1; + } + + const int BMP_FRAME_SIZE = 1086; // 128x64 1bpp BMP + uint8_t frame_buf[BMP_FRAME_SIZE]; + int frame_num = 0; + + while (fread(frame_buf, 1, BMP_FRAME_SIZE, fp) == BMP_FRAME_SIZE) { + struct timeval start, end; + gettimeofday(&start, NULL); + + // Save to temp file + char temp_name[] = "/tmp/frameXXXXXX.bmp"; + int temp_fd = mkstemps(temp_name, 4); + if (temp_fd < 0) { + perror("mkstemps"); + fclose(fp); + return 1; + } + + if (write(temp_fd, frame_buf, BMP_FRAME_SIZE) != BMP_FRAME_SIZE) { + perror("write temp bmp"); + close(temp_fd); + unlink(temp_name); + break; + } + close(temp_fd); + + int w = 0, h = 0; + if (load_bitmap_to_bool_array(temp_name, bitmap, &w, &h) == 0) { + rc += ssd1306_oled_draw_bitmap(bitmap); + } else { + fprintf(stderr, "Failed to parse BMP frame %d\n", frame_num); + } + + unlink(temp_name); + + gettimeofday(&end, NULL); + + // Compute elapsed time in ms + long elapsed_ms = (end.tv_sec - start.tv_sec) * 1000L + + (end.tv_usec - start.tv_usec) / 1000L; + + long delay_ms = y - elapsed_ms; + if (delay_ms > 0) { + usleep(delay_ms * 1000); + } + + frame_num++; + } + + fclose(fp); + } + + // close the I2C device node + ssd1306_end(); + + return rc; +} diff --git a/main.c b/main.c index d80dfb7..57885f7 100644 --- a/main.c +++ b/main.c @@ -1,11 +1,14 @@ #include #include +#include #include #include #include #include #include +#include #include +#include #include "ssd1306.h" @@ -18,6 +21,7 @@ void print_help() printf("-f\t\t0/small font 5x7 1/normal font 8x8 (default normal font)\n"); printf("-h\t\thelp message\n"); printf("-i\t\t0/normal oled 1/invert oled\n"); + printf("-b\t\tread and display a bmp file (128x64 or 128x32 @ 1bpp)\n"); printf("-l\t\tput your line to display\n"); printf("-m\t\tput your strings to oled\n"); printf("-n\t\tI2C device node address (0,1,2..., default 0)\n"); @@ -26,6 +30,61 @@ void print_help() printf("-y\t\ty position\n"); } +// New buffer and path for bitmap file +#define MAX_WIDTH 128 +#define MAX_HEIGHT 64 +bool bitmap[MAX_HEIGHT][MAX_WIDTH]; +char bmp_path[256] = {0}; + +int load_bitmap_to_bool_array(const char *filepath, bool output[MAX_HEIGHT][MAX_WIDTH], int *out_width, int *out_height) { + FILE *fp = fopen(filepath, "rb"); + if (!fp) { + perror("fopen"); + return -1; + } + + uint8_t header[54]; + if (fread(header, 1, 54, fp) != 54 || header[0] != 'B' || header[1] != 'M') { + fclose(fp); + fprintf(stderr, "Invalid BMP file\n"); + return -1; + } + + uint32_t offset = *(uint32_t *)&header[10]; + int width = *(int32_t *)&header[18]; + int height = *(int32_t *)&header[22]; + uint16_t bpp = *(uint16_t *)&header[28]; + + if (bpp != 1 || width > MAX_WIDTH || (height != 32 && height != 64)) { + fclose(fp); + fprintf(stderr, "Only 1bpp 128x32 or 128x64 BMPs supported.\n"); + return -1; + } + + fseek(fp, offset, SEEK_SET); + size_t row_bytes = ((width + 31) / 32) * 4; + + memset(output, 0, sizeof(bool) * MAX_WIDTH * MAX_HEIGHT); + + // BMP rows are bottom-up + for (int row = height - 1; row >= 0; row--) { + uint8_t row_data[row_bytes]; + fread(row_data, 1, row_bytes, fp); + + for (int col = 0; col < width; col++) { + int byte_index = col / 8; + int bit_index = 7 - (col % 8); + uint8_t byte = row_data[byte_index]; + output[row][col] = (byte >> bit_index) & 0x01; + } + } + + fclose(fp); + *out_width = width; + *out_height = height; + return 0; +} + int main(int argc, char **argv) { uint8_t i2c_node_address = 0; @@ -45,7 +104,7 @@ int main(int argc, char **argv) while(cmd_opt != -1) { - cmd_opt = getopt(argc, argv, "I:c::d:f:hi:l:m:n:r:x:y:"); + cmd_opt = getopt(argc, argv, "I:c::d:f:hi:l:m:n:r:x:y:b:"); /* Lets parse */ switch (cmd_opt) { @@ -97,6 +156,9 @@ int main(int argc, char **argv) case 'y': y = atoi(optarg); break; + case 'b': + strncpy(bmp_path, optarg, sizeof(bmp_path)); + break; case -1: // just ignore break; @@ -193,7 +255,17 @@ int main(int argc, char **argv) { rc += ssd1306_oled_onoff(display); } - + + // print bitmap image to screen + if (bmp_path[0]) { + int w = 0, h = 0; + if (load_bitmap_to_bool_array(bmp_path, bitmap, &w, &h) == 0) { + rc += ssd1306_oled_draw_bitmap(bitmap); + } else { + fprintf(stderr, "Failed to load or parse bitmap from %s\n", bmp_path); + } + } + // set cursor XY if (x > -1 && y > -1) { diff --git a/owo.bmp b/owo.bmp new file mode 100644 index 0000000..ffa8218 Binary files /dev/null and b/owo.bmp differ diff --git a/ssd1306.c b/ssd1306.c index e3d4764..62ea53d 100644 --- a/ssd1306.c +++ b/ssd1306.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -464,3 +465,38 @@ uint8_t ssd1306_oled_load_resolution() return 0; } + +uint8_t ssd1306_oled_draw_bitmap(bool bitmap[SSD1306_MAX_HEIGHT][SSD1306_MAX_WIDTH]) +{ + if (max_lines == 0 || max_columns == 0) + return 1; + + uint8_t pages = max_lines / 8; // 8 pixels per page + uint8_t col, page; + + for (page = 0; page < pages; page++) + { + // Set page and column to start of the line + ssd1306_oled_set_XY(0, page); + + // First byte is the control byte + data_buf[0] = SSD1306_DATA_CONTROL_BYTE; + + for (col = 0; col < max_columns; col++) + { + uint8_t byte = 0; + for (uint8_t bit = 0; bit < 8; bit++) + { + if (bitmap[page * 8 + bit][col]) + byte |= (1 << bit); + } + data_buf[col + 1] = byte; + } + + // Send the full line (control byte + display data) + if (_i2c_write(data_buf, max_columns + 1) != 0) + return 1; + } + + return 0; +} diff --git a/ssd1306.h b/ssd1306.h index cfb3687..30bafdc 100644 --- a/ssd1306.h +++ b/ssd1306.h @@ -1,6 +1,11 @@ #ifndef __SSD1306_H__ #define __SSD1306_H__ +#include + +#define SSD1306_MAX_WIDTH 128 +#define SSD1306_MAX_HEIGHT 64 + #define SSD1306_I2C_ADDR 0x3c #define SSD1306_COMM_CONTROL_BYTE 0x00 @@ -81,5 +86,6 @@ uint8_t ssd1306_oled_clear_line(uint8_t row); uint8_t ssd1306_oled_clear_screen(); uint8_t ssd1306_oled_save_resolution(uint8_t column, uint8_t row); uint8_t ssd1306_oled_load_resolution(); +uint8_t ssd1306_oled_draw_bitmap(bool bitmap[SSD1306_MAX_HEIGHT][SSD1306_MAX_WIDTH]); #endif