Newer
Older
ESP32-RetroPlay / components / ssd1306 / ssd1306.c
#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "esp_log.h"

#include "ssd1306.h"
#include "font8x8_basic.h"

#define PACK8 __attribute__((aligned( __alignof__( uint8_t ) ), packed ))

typedef union out_column_t {
	uint32_t u32;
	uint8_t  u8[4];
} PACK8 out_column_t;

void ssd1306_init(SSD1306_t * dev, int width, int height)
{
	if (dev->_address == SPI_ADDRESS) {
		spi_init(dev, width, height);
	} else {
		i2c_init(dev, width, height);
	}
	// Initialize internal buffer
	for (int i=0;i<dev->_pages;i++) {
		memset(dev->_page[i]._segs, 0, 128);
	}
}

int ssd1306_get_width(SSD1306_t * dev)
{
	return dev->_width;
}

int ssd1306_get_height(SSD1306_t * dev)
{
	return dev->_height;
}

int ssd1306_get_pages(SSD1306_t * dev)
{
	return dev->_pages;
}

// void ssd1306_show_buffer(SSD1306_t * dev)
// {
// 	if (dev->_address == SPI_ADDRESS) {
// 		for (int page=0; page<dev->_pages;page++) {
// 			spi_display_image(dev, page, 0, dev->_page[page]._segs, dev->_width);
// 		}
// 	} else {
// 		for (int page=0; page<dev->_pages;page++) {
// 			i2c_display_image(dev, page, 0, dev->_page[page]._segs, dev->_width);
// 		}
// 	}
// }

void ssd1306_set_buffer(SSD1306_t * dev, const uint8_t * buffer)
{
	int index = 0;
	for (int page=0; page<dev->_pages;page++) {
		memcpy(&dev->_page[page]._segs, &buffer[index], 128);
		index = index + 128;
	}
}

void ssd1306_get_buffer(SSD1306_t * dev, uint8_t * buffer)
{
	int index = 0;
	for (int page=0; page<dev->_pages;page++) {
		memcpy(&buffer[index], &dev->_page[page]._segs, 128);
		index = index + 128;
	}
}

void ssd1306_set_page(SSD1306_t * dev, int page, const uint8_t * buffer)
{
	memcpy(&dev->_page[page]._segs, buffer, 128);
}

void ssd1306_get_page(SSD1306_t * dev, int page, uint8_t * buffer)
{
	memcpy(buffer, &dev->_page[page]._segs, 128);
}

void ssd1306_display_image(SSD1306_t * dev, int page, int seg, const uint8_t * images, int width)
{
	if (dev->_address == SPI_ADDRESS) {
		spi_display_image(dev, page, seg, images, width);
	} else {
		i2c_display_image(dev, page, seg, images, width);
	}
	// Set to internal buffer
	memcpy(&dev->_page[page]._segs[seg], images, width);
}

void ssd1306_display_text(SSD1306_t * dev, int page, const char * text, int text_len, bool invert)
{
	if (page >= dev->_pages) return;
	int _text_len = text_len;
	if (_text_len > 16) _text_len = 16;

	int seg = 0;
	uint8_t image[8];
	for (int i = 0; i < _text_len; i++) {
		memcpy(image, font8x8_basic_tr[(uint8_t)text[i]], 8);
		if (invert) ssd1306_invert(image, 8);
		if (dev->_flip) ssd1306_flip(image, 8);
		ssd1306_display_image(dev, page, seg, image, 8);
		seg = seg + 8;
	}
}

void ssd1306_display_text_box1(SSD1306_t * dev, int page, int seg, const char * text, int box_width, int text_len, bool invert, int delay)
{
	if (page >= dev->_pages) return;
	int text_box_pixel = box_width * 8;
	if (seg + text_box_pixel > dev->_width) return;

	int _seg = seg;
	uint8_t image[8];
	for (int i = 0; i < box_width; i++) {
		memcpy(image, font8x8_basic_tr[(uint8_t)text[i]], 8);
		if (invert) ssd1306_invert(image, 8);
		if (dev->_flip) ssd1306_flip(image, 8);
		ssd1306_display_image(dev, page, _seg, image, 8);
		_seg = _seg + 8;
	}
	vTaskDelay(delay);

	// Horizontally scroll inside the box
	for (int _text=box_width;_text<text_len;_text++) {
		memcpy(image, font8x8_basic_tr[(uint8_t)text[_text]], 8);
		if (invert) ssd1306_invert(image, 8);
		if (dev->_flip) ssd1306_flip(image, 8);
		for (int _bit=0;_bit<8;_bit++) {
			for (int _pixel=0;_pixel<text_box_pixel;_pixel++) {
				//ESP_LOGI(__FUNCTION__, "_text=%d _bit=%d _pixel=%d", _text, _bit, _pixel);
				dev->_page[page]._segs[_pixel+seg] = dev->_page[page]._segs[_pixel+seg+1];
			}
			dev->_page[page]._segs[seg+text_box_pixel-1] = image[_bit];
			ssd1306_display_image(dev, page, seg, &dev->_page[page]._segs[seg], text_box_pixel);
			vTaskDelay(delay);
		}
	}
}

void ssd1306_display_text_box2(SSD1306_t * dev, int page, int seg, const char * text, int box_width, int text_len, bool invert, int delay)
{
	if (page >= dev->_pages) return;
	int text_box_pixel = box_width * 8;
	if (seg + text_box_pixel > dev->_width) return;

	int _seg = seg;
	uint8_t image[8];

	// Fill the text box with blanks
	for (int i = 0; i < box_width; i++) {
		//memcpy(image, font8x8_basic_tr[(uint8_t)text[i]], 8);
		memcpy(image, font8x8_basic_tr[0x20], 8);
		if (invert) ssd1306_invert(image, 8);
		if (dev->_flip) ssd1306_flip(image, 8);
		ssd1306_display_image(dev, page, _seg, image, 8);
		_seg = _seg + 8;
	}
	vTaskDelay(delay);

	// Horizontally scroll inside the box
	for (int _text=0;_text<text_len;_text++) {
		memcpy(image, font8x8_basic_tr[(uint8_t)text[_text]], 8);
		if (invert) ssd1306_invert(image, 8);
		if (dev->_flip) ssd1306_flip(image, 8);
		for (int _bit=0;_bit<8;_bit++) {
			for (int _pixel=0;_pixel<text_box_pixel;_pixel++) {
				//ESP_LOGI(__FUNCTION__, "_text=%d _bit=%d _pixel=%d", _text, _bit, _pixel);
				dev->_page[page]._segs[_pixel+seg] = dev->_page[page]._segs[_pixel+seg+1];
			}
			dev->_page[page]._segs[seg+text_box_pixel-1] = image[_bit];
			ssd1306_display_image(dev, page, seg, &dev->_page[page]._segs[seg], text_box_pixel);
			vTaskDelay(delay);
		}
	}

	// Horizontally scroll inside the box
	for (int _text=0;_text<box_width;_text++) {
		memcpy(image, font8x8_basic_tr[0x20], 8);
		if (invert) ssd1306_invert(image, 8);
		if (dev->_flip) ssd1306_flip(image, 8);
		for (int _bit=0;_bit<8;_bit++) {
			for (int _pixel=0;_pixel<text_box_pixel;_pixel++) {
				//ESP_LOGI(__FUNCTION__, "_text=%d _bit=%d _pixel=%d", _text, _bit, _pixel);
				dev->_page[page]._segs[_pixel+seg] = dev->_page[page]._segs[_pixel+seg+1];
			}
			dev->_page[page]._segs[seg+text_box_pixel-1] = image[_bit];
			ssd1306_display_image(dev, page, seg, &dev->_page[page]._segs[seg], text_box_pixel);
			vTaskDelay(delay);
		}
	}
}

// by Coert Vonk
void 
ssd1306_display_text_x3(SSD1306_t * dev, int page, const char * text, int text_len, bool invert)
{
	if (page >= dev->_pages) return;
	int _text_len = text_len;
	if (_text_len > 5) _text_len = 5;

	int seg = 0;

	for (int nn = 0; nn < _text_len; nn++) {

		uint8_t const * const in_columns = font8x8_basic_tr[(uint8_t)text[nn]];

		// make the character 3x as high
		out_column_t out_columns[8];
		memset(out_columns, 0, sizeof(out_columns));

		for (int xx = 0; xx < 8; xx++) { // for each column (x-direction)

			uint32_t in_bitmask = 0b1;
			uint32_t out_bitmask = 0b111;

			for (int yy = 0; yy < 8; yy++) { // for pixel (y-direction)
				if (in_columns[xx] & in_bitmask) {
					out_columns[xx].u32 |= out_bitmask;
				}
				in_bitmask <<= 1;
				out_bitmask <<= 3;
			}
		}

		// render character in 8 column high pieces, making them 3x as wide
		for (int yy = 0; yy < 3; yy++)	{ // for each group of 8 pixels high (y-direction)

			uint8_t image[24];
			for (int xx = 0; xx < 8; xx++) { // for each column (x-direction)
				image[xx*3+0] = 
				image[xx*3+1] = 
				image[xx*3+2] = out_columns[xx].u8[yy];
			}
			if (invert) ssd1306_invert(image, 24);
			if (dev->_flip) ssd1306_flip(image, 24);
			if (dev->_address == SPI_ADDRESS) {
				spi_display_image(dev, page+yy, seg, image, 24);
			} else {
				i2c_display_image(dev, page+yy, seg, image, 24);
			}
			memcpy(&dev->_page[page+yy]._segs[seg], image, 24);
		}
		seg = seg + 24;
	}
}

void ssd1306_clear_screen(SSD1306_t * dev, bool invert)
{
	memset(dev->buffer, 0, SSD1306_BUFFER_SIZE); // Clear the buffer
	char space[16];
	memset(space, 0x00, sizeof(space));
	for (int page = 0; page < dev->_pages; page++) {
		ssd1306_display_text(dev, page, space, sizeof(space), invert);
	}
}

void ssd1306_clear_line(SSD1306_t * dev, int page, bool invert)
{
	char space[16];
	memset(space, 0x00, sizeof(space));
	ssd1306_display_text(dev, page, space, sizeof(space), invert);
}

void ssd1306_contrast(SSD1306_t * dev, int contrast)
{
	if (dev->_address == SPI_ADDRESS) {
		spi_contrast(dev, contrast);
	} else {
		i2c_contrast(dev, contrast);
	}
}

void ssd1306_software_scroll(SSD1306_t * dev, int start, int end)
{
	ESP_LOGD(__FUNCTION__, "software_scroll start=%d end=%d _pages=%d", start, end, dev->_pages);
	if (start < 0 || end < 0) {
		dev->_scEnable = false;
	} else if (start >= dev->_pages || end >= dev->_pages) {
		dev->_scEnable = false;
	} else {
		dev->_scEnable = true;
		dev->_scStart = start;
		dev->_scEnd = end;
		dev->_scDirection = 1;
		if (start > end ) dev->_scDirection = -1;
	}
}


void ssd1306_scroll_text(SSD1306_t * dev, const char * text, int text_len, bool invert)
{
	ESP_LOGD(__FUNCTION__, "dev->_scEnable=%d", dev->_scEnable);
	if (dev->_scEnable == false) return;

	void (*func)(SSD1306_t * dev, int page, int seg, const uint8_t * images, int width);
	if (dev->_address == SPI_ADDRESS) {
		func = spi_display_image;
	} else {
		func = i2c_display_image;
	}

	int srcIndex = dev->_scEnd - dev->_scDirection;
	while(1) {
		int dstIndex = srcIndex + dev->_scDirection;
		ESP_LOGD(__FUNCTION__, "srcIndex=%d dstIndex=%d", srcIndex,dstIndex);
		for(int seg = 0; seg < dev->_width; seg++) {
			dev->_page[dstIndex]._segs[seg] = dev->_page[srcIndex]._segs[seg];
		}
		(*func)(dev, dstIndex, 0, dev->_page[dstIndex]._segs, sizeof(dev->_page[dstIndex]._segs));
		if (srcIndex == dev->_scStart) break;
		srcIndex = srcIndex - dev->_scDirection;
	}
	
	int _text_len = text_len;
	if (_text_len > 16) _text_len = 16;
	
	ssd1306_display_text(dev, srcIndex, text, text_len, invert);
}

void ssd1306_scroll_clear(SSD1306_t * dev)
{
	ESP_LOGD(__FUNCTION__, "dev->_scEnable=%d", dev->_scEnable);
	if (dev->_scEnable == false) return;

	int srcIndex = dev->_scEnd - dev->_scDirection;
	while(1) {
		int dstIndex = srcIndex + dev->_scDirection;
		ESP_LOGD(__FUNCTION__, "srcIndex=%d dstIndex=%d", srcIndex,dstIndex);
		ssd1306_clear_line(dev, dstIndex, false);
		if (dstIndex == dev->_scStart) break;
		srcIndex = srcIndex - dev->_scDirection;
	}
}


void ssd1306_hardware_scroll(SSD1306_t * dev, ssd1306_scroll_type_t scroll)
{
	if (dev->_address == SPI_ADDRESS) {
		spi_hardware_scroll(dev, scroll);
	} else {
		i2c_hardware_scroll(dev, scroll);
	}
}

// delay = 0 : display with no wait
// delay > 0 : display with wait
// delay < 0 : no display
void ssd1306_wrap_arround(SSD1306_t * dev, ssd1306_scroll_type_t scroll, int start, int end, int8_t delay)
{
	if (scroll == SCROLL_RIGHT) {
		int _start = start; // 0 to 7
		int _end = end; // 0 to 7
		if (_end >= dev->_pages) _end = dev->_pages - 1;
		uint8_t wk;
		//for (int page=0;page<dev->_pages;page++) {
		for (int page=_start;page<=_end;page++) {
			wk = dev->_page[page]._segs[127];
			for (int seg=127;seg>0;seg--) {
				dev->_page[page]._segs[seg] = dev->_page[page]._segs[seg-1];
			}
			dev->_page[page]._segs[0] = wk;
		}

	} else if (scroll == SCROLL_LEFT) {
		int _start = start; // 0 to 7
		int _end = end; // 0 to 7
		if (_end >= dev->_pages) _end = dev->_pages - 1;
		uint8_t wk;
		//for (int page=0;page<dev->_pages;page++) {
		for (int page=_start;page<=_end;page++) {
			wk = dev->_page[page]._segs[0];
			for (int seg=0;seg<127;seg++) {
				dev->_page[page]._segs[seg] = dev->_page[page]._segs[seg+1];
			}
			dev->_page[page]._segs[127] = wk;
		}

	} else if (scroll == SCROLL_UP) {
		int _start = start; // 0 to {width-1}
		int _end = end; // 0 to {width-1}
		if (_end >= dev->_width) _end = dev->_width - 1;
		uint8_t wk0;
		uint8_t wk1;
		uint8_t wk2;
		uint8_t save[128];
		// Save pages 0
		for (int seg=0;seg<128;seg++) {
			save[seg] = dev->_page[0]._segs[seg];
		}
		// Page0 to Page6
		for (int page=0;page<dev->_pages-1;page++) {
			//for (int seg=0;seg<128;seg++) {
			for (int seg=_start;seg<=_end;seg++) {
				wk0 = dev->_page[page]._segs[seg];
				wk1 = dev->_page[page+1]._segs[seg];
				if (dev->_flip) wk0 = ssd1306_rotate_byte(wk0);
				if (dev->_flip) wk1 = ssd1306_rotate_byte(wk1);
				if (seg == 0) {
					ESP_LOGD(__FUNCTION__, "b page=%d wk0=%02x wk1=%02x", page, wk0, wk1);
				}
				wk0 = wk0 >> 1;
				wk1 = wk1 & 0x01;
				wk1 = wk1 << 7;
				wk2 = wk0 | wk1;
				if (seg == 0) {
					ESP_LOGD(__FUNCTION__, "a page=%d wk0=%02x wk1=%02x wk2=%02x", page, wk0, wk1, wk2);
				}
				if (dev->_flip) wk2 = ssd1306_rotate_byte(wk2);
				dev->_page[page]._segs[seg] = wk2;
			}
		}
		// Page7
		int pages = dev->_pages-1;
		//for (int seg=0;seg<128;seg++) {
		for (int seg=_start;seg<=_end;seg++) {
			wk0 = dev->_page[pages]._segs[seg];
			wk1 = save[seg];
			if (dev->_flip) wk0 = ssd1306_rotate_byte(wk0);
			if (dev->_flip) wk1 = ssd1306_rotate_byte(wk1);
			wk0 = wk0 >> 1;
			wk1 = wk1 & 0x01;
			wk1 = wk1 << 7;
			wk2 = wk0 | wk1;
			if (dev->_flip) wk2 = ssd1306_rotate_byte(wk2);
			dev->_page[pages]._segs[seg] = wk2;
		}

	} else if (scroll == SCROLL_DOWN) {
		int _start = start; // 0 to {width-1}
		int _end = end; // 0 to {width-1}
		if (_end >= dev->_width) _end = dev->_width - 1;
		uint8_t wk0;
		uint8_t wk1;
		uint8_t wk2;
		uint8_t save[128];
		// Save pages 7
		int pages = dev->_pages-1;
		for (int seg=0;seg<128;seg++) {
			save[seg] = dev->_page[pages]._segs[seg];
		}
		// Page7 to Page1
		for (int page=pages;page>0;page--) {
			//for (int seg=0;seg<128;seg++) {
			for (int seg=_start;seg<=_end;seg++) {
				wk0 = dev->_page[page]._segs[seg];
				wk1 = dev->_page[page-1]._segs[seg];
				if (dev->_flip) wk0 = ssd1306_rotate_byte(wk0);
				if (dev->_flip) wk1 = ssd1306_rotate_byte(wk1);
				if (seg == 0) {
					ESP_LOGD(__FUNCTION__, "b page=%d wk0=%02x wk1=%02x", page, wk0, wk1);
				}
				wk0 = wk0 << 1;
				wk1 = wk1 & 0x80;
				wk1 = wk1 >> 7;
				wk2 = wk0 | wk1;
				if (seg == 0) {
					ESP_LOGD(__FUNCTION__, "a page=%d wk0=%02x wk1=%02x wk2=%02x", page, wk0, wk1, wk2);
				}
				if (dev->_flip) wk2 = ssd1306_rotate_byte(wk2);
				dev->_page[page]._segs[seg] = wk2;
			}
		}
		// Page0
		//for (int seg=0;seg<128;seg++) {
		for (int seg=_start;seg<=_end;seg++) {
			wk0 = dev->_page[0]._segs[seg];
			wk1 = save[seg];
			if (dev->_flip) wk0 = ssd1306_rotate_byte(wk0);
			if (dev->_flip) wk1 = ssd1306_rotate_byte(wk1);
			wk0 = wk0 << 1;
			wk1 = wk1 & 0x80;
			wk1 = wk1 >> 7;
			wk2 = wk0 | wk1;
			if (dev->_flip) wk2 = ssd1306_rotate_byte(wk2);
			dev->_page[0]._segs[seg] = wk2;
		}

	} else if (scroll == PAGE_SCROLL_DOWN) {
		uint8_t save[128];
		// Save pages 7
		for (int seg=0;seg<128;seg++) {
			save[seg] = dev->_page[dev->_pages-1]._segs[seg];
		}
		// Page7 to Page1
		for (int page=dev->_pages-1;page>0;page--) {
			for (int seg=0;seg<128;seg++) {
				dev->_page[page]._segs[seg] = dev->_page[page-1]._segs[seg];
			}
		}
		// Store  pages 0
		for (int seg=0;seg<128;seg++) {
			dev->_page[0]._segs[seg] = save[seg];
		}

	} else if (scroll == PAGE_SCROLL_UP) {
		uint8_t save[128];
		// Save pages 0
		for (int seg=0;seg<128;seg++) {
			save[seg] = dev->_page[0]._segs[seg];
		}
		// Page0 to Page6
		for (int page=0;page<dev->_pages-1;page++) {
			for (int seg=0;seg<128;seg++) {
				dev->_page[page]._segs[seg] = dev->_page[page+1]._segs[seg];
			}
		}
		// Store  pages 7
		for (int seg=0;seg<128;seg++) {
			dev->_page[dev->_pages-1]._segs[seg] = save[seg];
		}
	}

	if (delay >= 0) {
		for (int page=0;page<dev->_pages;page++) {
			if (dev->_address == SPI_ADDRESS) {
				spi_display_image(dev, page, 0, dev->_page[page]._segs, 128);
			} else {
				i2c_display_image(dev, page, 0, dev->_page[page]._segs, 128);
			}
			if (delay) vTaskDelay(delay);
		}
	}

}

void _ssd1306_bitmaps(SSD1306_t * dev, int xpos, int ypos, const uint8_t * bitmap, int width, int height, bool invert)
{
	if ( (width % 8) != 0) {
		ESP_LOGE(__FUNCTION__, "width must be a multiple of 8");
		return;
	}
	int _width = width / 8;
	uint8_t wk0;
	uint8_t wk1;
	uint8_t wk2;
	uint8_t page = (ypos / 8);
	uint8_t _seg = xpos;
	uint8_t dstBits = (ypos % 8);
	ESP_LOGD(__FUNCTION__, "_width=%d ypos=%d page=%d dstBits=%d", _width, ypos, page, dstBits);
	int offset = 0;
	for(int _height=0;_height<height;_height++) {
		for (int index=0;index<_width;index++) {
			for (int srcBits=7; srcBits>=0; srcBits--) {
				wk0 = dev->_page[page]._segs[_seg];
				if (dev->_flip) wk0 = ssd1306_rotate_byte(wk0);

				wk1 = bitmap[index+offset];
				if (invert) wk1 = ~wk1;

				//wk2 = ssd1306_copy_bit(bitmap[index+offset], srcBits, wk0, dstBits);
				wk2 = ssd1306_copy_bit(wk1, srcBits, wk0, dstBits);
				if (dev->_flip) wk2 = ssd1306_rotate_byte(wk2);

				ESP_LOGD(__FUNCTION__, "index=%d offset=%d wk1=0x%x page=%d _seg=%d, wk2=%02x", index, offset, wk1, page, _seg, wk2);
				if (_seg >= 128) {
					ESP_LOGW(__FUNCTION__, "segment is out of range");
					break;
				}
				if (page >= dev->_pages) {
					ESP_LOGW(__FUNCTION__, "page is out of range");
					break;
				}
				dev->_page[page]._segs[_seg] = wk2;
				_seg++;
			}
		}
		//vTaskDelay(1);
		offset = offset + _width;
		dstBits++;
		_seg = xpos;
		if (dstBits == 8) {
			page++;
			dstBits=0;
		}
	}

#if 0
	for (int _seg=ypos;_seg<ypos+width;_seg++) {
		ssd1306_dump_page(dev, page-1, _seg);
	}
	for (int _seg=ypos;_seg<ypos+width;_seg++) {
		ssd1306_dump_page(dev, page, _seg);
	}
#endif
	//ssd1306_show_buffer(dev);
}


void ssd1306_bitmaps(SSD1306_t * dev, int xpos, int ypos, const uint8_t * bitmap, int width, int height, bool invert)
{
	_ssd1306_bitmaps(dev, xpos, ypos, bitmap, width, height, invert);
	
	// Calculate the range of pages and segments to update
	int start_page = ypos / 8;
	int end_page = (ypos + height - 1) / 8;
	int start_seg = xpos;
	int end_seg = xpos + width - 1;

	// Update only the modified pages and segments
	for (int page = start_page; page <= end_page; page++) {
		int seg_start = (page == start_page) ? start_seg : 0;
		int seg_end = (page == end_page) ? end_seg : 127;
		int seg_width = seg_end - seg_start + 1;
		ssd1306_display_image(dev, page, seg_start, &dev->_page[page]._segs[seg_start], seg_width);
	}
}


// Set pixel to internal buffer. Not show it.
// void _ssd1306_pixel(SSD1306_t * dev, int xpos, int ypos, bool invert)
// {
// 	uint8_t _page = (ypos / 8);
// 	uint8_t _bits = (ypos % 8);
// 	uint8_t _seg = xpos;
// 	uint8_t wk0 = dev->_page[_page]._segs[_seg];
// 	uint8_t wk1 = 1 << _bits;
// 	ESP_LOGD(__FUNCTION__, "ypos=%d _page=%d _bits=%d wk0=0x%02x wk1=0x%02x", ypos, _page, _bits, wk0, wk1);
// 	if (invert) {
// 		wk0 = wk0 & ~wk1;
// 	} else {
// 		wk0 = wk0 | wk1;
// 	}
// 	if (dev->_flip) wk0 = ssd1306_rotate_byte(wk0);
// 	ESP_LOGD(__FUNCTION__, "wk0=0x%02x wk1=0x%02x", wk0, wk1);
// 	dev->_page[_page]._segs[_seg] = wk0;
// }

void _ssd1306_pixel(SSD1306_t *dev, int x, int y, bool color) {
    if (x < 0 || x >= dev->_width || y < 0 || y >= dev->_height) return; // bounds check

    int page = y / 8;
    int bit = y % 8;
    uint8_t *byte = &dev->buffer[x + page * dev->_width];

    // Check if the pixel's current state is different from the new color
    bool current_color = (*byte >> bit) & 1;

    if (current_color != color) {
        // Only modify if the color is actually changing
        if (color)
            *byte |= (1 << bit);
        else
            *byte &= ~(1 << bit);

        // Mark the page as dirty
        if (page >= 0 && page < sizeof(dev->_dirty_pages) / sizeof(dev->_dirty_pages[0])) {
            dev->_dirty_pages[page] = true;
        }
    }
}


// Set line to internal buffer. Not show it.
void _ssd1306_line(SSD1306_t * dev, int x1, int y1, int x2, int y2,  bool invert)
{
	int i;
	int dx,dy;
	int sx,sy;
	int E;

	/* distance between two points */
	dx = ( x2 > x1 ) ? x2 - x1 : x1 - x2;
	dy = ( y2 > y1 ) ? y2 - y1 : y1 - y2;

	/* direction of two point */
	sx = ( x2 > x1 ) ? 1 : -1;
	sy = ( y2 > y1 ) ? 1 : -1;

	/* inclination < 1 */
	if ( dx > dy ) {
		E = -dx;
		for ( i = 0 ; i <= dx ; i++ ) {
			_ssd1306_pixel(dev, x1, y1, invert);
			x1 += sx;
			E += 2 * dy;
			if ( E >= 0 ) {
			y1 += sy;
			E -= 2 * dx;
		}
	}

	/* inclination >= 1 */
	} else {
		E = -dy;
		for ( i = 0 ; i <= dy ; i++ ) {
			_ssd1306_pixel(dev, x1, y1, invert);
			y1 += sy;
			E += 2 * dx;
			if ( E >= 0 ) {
				x1 += sx;
				E -= 2 * dy;
			}
		}
	}
}

// Draw circle
void _ssd1306_circle(SSD1306_t * dev, int x0, int y0, int r, unsigned int opt, bool invert)
{
	int x;
	int y;
	int err;
	int old_err;

	x=0;
	y=-r;
	err=2-2*r;
	do{
		if ((opt & OLED_DRAW_UPPER_LEFT) == OLED_DRAW_UPPER_LEFT)
			_ssd1306_pixel(dev, x0-x, y0+y, invert); 
		if ((opt & OLED_DRAW_UPPER_RIGHT) == OLED_DRAW_UPPER_RIGHT)
			_ssd1306_pixel(dev, x0-y, y0-x, invert); 
		if ((opt & OLED_DRAW_LOWER_RIGHT) == OLED_DRAW_LOWER_RIGHT)
			_ssd1306_pixel(dev, x0+x, y0-y, invert); 
		if ((opt & OLED_DRAW_LOWER_LEFT) == OLED_DRAW_LOWER_LEFT)
			_ssd1306_pixel(dev, x0+y, y0+x, invert); 
		if ((old_err=err)<=x) err+=++x*2+1;
		if (old_err>y || err>x) err+=++y*2+1;	 
	} while(y<0);
}

// Draw disc (fill circle)
void _ssd1306_disc(SSD1306_t * dev, int x0, int y0, int r, unsigned int opt, bool invert)
{
	int x;
	int y;
	int err;
	int old_err;
	int ChangeX;

	x=0;
	y=-r;
	err=2-2*r;
	ChangeX=1;
	do{
		if(ChangeX) {
			//_ssd1306_line(dev, x0-x, y0-y, x0-x, y0+y, invert);
			//_ssd1306_line(dev, x0+x, y0-y, x0+x, y0+y, invert);
			if ((opt & OLED_DRAW_LOWER_LEFT) == OLED_DRAW_LOWER_LEFT)
				_ssd1306_line(dev, x0-x, y0-y, x0-x, y0, invert);
			if ((opt & OLED_DRAW_UPPER_LEFT) == OLED_DRAW_UPPER_LEFT)
				_ssd1306_line(dev, x0-x, y0, x0-x, y0+y, invert);
			if ((opt & OLED_DRAW_LOWER_RIGHT) == OLED_DRAW_LOWER_RIGHT)
				_ssd1306_line(dev, x0+x, y0-y, x0+x, y0, invert);
			if ((opt & OLED_DRAW_UPPER_RIGHT) == OLED_DRAW_UPPER_RIGHT)
				_ssd1306_line(dev, x0+x, y0, x0+x, y0+y, invert);

		} // endif
		ChangeX=(old_err=err)<=x;
		if (ChangeX) err+=++x*2+1;
		if (old_err>y || err>x) err+=++y*2+1;
	} while(y<=0);


}

// Draw cursor
void _ssd1306_cursor(SSD1306_t * dev, int x0, int y0, int r, bool invert)
{
	_ssd1306_line(dev, x0-r, y0, x0+r, y0, invert);
	_ssd1306_line(dev, x0, y0-r, x0, y0+r, invert);
}

void ssd1306_invert(uint8_t *buf, size_t blen)
{
	uint8_t wk;
	for(int i=0; i<blen; i++){
		wk = buf[i];
		buf[i] = ~wk;
	}
}

// Flip upside down
void ssd1306_flip(uint8_t *buf, size_t blen)
{
	for(int i=0; i<blen; i++){
		buf[i] = ssd1306_rotate_byte(buf[i]);
	}
}

uint8_t ssd1306_copy_bit(uint8_t src, int srcBits, uint8_t dst, int dstBits)
{
	ESP_LOGD(__FUNCTION__, "src=%02x srcBits=%d dst=%02x dstBits=%d", src, srcBits, dst, dstBits);
	uint8_t smask = 0x01 << srcBits;
	uint8_t dmask = 0x01 << dstBits;
	uint8_t _src = src & smask;
#if 0
	if (_src != 0) _src = 1;
	uint8_t _wk = _src << dstBits;
	uint8_t _dst = dst | _wk;
#endif
	uint8_t _dst;
	if (_src != 0) {
		_dst = dst | dmask; // set bit
	} else {
		_dst = dst & ~(dmask); // clear bit
	}
	return _dst;
}


// Rotate 8-bit data
// 0x12-->0x48
uint8_t ssd1306_rotate_byte(uint8_t ch1) {
	uint8_t ch2 = 0;
	for (int j=0;j<8;j++) {
		ch2 = (ch2 << 1) + (ch1 & 0x01);
		ch1 = ch1 >> 1;
	}
	return ch2;
}


void ssd1306_fadeout(SSD1306_t * dev)
{
	void (*func)(SSD1306_t * dev, int page, int seg, const uint8_t * images, int width);
	if (dev->_address == SPI_ADDRESS) {
		func = spi_display_image;
	} else {
		func = i2c_display_image;
	}

	uint8_t image[1];
	for(int page=0; page<dev->_pages; page++) {
		image[0] = 0xFF;
		for(int line=0; line<8; line++) {
			if (dev->_flip) {
				image[0] = image[0] >> 1;
			} else {
				image[0] = image[0] << 1;
			}
			for(int seg=0; seg<128; seg++) {
				(*func)(dev, page, seg, image, 1);
				dev->_page[page]._segs[seg] = image[0];
			}
		}
	}
}

// Rotate character image
// Only valid for 8 dots x 8 dots
void ssd1306_rotate_image(uint8_t *image, bool flip) {
	uint8_t _image[8];
	uint8_t _smask = 0x01;
	for (int i=0;i<8;i++) {
		uint8_t _dmask = 0x80;
		_image[i] = 0;
		for (int j=0;j<8;j++) {
			uint8_t _wk = image[j] & _smask;
			ESP_LOGD(__FUNCTION__, "image[%d]=0x%x _smask=0x%x _wk=0x%x", j, image[j], _smask, _wk);
			if (_wk != 0) {
				_image[i] = _image[i] + _dmask;
			}
			_dmask = _dmask >> 1;
		}
		_smask = _smask << 1;
	}

	for (int i=0;i<8;i++) {
		image[i] = _image[i];
	}
	if (flip) ssd1306_flip(image, 8);
#if 0
	for (int i=0;i<8;i++) {
		ESP_LOGI(__FUNCTION__, "image[%d]=0x%x", i, image[i]);
	}
#endif
}

void ssd1306_display_rotate_text(SSD1306_t * dev, int seg, const char * text, int text_len, bool invert) {
	int _text_len = text_len;
	if (_text_len > 8) _text_len = 8;
	uint8_t image[8];
	int _page = dev->_pages-1;
	for (uint8_t i = 0; i < _text_len; i++) {
		memcpy(image, font8x8_basic_tr[(uint8_t)text[i]], 8);
		ssd1306_rotate_image(image, dev->_flip);
		ESP_LOGD(__FUNCTION__, "_page=%d seg=%d", _page, seg);
		if (invert) ssd1306_invert(image, 8);
		ssd1306_display_image(dev, _page, seg, image, 8);
		_page--;
		if (_page < 0) return;
	}
}

void ssd1306_dump(SSD1306_t dev)
{
	printf("_address=%x\n",dev._address);
	printf("_width=%x\n",dev._width);
	printf("_height=%x\n",dev._height);
	printf("_pages=%x\n",dev._pages);
}

void ssd1306_dump_page(SSD1306_t * dev, int page, int seg)
{
	ESP_LOGI(__FUNCTION__, "dev->_page[%d]._segs[%d]=%02x", page, seg, dev->_page[page]._segs[seg]);
}

void ssd1306_show_buffer(SSD1306_t *dev)
{
    for (int page = 0; page < dev->_pages; page++) {
        // Only update and send the page if it's marked as dirty
        if (dev->_dirty_pages[page]) {
            // Copy from buff to device page buffers
            memcpy(dev->_page[page]._segs, dev->buffer + page * dev->_width, dev->_width);

            if (dev->_address == SPI_ADDRESS) {
                spi_display_image(dev, page, 0, dev->_page[page]._segs, dev->_width);
            } else {
                i2c_display_image(dev, page, 0, dev->_page[page]._segs, dev->_width);
            }

            // After successfully sending the page, mark it as clean
            dev->_dirty_pages[page] = false;
        }
    }
}

/**
 * @brief Draws a single character on the display buffer at a specific pixel coordinate.
 * @param dev Pointer to the SSD1306_t structure.
 * @param x X-coordinate of the top-left corner of the character.
 * @param y Y-coordinate of the top-left corner of the character.
 * @param ch The character to draw.
 * @param font_size The font size (e.g., 1 for 8x8 font). Currently only font_size 1 is supported.
 * @param color True for white, false for black.
 */
void ssd1306_draw_char(SSD1306_t *dev, int x, int y, char ch, int font_size, bool color) {
    // Currently, only font_size 1 (8x8 pixels) is directly supported by this function.
    // Scaling for other sizes would require more complex pixel manipulation.
    if (font_size != 1) {
        return;
    }

    // Ensure character code is within bounds of font8x8_basic_tr
    if ((uint8_t)ch >= 128) { // Assuming font8x8_basic_tr has 128 characters
        ch = '?'; // Replace with a default character if out of bounds
    }

    // Get the 8-byte bitmap for the character (each byte represents a column)
    // font8x8_basic_tr is typically structured as [char_code][column_index],
    // where each byte's bits represent the pixels in that column.
    const uint8_t *bitmap = font8x8_basic_tr[(uint8_t)ch];

    for (int col = 0; col < 8; col++) { // Iterate through columns of the character bitmap (0 to 7)
        uint8_t column_data = bitmap[col];
        for (int row = 0; row < 8; row++) { // Iterate through rows within the column (0 to 7)
            // Calculate the absolute pixel coordinates on the display
            int pixel_x = x + col;
            int pixel_y = y + row;

            // Check if the pixel is set in the bitmap (bit is 1)
            if ((column_data >> row) & 0x01) {
                // Draw the pixel with the specified color
                _ssd1306_pixel(dev, pixel_x, pixel_y, color);
            } else {
                // If the bitmap pixel is OFF (bit is 0), explicitly draw it with the inverse color (black)
                // This ensures that previous content at this pixel location is cleared.
                _ssd1306_pixel(dev, pixel_x, pixel_y, !color);
            }
        }
    }
}