libflashrom: Return progress state to the library user

Projects using libflashrom like fwupd expect the user to wait for the
operation to complete. To avoid the user thinking the process has
"hung" or "got stuck" report back the progress complete of the erase,
write and read operations.

Add a new --progress flag to the CLI to report progress of operations.

Include a test for the dummy spi25 device.

Tested: ./test_build.sh; ./flashrom -p lspcon_i2c_spi:bus=7 -r /dev/null --progress

flashrom-stable:
* Closer to original libflashrom API.
* Split update_progress() into progress_start/_set/_add/_finish:
  Simplifies progress calls scattered through the code base. We let
  the core code in `flashprog.c` handle the total progress. Only API
  is flashprog_progress_add().  Erase progress is completely handled
  in `flashprog.c`. Fine grained read/write progress can be reported
  at the chip/programmer level.
* Add calls to all chip read/write paths and opaque programmers
  except for read_memmapped() (which is handled in follow ups).
* At least one wrinkle left: Erasing unaligned regions will slightly
  overshoot total progress.

Change-Id: I7197572bb7f19e3bdb2bde855d70a0f50fd3854c
Signed-off-by: Richard Hughes <richard@hughsie.com>
Signed-off-by: Daniel Campello <campello@chromium.org>
Signed-off-by: Nico Huber <nico.h@gmx.de>
Original-Reviewed-on: https://review.coreboot.org/c/flashrom/+/49643
Original-Reviewed-by: Edward O'Callaghan <quasisec@chromium.org>
Original-Reviewed-by: Anastasia Klimchuk <aklm@chromium.org>
Original-Reviewed-by: Thomas Heijligen <src@posteo.de>
Reviewed-on: https://review.sourcearcade.org/c/flashprog/+/74731
Reviewed-by: Arthur Heymans <arthur@aheymans.xyz>
diff --git a/82802ab.c b/82802ab.c
index a348347..0e45e3d 100644
--- a/82802ab.c
+++ b/82802ab.c
@@ -135,6 +135,7 @@
 		chip_writeb(flash, 0x40, dst);
 		chip_writeb(flash, *src++, dst++);
 		wait_82802ab(flash);
+		flashprog_progress_add(flash, 1);
 	}
 
 	/* FIXME: Ignore errors for now. */
diff --git a/at45db.c b/at45db.c
index acde996..985d3ca 100644
--- a/at45db.c
+++ b/at45db.c
@@ -250,6 +250,7 @@
 			msg_cerr("%s: error sending read command!\n", __func__);
 			return ret;
 		}
+		flashprog_progress_add(flash, chunk);
 		addr += chunk;
 		buf += chunk;
 		len -= chunk;
@@ -292,6 +293,7 @@
 		}
 		/* Copy result without dummy bytes into buf and advance address counter respectively. */
 		memcpy(buf, tmp + 4, chunk - 4);
+		flashprog_progress_add(flash, chunk - 4);
 		addr += chunk - 4;
 		buf += chunk - 4;
 		len -= chunk - 4;
@@ -550,6 +552,7 @@
 			msg_cerr("Writing page %u failed!\n", i);
 			return 1;
 		}
+		flashprog_progress_add(flash, page_size);
 	}
 	return 0;
 }
diff --git a/cli_classic.c b/cli_classic.c
index fede2ca..ab5f8b1 100644
--- a/cli_classic.c
+++ b/cli_classic.c
@@ -70,6 +70,7 @@
 #if CONFIG_PRINT_WIKI == 1
 	       " -z | --list-supported-wiki         print supported devices in wiki syntax\n"
 #endif
+	       "      --progress                    show progress percentage on the standard output\n"
 	       " -p | --programmer <name>[:<param>] specify the programmer device. One of\n");
 	list_programmers_linebreak(4, 80, 0);
 	printf(".\n\nYou can specify one of -h, -R, -L, "
@@ -219,6 +220,7 @@
 	bool read_it = false, write_it = false, erase_it = false, verify_it = false;
 	bool dont_verify_it = false, dont_verify_all = false;
 	bool list_supported = false;
+	bool show_progress = false;
 	struct flashprog_layout *layout = NULL;
 	static const struct programmer_entry *prog = NULL;
 	enum {
@@ -228,6 +230,7 @@
 		OPTION_FLASH_CONTENTS,
 		OPTION_FLASH_NAME,
 		OPTION_FLASH_SIZE,
+		OPTION_PROGRESS,
 	};
 	int ret = 0;
 
@@ -258,6 +261,7 @@
 		{"help",		0, NULL, 'h'},
 		{"version",		0, NULL, 'R'},
 		{"output",		1, NULL, 'o'},
+		{"progress",		0, NULL, OPTION_PROGRESS},
 		{NULL,			0, NULL, 0},
 	};
 
@@ -474,6 +478,9 @@
 				cli_classic_abort_usage("No log filename specified.\n");
 			}
 			break;
+		case OPTION_PROGRESS:
+			show_progress = true;
+			break;
 		default:
 			cli_classic_abort_usage(NULL);
 			break;
@@ -649,6 +656,9 @@
 
 	fill_flash = &flashes[0];
 
+	if (show_progress)
+		flashprog_set_progress_callback(fill_flash, &flashprog_progress_cb, NULL);
+
 	print_chip_support_status(fill_flash->chip);
 
 	if (max_decode_exceeded(matched_master, fill_flash) && !force) {
diff --git a/cli_output.c b/cli_output.c
index 2108e92..9074356 100644
--- a/cli_output.c
+++ b/cli_output.c
@@ -64,6 +64,32 @@
 	verbose_screen = oldverbose_screen;
 }
 
+static const char *flashprog_progress_stage_to_string(enum flashprog_progress_stage stage)
+{
+	if (stage == FLASHPROG_PROGRESS_READ)
+		return "READ";
+	if (stage == FLASHPROG_PROGRESS_WRITE)
+		return "WRITE";
+	if (stage == FLASHPROG_PROGRESS_ERASE)
+		return "ERASE";
+	return "UNKNOWN";
+}
+
+void flashprog_progress_cb(enum flashprog_progress_stage stage, size_t current, size_t total, void *user_data)
+{
+	static enum flashprog_progress_stage last_stage = (enum flashprog_progress_stage)-1;
+	static unsigned int last_pc = (unsigned int)-1;
+
+	const unsigned int pc = total ? (current * 100ull) / total : 100;
+
+	if (last_stage == stage && last_pc == pc)
+		return;
+
+	msg_ginfo("[%s] %u%% complete... ", flashprog_progress_stage_to_string(stage), pc);
+	last_stage = stage;
+	last_pc = pc;
+}
+
 /* Please note that level is the verbosity, not the importance of the message. */
 int flashprog_print_cb(enum flashprog_log_level level, const char *fmt, va_list ap)
 {
diff --git a/dediprog.c b/dediprog.c
index 4d6df5c..af9e157 100644
--- a/dediprog.c
+++ b/dediprog.c
@@ -207,6 +207,7 @@
 }
 
 struct dediprog_transfer_status {
+	struct flashctx *flash;
 	int error; /* OK if 0, ERROR else */
 	unsigned int queued_idx;
 	unsigned int finished_idx;
@@ -219,6 +220,7 @@
 		status->error = 1;
 		msg_perr("SPI bulk read failed!\n");
 	}
+	flashprog_progress_add(status->flash, transfer->actual_length);
 	++status->finished_idx;
 }
 
@@ -459,7 +461,7 @@
 	const unsigned int chunksize = 512;
 	const unsigned int count = len / chunksize;
 
-	struct dediprog_transfer_status status = { 0, 0, 0 };
+	struct dediprog_transfer_status status = { flash, 0, 0, 0 };
 	struct libusb_transfer *transfers[DEDIPROG_ASYNC_TRANSFERS] = { NULL, };
 	struct libusb_transfer *transfer;
 
@@ -671,6 +673,7 @@
 			msg_perr("SPI bulk write failed, expected %i, got %s!\n", 512, libusb_error_name(ret));
 			return 1;
 		}
+		flashprog_progress_add(flash, chunksize);
 	}
 
 	return 0;
diff --git a/edi.c b/edi.c
index a2219ac..5b4b4c9 100644
--- a/edi.c
+++ b/edi.c
@@ -387,6 +387,8 @@
 			msg_perr("%s: Timed out waiting for SPI not busy!\n", __func__);
 			return -1;
 		}
+
+		flashprog_progress_add(flash, flash->chip->page_size);
 	}
 
 	rc = edi_spi_disable(flash);
@@ -447,6 +449,7 @@
 
 		buf++;
 		address++;
+		flashprog_progress_add(flash, 1);
 	}
 
 	rc = edi_spi_disable(flash);
diff --git a/en29lv640b.c b/en29lv640b.c
index 5b01904..2daeef1 100644
--- a/en29lv640b.c
+++ b/en29lv640b.c
@@ -48,6 +48,7 @@
 #endif
 		dst += 2;
 		src += 2;
+		flashprog_progress_add(flash, 2);
 	}
 
 	/* FIXME: Ignore errors for now. */
diff --git a/flashprog.8.tmpl b/flashprog.8.tmpl
index a6076fe..ec6c044 100644
--- a/flashprog.8.tmpl
+++ b/flashprog.8.tmpl
@@ -51,7 +51,8 @@
              [(\fB\-l\fR <file>|\fB\-\-ifd\fR|\fB\-\-fmap\fR|\fB\-\-fmap-file\fR <file>)
               [\fB\-i\fR <include>]...]
              [\fB\-n\fR] [\fB\-N\fR] [\fB\-f\fR])]
-         [\fB\-V\fR[\fBV\fR[\fBV\fR]]] [\fB-o\fR <logfile>]
+         [\fB\-V\fR[\fBV\fR[\fBV\fR]]] [\fB-o\fR <logfile>] [\fB\-\-progress\fR]
+
 .SH DESCRIPTION
 .B flashprog
 is a utility for detecting, reading, writing, verifying and erasing flash
@@ -376,6 +377,9 @@
 way to gather logs from flashprog because they will be verbose even if the
 on-screen messages are not verbose and don't require output redirection.
 .TP
+.B "\-\-progress"
+Show progress percentage of operations on the standard output.
+.TP
 .B "\-R, \-\-version"
 Show version information and exit.
 .SH PROGRAMMER-SPECIFIC INFORMATION
diff --git a/flashprog.c b/flashprog.c
index c1a1b2f..e525f48 100644
--- a/flashprog.c
+++ b/flashprog.c
@@ -268,6 +268,63 @@
 	return extract_param(&programmer_param, param_name, ",");
 }
 
+static void flashprog_progress_report(struct flashprog_progress *const p)
+{
+	if (p->current > p->total) {
+		msg_gdbg2("Sanitizing progress report: %zu bytes off.", p->current - p->total);
+		p->current = p->total;
+	}
+
+	if (!p->callback)
+		return;
+
+	p->callback(p->stage, p->current, p->total, p->user_data);
+}
+
+static void flashprog_progress_start(struct flashprog_flashctx *const flashctx,
+				    const enum flashprog_progress_stage stage, const size_t total)
+{
+	flashctx->progress.stage	= stage;
+	flashctx->progress.current	= 0;
+	flashctx->progress.total	= total;
+	flashprog_progress_report(&flashctx->progress);
+}
+
+static void flashprog_progress_start_by_layout(struct flashprog_flashctx *const flashctx,
+					      const enum flashprog_progress_stage stage,
+					      const struct flashprog_layout *const layout)
+{
+	const struct romentry *entry = NULL;
+	size_t total = 0;
+
+	while ((entry = layout_next_included(layout, entry)))
+		total += entry->end - entry->start + 1;
+
+	flashprog_progress_start(flashctx, stage, total);
+}
+
+static void flashprog_progress_set(struct flashprog_flashctx *const flashctx, const size_t current)
+{
+	flashctx->progress.current = current;
+	flashprog_progress_report(&flashctx->progress);
+}
+
+/** @private */
+void flashprog_progress_add(struct flashprog_flashctx *const flashctx, const size_t progress)
+{
+	flashctx->progress.current += progress;
+	flashprog_progress_report(&flashctx->progress);
+}
+
+static void flashprog_progress_finish(struct flashprog_flashctx *const flashctx)
+{
+	if (flashctx->progress.current == flashctx->progress.total)
+		return;
+
+	flashctx->progress.current = flashctx->progress.total;
+	flashprog_progress_report(&flashctx->progress);
+}
+
 /* Returns the number of well-defined erasers for a chip. */
 static unsigned int count_usable_erasers(const struct flashctx *flash)
 {
@@ -318,6 +375,14 @@
 	return ret;
 }
 
+int flashprog_read_range(struct flashctx *flash, uint8_t *buf, unsigned int start, unsigned int len)
+{
+	flashprog_progress_start(flash, FLASHPROG_PROGRESS_READ, len);
+	const int ret = flash->chip->read(flash, buf, start, len);
+	flashprog_progress_finish(flash);
+	return ret;
+}
+
 /*
  * @cmpbuf	buffer to compare against, cmpbuf[0] is expected to match the
  *		flash content at location start
@@ -796,6 +861,8 @@
 	const struct flashprog_layout *const layout = get_layout(flashctx);
 	const struct romentry *entry = NULL;
 
+	flashprog_progress_start_by_layout(flashctx, FLASHPROG_PROGRESS_READ, layout);
+
 	while ((entry = layout_next_included(layout, entry))) {
 		const chipoff_t region_start	= entry->start;
 		const chipsize_t region_len	= entry->end - entry->start + 1;
@@ -803,6 +870,9 @@
 		if (flashctx->chip->read(flashctx, buffer + region_start, region_start, region_len))
 			return 1;
 	}
+
+	flashprog_progress_finish(flashctx);
+
 	return 0;
 }
 
@@ -1047,8 +1117,10 @@
 					  flash_offset + starthere, lenhere))
 			return 1;
 		starthere += lenhere;
-		if (skipped)
+		if (skipped) {
+			flashprog_progress_set(flashctx, starthere);
 			*skipped = false;
+		}
 	}
 	return 0;
 }
@@ -1125,17 +1197,27 @@
 		info->region_end   = entry->end;
 
 		if (do_erase) {
-			select_erase_functions(flashctx, erase_layouts, layout_count, info);
+			const size_t total = select_erase_functions(flashctx, erase_layouts, layout_count, info);
+
+			/* We verify every erased block manually. Technically that's
+			   reading, but accounting for it as part of the erase helps
+			   to provide a smooth, overall progress. Hence `total * 2`. */
+			flashprog_progress_start(flashctx, FLASHPROG_PROGRESS_ERASE, total * 2);
+
 			ret = walk_eraseblocks(flashctx, erase_layouts, layout_count, info, per_blockfn);
 			if (ret) {
 				msg_cerr("FAILED!\n");
 				goto free_ret;
 			}
+
+			flashprog_progress_finish(flashctx);
 		}
 
 		if (info->newcontents) {
 			bool skipped = true;
 			msg_cdbg("0x%06x-0x%06x:", info->region_start, info->region_end);
+			flashprog_progress_start(flashctx, FLASHPROG_PROGRESS_WRITE,
+						info->region_end - info->region_start + 1);
 			ret = write_range(flashctx, info->region_start,
 					  info->curcontents + info->region_start,
 					  info->newcontents + info->region_start,
@@ -1144,6 +1226,7 @@
 				msg_cerr("FAILED!\n");
 				goto free_ret;
 			}
+			flashprog_progress_finish(flashctx);
 			if (skipped) {
 				msg_cdbg("S\n");
 			} else {
@@ -1211,6 +1294,7 @@
 	msg_cdbg("E");
 	if (erasefn(flashctx, info->erase_start, erase_len))
 		goto _free_ret;
+	flashprog_progress_add(flashctx, erase_len);
 	if (check_erased_range(flashctx, info->erase_start, erase_len)) {
 		msg_cerr("ERASE FAILED!\n");
 		goto _free_ret;
@@ -1291,6 +1375,8 @@
 {
 	const struct romentry *entry = NULL;
 
+	flashprog_progress_start_by_layout(flashctx, FLASHPROG_PROGRESS_READ, layout);
+
 	while ((entry = layout_next_included(layout, entry))) {
 		const chipoff_t region_start	= entry->start;
 		const chipsize_t region_len	= entry->end - entry->start + 1;
@@ -1301,6 +1387,9 @@
 				  region_start, region_len))
 			return 3;
 	}
+
+	flashprog_progress_finish(flashctx);
+
 	return 0;
 }
 
@@ -1723,7 +1812,7 @@
 		 */
 		msg_cinfo("Reading old flash chip contents... ");
 		if (verify_all) {
-			if (flashctx->chip->read(flashctx, oldcontents, 0, flash_size)) {
+			if (flashprog_read_range(flashctx, oldcontents, 0, flash_size)) {
 				msg_cinfo("FAILED.\n");
 				goto _finalize_ret;
 			}
@@ -1743,7 +1832,7 @@
 		if (verify_all) {
 			msg_cerr("Checking if anything has changed.\n");
 			msg_cinfo("Reading current flash chip contents... ");
-			if (!flashctx->chip->read(flashctx, curcontents, 0, flash_size)) {
+			if (!flashprog_read_range(flashctx, curcontents, 0, flash_size)) {
 				msg_cinfo("done.\n");
 				if (!memcmp(oldcontents, curcontents, flash_size)) {
 					nonfatal_help_message();
diff --git a/fmap.c b/fmap.c
index 0eb5bf9..0e8dd95 100644
--- a/fmap.c
+++ b/fmap.c
@@ -167,7 +167,7 @@
 		goto _finalize_ret;
 	}
 
-	ret = flashctx->chip->read(flashctx, buf + rom_offset, rom_offset, len);
+	ret = flashprog_read_range(flashctx, buf + rom_offset, rom_offset, len);
 	if (ret) {
 		msg_pdbg("Cannot read ROM contents.\n");
 		goto _free_ret;
@@ -232,7 +232,7 @@
 
 			/* Read errors are considered non-fatal since we may
 			 * encounter locked regions and want to continue. */
-			if (flashctx->chip->read(flashctx, (uint8_t *)fmap, offset, sig_len)) {
+			if (flashprog_read_range(flashctx, (uint8_t *)fmap, offset, sig_len)) {
 				/*
 				 * Print in verbose mode only to avoid excessive
 				 * messages for benign errors. Subsequent error
@@ -245,7 +245,7 @@
 			if (memcmp(fmap, FMAP_SIGNATURE, sig_len) != 0)
 				continue;
 
-			if (flashctx->chip->read(flashctx, (uint8_t *)fmap + sig_len,
+			if (flashprog_read_range(flashctx, (uint8_t *)fmap + sig_len,
 						offset + sig_len, sizeof(*fmap) - sig_len)) {
 				msg_cerr("Cannot read %zu bytes at offset %06zx\n",
 						sizeof(*fmap) - sig_len, offset + sig_len);
@@ -277,7 +277,7 @@
 		goto _free_ret;
 	}
 
-	if (flashctx->chip->read(flashctx, (uint8_t *)fmap + sizeof(*fmap),
+	if (flashprog_read_range(flashctx, (uint8_t *)fmap + sizeof(*fmap),
 				offset + sizeof(*fmap), fmap_len - sizeof(*fmap))) {
 		msg_cerr("Cannot read %zu bytes at offset %06zx\n",
 				fmap_len - sizeof(*fmap), offset + sizeof(*fmap));
diff --git a/ichspi.c b/ichspi.c
index 74b4e3e..18f0d3f 100644
--- a/ichspi.c
+++ b/ichspi.c
@@ -1465,6 +1465,7 @@
 		if (ich_hwseq_wait_for_cycle_complete(block_len))
 			return 1;
 		ich_read_data(buf, block_len, ICH9_REG_FDATA0);
+		flashprog_progress_add(flash, block_len);
 		addr += block_len;
 		buf += block_len;
 		len -= block_len;
@@ -1505,6 +1506,7 @@
 
 		if (ich_hwseq_wait_for_cycle_complete(block_len))
 			return -1;
+		flashprog_progress_add(flash, block_len);
 		addr += block_len;
 		buf += block_len;
 		len -= block_len;
diff --git a/include/flash.h b/include/flash.h
index db47219..b411f34 100644
--- a/include/flash.h
+++ b/include/flash.h
@@ -339,6 +339,14 @@
 
 typedef int (*chip_restore_fn_cb_t)(struct flashctx *flash, uint8_t status);
 
+struct flashprog_progress {
+	flashprog_progress_callback *callback;
+	enum flashprog_progress_stage stage;
+	size_t current;
+	size_t total;
+	void *user_data;
+};
+
 struct flashprog_flashctx {
 	struct flashchip *chip;
 	/* FIXME: The memory mappings should be saved in a more structured way. */
@@ -375,6 +383,8 @@
 		chip_restore_fn_cb_t func;
 		uint8_t status;
 	} chip_restore_fn[MAX_CHIP_RESTORE_FUNCTIONS];
+
+	struct flashprog_progress progress;
 };
 
 /* Timing used in probe routines. ZERO is -2 to differentiate between an unset
@@ -438,6 +448,7 @@
 int erase_flash(struct flashctx *flash);
 struct registered_master;
 int probe_flash(struct registered_master *mst, int startchip, struct flashctx *fill_flash, int force);
+int flashprog_read_range(struct flashctx *, uint8_t *buf, unsigned int start, unsigned int len);
 int verify_range(struct flashctx *flash, const uint8_t *cmpbuf, unsigned int start, unsigned int len);
 void emergency_help_message(void);
 void list_programmers_linebreak(int startcol, int cols, int paren);
@@ -471,6 +482,7 @@
 int close_logfile(void);
 void start_logging(void);
 int flashprog_print_cb(enum flashprog_log_level level, const char *fmt, va_list ap);
+void flashprog_progress_cb(enum flashprog_progress_stage, size_t current, size_t total, void *user_data);
 /* Let gcc and clang check for correct printf-style format strings. */
 int print(enum flashprog_log_level level, const char *fmt, ...)
 #ifdef __MINGW32__
@@ -499,6 +511,7 @@
 #define msg_gspew(...)	print(FLASHPROG_MSG_SPEW, __VA_ARGS__)	/* general debug spew  */
 #define msg_pspew(...)	print(FLASHPROG_MSG_SPEW, __VA_ARGS__)	/* programmer debug spew  */
 #define msg_cspew(...)	print(FLASHPROG_MSG_SPEW, __VA_ARGS__)	/* chip debug spew  */
+void flashprog_progress_add(struct flashprog_flashctx *, size_t progress);
 
 /* spi.c */
 struct spi_command {
diff --git a/include/libflashprog.h b/include/libflashprog.h
index 9c39f5f..40050e3 100644
--- a/include/libflashprog.h
+++ b/include/libflashprog.h
@@ -51,6 +51,14 @@
 int flashprog_flash_erase(struct flashprog_flashctx *);
 void flashprog_flash_release(struct flashprog_flashctx *);
 
+enum flashprog_progress_stage {
+	FLASHPROG_PROGRESS_READ,
+	FLASHPROG_PROGRESS_WRITE,
+	FLASHPROG_PROGRESS_ERASE,
+};
+typedef void(flashprog_progress_callback)(enum flashprog_progress_stage, size_t current, size_t total, void *user_data);
+void flashprog_set_progress_callback(struct flashprog_flashctx *, flashprog_progress_callback *, void *user_data);
+
 /** @ingroup flashprog-flash */
 enum flashprog_flag {
 	FLASHPROG_FLAG_FORCE,
diff --git a/it87spi.c b/it87spi.c
index 25e826e..70b1a8f 100644
--- a/it87spi.c
+++ b/it87spi.c
@@ -435,6 +435,7 @@
 			int ret = it8716f_spi_page_program(flash, buf, start);
 			if (ret)
 				return ret;
+			flashprog_progress_add(flash, chip->page_size);
 			start += chip->page_size;
 			len -= chip->page_size;
 			buf += chip->page_size;
diff --git a/jedec.c b/jedec.c
index a8b734b..83eb44b 100644
--- a/jedec.c
+++ b/jedec.c
@@ -421,6 +421,7 @@
 		if (write_byte_program_jedec_common(flash, src, dst, mask))
 			failed = 1;
 		dst++, src++;
+		flashprog_progress_add(flash, 1);
 	}
 	if (failed)
 		msg_cerr(" writing sector at 0x%" PRIxPTR " failed!\n", olddst);
@@ -487,6 +488,7 @@
 	 * we're OK for now.
 	 */
 	unsigned int page_size = flash->chip->page_size;
+	unsigned int nwrites = (start + len - 1) / page_size;
 
 	/* Warning: This loop has a very unusual condition and body.
 	 * The loop needs to go through each page with at least one affected
@@ -497,7 +499,7 @@
 	 * (start + len - 1) / page_size. Since we want to include that last
 	 * page as well, the loop condition uses <=.
 	 */
-	for (i = start / page_size; i <= (start + len - 1) / page_size; i++) {
+	for (i = start / page_size; i <= nwrites; i++) {
 		/* Byte position of the first byte in the range in this page. */
 		/* starthere is an offset to the base address of the chip. */
 		starthere = max(start, i * page_size);
diff --git a/libflashprog.c b/libflashprog.c
index 6c2a9c9..2b389d1 100644
--- a/libflashprog.c
+++ b/libflashprog.c
@@ -276,6 +276,25 @@
 }
 
 /**
+ * @brief Set the progress callback function.
+ *
+ * Set a callback function which will be invoked whenever libflashprog wants
+ * to indicate the progress has changed. This allows frontends to do whatever
+ * they see fit with such values, e.g. update a progress bar in a GUI tool.
+ *
+ * @param flashctx Current flash context.
+ * @param progress_callback Pointer to the new progress callback function.
+ * @param user_data Pointer to any data the API user wants to have passed to the callback.
+ */
+void flashprog_set_progress_callback(struct flashprog_flashctx *const flashctx,
+				    flashprog_progress_callback *const progress_callback,
+				    void *const user_data)
+{
+	flashctx->progress.callback = progress_callback;
+	flashctx->progress.user_data = user_data;
+}
+
+/**
  * @brief Set a flag in the given flash context.
  *
  * @param flashctx Flash context to alter.
@@ -355,7 +374,7 @@
 		goto _free_ret;
 
 	msg_cinfo("Reading ich descriptor... ");
-	if (flashctx->chip->read(flashctx, desc, 0, 0x1000)) {
+	if (flashprog_read_range(flashctx, desc, 0, 0x1000)) {
 		msg_cerr("Read operation failed!\n");
 		msg_cinfo("FAILED.\n");
 		ret = 2;
diff --git a/libflashprog.map b/libflashprog.map
index 5294d96..9bf8ec0 100644
--- a/libflashprog.map
+++ b/libflashprog.map
@@ -21,6 +21,7 @@
     flashprog_programmer_init;
     flashprog_programmer_shutdown;
     flashprog_set_log_callback;
+    flashprog_set_progress_callback;
     flashprog_shutdown;
     flashprog_wp_cfg_new;
     flashprog_wp_cfg_release;
diff --git a/linux_mtd.c b/linux_mtd.c
index cbc9717..23041d4 100644
--- a/linux_mtd.c
+++ b/linux_mtd.c
@@ -215,6 +215,7 @@
 		}
 
 		i += step;
+		flashprog_progress_add(flash, step);
 	}
 
 	return 0;
@@ -257,6 +258,7 @@
 		}
 
 		i += step;
+		flashprog_progress_add(flash, step);
 	}
 
 	return 0;
diff --git a/nicintel_eeprom.c b/nicintel_eeprom.c
index ca301e0..2839e7b 100644
--- a/nicintel_eeprom.c
+++ b/nicintel_eeprom.c
@@ -181,10 +181,12 @@
 	while (len > 0) {
 		if (nicintel_ee_read_word(addr / 2, &data))
 			return -1;
+		flashprog_progress_add(flash, 1);
 		*buf++ = data & 0xff;
 		addr++;
 		len--;
 		if (len > 0) {
+			flashprog_progress_add(flash, 1);
 			*buf++ = (data >> 8) & 0xff;
 			addr++;
 			len--;
@@ -232,8 +234,10 @@
 			return -1;
 		}
 
-		if (buf)
+		if (buf) {
 			buf ++;
+			flashprog_progress_add(flash, 1);
+		}
 		addr ++;
 		len --;
 	}
@@ -261,8 +265,10 @@
 			return -1;
 		}
 
-		if (buf)
+		if (buf) {
 			buf += 2;
+			flashprog_progress_add(flash, min(len, 2));
+		}
 		if (len > 2)
 			len -= 2;
 		else
@@ -376,6 +382,8 @@
 			nicintel_ee_bitbang((buf) ? *buf++ : 0xff, NULL);
 			len--;
 			addr++;
+			if (buf)
+				flashprog_progress_add(flash, 1);
 			if (!(addr & EE_PAGE_MASK))
 				break;
 		}
diff --git a/spi25.c b/spi25.c
index d6c00c2..5f427ca 100644
--- a/spi25.c
+++ b/spi25.c
@@ -668,6 +668,7 @@
 		ret = spi_nbyte_read(flash, start, buf, to_read);
 		if (ret)
 			return ret;
+		flashprog_progress_add(flash, to_read);
 	}
 	return 0;
 }
@@ -710,6 +711,7 @@
 			rc = spi_nbyte_program(flash, starthere + j, buf + starthere - start + j, towrite);
 			if (rc)
 				return rc;
+			flashprog_progress_add(flash, towrite);
 		}
 	}
 
@@ -730,6 +732,7 @@
 	for (i = start; i < start + len; i++) {
 		if (spi_nbyte_program(flash, i, buf + i - start, 1))
 			return 1;
+		flashprog_progress_add(flash, 1);
 	}
 	return 0;
 }
@@ -772,6 +775,7 @@
 		goto bailout;
 
 	/* We already wrote 2 bytes in the multicommand step. */
+	flashprog_progress_add(flash, 2);
 	pos += 2;
 
 	/* Are there at least two more bytes to write? */
@@ -785,6 +789,7 @@
 		}
 		if (spi_poll_wip(flash, 10))
 			goto bailout;
+		flashprog_progress_add(flash, 2);
 	}
 
 	/* Use WRDI to exit AAI mode. This needs to be done before issuing any other non-AAI command. */
diff --git a/sst28sf040.c b/sst28sf040.c
index 3da25f1..fb3fea6 100644
--- a/sst28sf040.c
+++ b/sst28sf040.c
@@ -92,6 +92,7 @@
 
 		/* wait for Toggle bit ready */
 		toggle_ready_jedec(flash, bios);
+		flashprog_progress_add(flash, 1);
 	}
 
 	return 0;