changeset 2027:0f54a898db03

Implement Heartbeat Personal Trainer peripheral and add ROM DB entry for Outback Joey
author Michael Pavone <pavone@retrodev.com>
date Mon, 15 Feb 2021 11:24:06 -0800
parents aa338eb0eded
children e597572f45ce
files blastem.c io.c io.h rom.db romdb.c romdb.h
diffstat 6 files changed, 354 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/blastem.c	Wed Feb 10 20:13:27 2021 -0800
+++ b/blastem.c	Mon Feb 15 11:24:06 2021 -0800
@@ -281,12 +281,23 @@
 	return save_dir;
 }
 
+const char *get_save_fname(uint8_t save_type)
+{
+	switch(save_type)
+	{
+	case SAVE_I2C: return "save.eeprom";
+	case SAVE_NOR: return "save.nor";
+	case SAVE_HBPT: return "save.hbpt";
+	default: return "save.sram";
+	}
+}
+
 void setup_saves(system_media *media, system_header *context)
 {
 	static uint8_t persist_save_registered;
 	rom_info *info = &context->info;
 	char *save_dir = get_save_dir(info->is_save_lock_on ? media->chain : media);
-	char const *parts[] = {save_dir, PATH_SEP, info->save_type == SAVE_I2C ? "save.eeprom" : info->save_type == SAVE_NOR ? "save.nor" : "save.sram"};
+	char const *parts[] = {save_dir, PATH_SEP, get_save_fname(info->save_type)};
 	free(save_filename);
 	save_filename = alloc_concat_m(3, parts);
 	if (info->is_save_lock_on) {
--- a/io.c	Wed Feb 10 20:13:27 2021 -0800
+++ b/io.c	Mon Feb 15 11:24:06 2021 -0800
@@ -40,7 +40,8 @@
 	"EA 4-way Play cable B",
 	"Sega Parallel Transfer Board",
 	"Generic Device",
-	"Generic Serial"
+	"Generic Serial",
+	"Heartbeat Personal Trainer"
 };
 
 #define GAMEPAD_TH0 0
@@ -59,6 +60,13 @@
 	IO_READ
 };
 
+enum {
+	HBPT_NEED_INIT,
+	HBPT_IDLE,
+	HBPT_CMD_PAYLOAD,
+	HBPT_REPLY
+};
+
 typedef struct {
 	uint8_t states[2], value;
 } gp_button_def;
@@ -87,6 +95,9 @@
 		if (port->device_type < IO_MOUSE && port->device.pad.gamepad_num == gamepad_num) {
 			return port;
 		}
+		if (port->device_type == IO_HEARTBEAT_TRAINER && port->device.heartbeat_trainer.device_num == gamepad_num) {
+			return port;
+		} 
 	}
 	return NULL;
 }
@@ -248,6 +259,10 @@
 			port->device_type = IO_GAMEPAD6;
 		}
 		port->device.pad.gamepad_num = device_type[gamepad_len+2] - '0';
+	} else if(startswith(device_type, "heartbeat_trainer.")) {
+		port->device_type = IO_HEARTBEAT_TRAINER;
+		port->device.heartbeat_trainer.nv_memory = NULL;
+		port->device.heartbeat_trainer.device_num = device_type[strlen("heartbeat_trainer.")] - '0';
 	} else if(startswith(device_type, "mouse")) {
 		if (port->device_type != IO_MOUSE) {
 			port->device_type = IO_MOUSE;
@@ -411,6 +426,30 @@
 #endif
 		if (ports[i].device_type == IO_GAMEPAD3 || ports[i].device_type == IO_GAMEPAD6 || ports[i].device_type == IO_GAMEPAD2) {
 			debug_message("IO port %s connected to gamepad #%d with type '%s'\n", io_name(i), ports[i].device.pad.gamepad_num, device_type_names[ports[i].device_type]);
+		} else if (ports[i].device_type == IO_HEARTBEAT_TRAINER) {
+			debug_message("IO port %s connected to Heartbeat Personal Trainer #%d\n", io_name(i), ports[i].device.heartbeat_trainer.device_num);
+			if (rom->save_type == SAVE_HBPT) {
+				ports[i].device.heartbeat_trainer.nv_memory = rom->save_buffer;
+				uint32_t page_size = 16;
+				for (; page_size < 128; page_size *= 2)
+				{
+					if (rom->save_size / page_size < 256) {
+						break;
+					}
+				}
+				ports[i].device.heartbeat_trainer.nv_page_size = page_size;
+				uint32_t num_pages = rom->save_size / page_size;
+				ports[i].device.heartbeat_trainer.nv_pages = num_pages < 256 ? num_pages : 255;
+			} else {
+				ports[i].device.heartbeat_trainer.nv_page_size = 16;
+				ports[i].device.heartbeat_trainer.nv_pages = 32;
+				size_t bufsize = 
+					ports[i].device.heartbeat_trainer.nv_page_size * ports[i].device.heartbeat_trainer.nv_pages
+					+ 5 + 8;
+				ports[i].device.heartbeat_trainer.nv_memory = malloc(bufsize);
+				memset(ports[i].device.heartbeat_trainer.nv_memory, 0xFF, bufsize);
+			}
+			ports[i].device.heartbeat_trainer.state = HBPT_NEED_INIT;
 		} else {
 			debug_message("IO port %s connected to device '%s'\n", io_name(i), device_type_names[ports[i].device_type]);
 		}
@@ -641,6 +680,257 @@
 }
 #endif
 
+enum {
+	HBPT_UNKNOWN1 = 1,
+	HBPT_POLL,
+	HBPT_READ_PAGE = 5,
+	HBPT_WRITE_PAGE,
+	HBPT_READ_RTC,
+	HBPT_SET_RTC,
+	HBPT_GET_STATUS,
+	HBPT_ERASE_NVMEM,
+	HBPT_NVMEM_PARAMS,
+	HBPT_INIT
+};
+
+static void start_reply(io_port *port, uint8_t bytes, const uint8_t *src)
+{
+	port->device.heartbeat_trainer.remaining_bytes = bytes;
+	port->device.heartbeat_trainer.state = HBPT_REPLY;
+	port->device.heartbeat_trainer.cur_buffer = (uint8_t *)src;
+}
+
+static void simple_reply(io_port *port, uint8_t value)
+{
+	port->device.heartbeat_trainer.param = value;
+	start_reply(port, 1, &port->device.heartbeat_trainer.param);
+}
+
+static void expect_payload(io_port *port, uint8_t bytes, uint8_t *dst)
+{
+	port->device.heartbeat_trainer.remaining_bytes = bytes;
+	port->device.heartbeat_trainer.state = HBPT_CMD_PAYLOAD;
+	port->device.heartbeat_trainer.cur_buffer = dst;
+}
+
+void hbpt_check_init(io_port *port)
+{
+	if (port->device.heartbeat_trainer.state == HBPT_NEED_INIT) {
+		port->device.heartbeat_trainer.rtc_base_timestamp = 0;
+		for (int i = 0; i < 8; i ++)
+		{
+			port->device.heartbeat_trainer.rtc_base_timestamp <<= 8;
+			port->device.heartbeat_trainer.rtc_base_timestamp |= port->device.heartbeat_trainer.nv_memory[i];
+		}
+		memcpy(port->device.heartbeat_trainer.rtc_base, port->device.heartbeat_trainer.nv_memory + 8, 5);
+		if (port->device.heartbeat_trainer.rtc_base_timestamp == UINT64_MAX) {
+			//uninitialized save, set the appropriate status bit
+			port->device.heartbeat_trainer.status |= 1;
+		}
+		port->device.heartbeat_trainer.bpm = 60;
+		port->device.heartbeat_trainer.state = HBPT_IDLE;
+	}
+}
+
+void hbpt_check_send_reply(io_port *port)
+{
+	if (port->device.heartbeat_trainer.state == HBPT_REPLY && !port->receive_end) {
+		port->serial_receiving = *(port->device.heartbeat_trainer.cur_buffer++);
+		port->receive_end = port->serial_cycle + 10 * port->serial_divider;
+		if (!--port->device.heartbeat_trainer.remaining_bytes) {
+			port->device.heartbeat_trainer.state = HBPT_IDLE;
+		}
+	}
+}
+
+uint8_t is_leap_year(uint16_t year)
+{
+	if (year & 3) {
+		return 0;
+	}
+	if (year % 100) {
+		return 1;
+	}
+	if (year % 400) {
+		return 0;
+	}
+	return 1;
+}
+
+uint8_t days_in_month(uint8_t month, uint16_t year)
+{
+	static uint8_t days_per_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+	if (month == 2 && is_leap_year(year)) {
+		return 29;
+	}
+	if (month > 12 || !month) {
+		return 30;
+	}
+	return days_per_month[month-1];
+}
+
+void hbpt_write_byte(io_port *port)
+{
+	hbpt_check_init(port);
+	uint8_t reply;
+	switch (port->device.heartbeat_trainer.state)
+	{
+	case HBPT_IDLE:
+		port->device.heartbeat_trainer.cmd = port->serial_transmitting;
+		switch (port->device.heartbeat_trainer.cmd)
+		{
+		case HBPT_UNKNOWN1:
+			start_reply(port, 11, NULL);
+			break;
+		case HBPT_POLL:
+			start_reply(port, 3, &port->device.heartbeat_trainer.bpm);
+			if (port->serial_cycle - port->last_poll_cycle > MIN_POLL_INTERVAL) {
+				process_events();
+				port->last_poll_cycle = port->serial_cycle;
+			}
+			port->device.heartbeat_trainer.buttons = (port->input[GAMEPAD_TH0] << 2 & 0xC0) | (port->input[GAMEPAD_TH1] & 0x1F);
+			if (port->device.heartbeat_trainer.cadence && port->input[GAMEPAD_TH1] & 0x20) {
+				port->device.heartbeat_trainer.cadence--;
+				printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence);
+			} else if (port->device.heartbeat_trainer.cadence < 255 && port->input[GAMEPAD_EXTRA] & 1) {
+				port->device.heartbeat_trainer.cadence++;
+				printf("Cadence: %d\n", port->device.heartbeat_trainer.cadence);
+			}
+			if (port->device.heartbeat_trainer.bpm && port->input[GAMEPAD_EXTRA] & 4) {
+				port->device.heartbeat_trainer.bpm--;
+				printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm);
+			} else if (port->device.heartbeat_trainer.bpm < 255 && port->input[GAMEPAD_EXTRA] & 2) {
+				port->device.heartbeat_trainer.bpm++;
+				printf("Heart Rate: %d\n", port->device.heartbeat_trainer.bpm);
+			}
+			
+			break;
+		case HBPT_READ_PAGE:
+		case HBPT_WRITE_PAGE:
+			//strictly speaking for the write case, we want 1 + page size here
+			//but the rest of the payload goes to a different destination
+			expect_payload(port, 1, &port->device.heartbeat_trainer.param);
+			break;
+		case HBPT_READ_RTC: {
+			uint8_t *rtc = port->device.heartbeat_trainer.rtc_base;
+			start_reply(port, 5, rtc);
+			uint64_t now = time(NULL);
+			uint64_t delta = (now - port->device.heartbeat_trainer.rtc_base_timestamp + 30) / 60;
+			rtc[4] += delta % 60;
+			if (rtc[4] > 59) {
+				rtc[4] -= 60;
+				rtc[3]++;
+			}
+			delta /= 60;
+			if (delta) {
+				rtc[3] += delta % 24;
+				delta /= 24;
+				if (rtc[3] > 23) {
+					rtc[3] -= 24;
+					delta++;
+				}
+				if (delta) {
+					uint16_t year = rtc[0] < 81 ? 2000 + rtc[0] : 1900 + rtc[0];
+					uint8_t days_cur_month = days_in_month(rtc[1], year);
+					while (delta + rtc[2] > days_cur_month) {
+						delta -= days_cur_month + 1 - rtc[2];
+						rtc[2] = 1;
+						if (++rtc[1] == 13) {
+							rtc[1] = 1;
+							year++;
+						}
+						days_cur_month = days_in_month(rtc[1], year);
+					}
+					rtc[1] += delta;
+					rtc[0] = year % 100;
+				}
+			}
+			printf("RTC %02d-%02d-%02d %02d:%02d\n", rtc[0], rtc[1], rtc[2], rtc[3], rtc[4]);
+			port->device.heartbeat_trainer.rtc_base_timestamp = now;
+			break;
+		}
+		case HBPT_SET_RTC:
+			port->device.heartbeat_trainer.rtc_base_timestamp = time(NULL);
+			expect_payload(port, 5, port->device.heartbeat_trainer.rtc_base);
+			break;
+		case HBPT_GET_STATUS:
+			simple_reply(port, port->device.heartbeat_trainer.status);
+			break;
+		case HBPT_ERASE_NVMEM:
+			expect_payload(port, 1, &port->device.heartbeat_trainer.param);
+			break;
+		case HBPT_NVMEM_PARAMS:
+			start_reply(port, 2, &port->device.heartbeat_trainer.nv_page_size);
+			break;
+		case HBPT_INIT:
+			expect_payload(port, 19, NULL);
+			break;
+		default:
+			// it's unclear what these commands do as they are unused by Outback Joey
+			// just return 0 to indicate failure
+			simple_reply(port, 0);
+		}
+		break;
+	case HBPT_CMD_PAYLOAD:
+		if (port->device.heartbeat_trainer.cur_buffer) {
+			*(port->device.heartbeat_trainer.cur_buffer++) = port->serial_transmitting;
+		}
+		if (!--port->device.heartbeat_trainer.remaining_bytes) {
+			switch (port->device.heartbeat_trainer.cmd)
+			{
+			case HBPT_READ_PAGE:
+			case HBPT_WRITE_PAGE:
+				if (
+					port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE 
+					&& port->device.heartbeat_trainer.cur_buffer != &port->device.heartbeat_trainer.param + 1) {
+					simple_reply(port, 1);
+					break;
+				}
+				port->device.heartbeat_trainer.remaining_bytes = port->device.heartbeat_trainer.nv_page_size;
+				port->device.heartbeat_trainer.cur_buffer =
+					port->device.heartbeat_trainer.param < port->device.heartbeat_trainer.nv_pages
+					? port->device.heartbeat_trainer.nv_memory + 5 + 8
+						+ port->device.heartbeat_trainer.param * port->device.heartbeat_trainer.nv_page_size
+					: NULL;
+				if (port->device.heartbeat_trainer.cmd == HBPT_WRITE_PAGE) {
+					return;
+				}
+				port->device.heartbeat_trainer.state = HBPT_REPLY;
+				break;
+			case HBPT_SET_RTC:
+				//save RTC base values back to nv memory area so it's saved to disk on exit
+				for (int i = 0; i < 8; i++)
+				{
+					port->device.heartbeat_trainer.nv_memory[i] = port->device.heartbeat_trainer.rtc_base_timestamp >> (56 - i*8);
+				}
+				memcpy(port->device.heartbeat_trainer.nv_memory + 8, port->device.heartbeat_trainer.rtc_base, 5);
+				simple_reply(port, 1);
+				break;
+			case HBPT_ERASE_NVMEM:
+				memset(
+					port->device.heartbeat_trainer.nv_memory + 5 + 8, 
+					port->device.heartbeat_trainer.param, 
+					port->device.heartbeat_trainer.nv_pages * port->device.heartbeat_trainer.nv_page_size
+				);
+				simple_reply(port, 1);
+				break;
+			case HBPT_INIT: {
+				static const char reply[] = "(C) HEARTBEAT CORP";
+				start_reply(port, strlen(reply), reply);
+				break;
+			}
+			}
+		}
+	}
+	hbpt_check_send_reply(port);
+}
+
+void hbpt_read_byte(io_port *port)
+{
+	hbpt_check_init(port);
+	hbpt_check_send_reply(port);
+}
+
 const int mouse_delays[] = {112*7, 120*7, 96*7, 132*7, 104*7, 96*7, 112*7, 96*7};
 
 enum {
@@ -667,6 +957,9 @@
 		if (port->serial_ctrl & SCTRL_BIT_TX_ENABLE) {
 			switch (port->device_type)
 			{
+			case IO_HEARTBEAT_TRAINER:
+				hbpt_write_byte(port);
+				break;
 #ifndef _WIN32
 			case IO_GENERIC_SERIAL:
 				write_serial_byte(port);
@@ -693,6 +986,9 @@
 		if (!port->receive_end) {
 			switch(port->device_type)
 			{
+			case IO_HEARTBEAT_TRAINER:
+				hbpt_read_byte(port);
+				break;
 #ifndef _WIN32
 			case IO_GENERIC_SERIAL:
 				read_serial_byte(port);
--- a/io.h	Wed Feb 10 20:13:27 2021 -0800
+++ b/io.h	Mon Feb 15 11:24:06 2021 -0800
@@ -25,7 +25,8 @@
 	IO_EA_MULTI_B,
 	IO_SEGA_PARALLEL,
 	IO_GENERIC,
-	IO_GENERIC_SERIAL
+	IO_GENERIC_SERIAL,
+	IO_HEARTBEAT_TRAINER
 };
 
 typedef struct {
@@ -58,6 +59,23 @@
 			uint8_t  mode;
 			uint8_t  cmd;
 		} keyboard;
+		struct {
+			uint8_t  *nv_memory;
+			uint8_t  *cur_buffer;
+			uint64_t rtc_base_timestamp;
+			uint8_t  rtc_base[5];
+			uint8_t  bpm;
+			uint8_t  cadence;
+			uint8_t  buttons;
+			uint8_t  nv_page_size;
+			uint8_t  nv_pages;
+			uint8_t  param;
+			uint8_t  state;
+			uint8_t  status;
+			uint8_t  device_num;
+			uint8_t  cmd;
+			uint8_t  remaining_bytes;
+		} heartbeat_trainer;
 	} device;
 	uint8_t  output;
 	uint8_t  control;
--- a/rom.db	Wed Feb 10 20:13:27 2021 -0800
+++ b/rom.db	Mon Feb 15 11:24:06 2021 -0800
@@ -1425,3 +1425,13 @@
 		
 	}
 }
+T-122026 {
+	name Outback Joey
+	HeartbeatTrainer {
+		size 512
+	}
+	device_overrides {
+		1 heartbeat_trainer.1
+		2 gamepad3.2
+	}
+}
--- a/romdb.c	Wed Feb 10 20:13:27 2021 -0800
+++ b/romdb.c	Mon Feb 15 11:24:06 2021 -0800
@@ -32,6 +32,8 @@
 		return "EEPROM";
 	} else if(save_type == SAVE_NOR) {
 		return "NOR Flash";
+	} else if(save_type == SAVE_HBPT) {
+		return "Heartbeat Personal Trainer";
 	}
 	return "SRAM";
 }
@@ -986,6 +988,19 @@
 		info.port1_override = tern_find_ptr(device_overrides, "1");
 		info.port2_override = tern_find_ptr(device_overrides, "2");
 		info.ext_override = tern_find_ptr(device_overrides, "ext");
+		if (
+			info.save_type == SAVE_NONE
+			&& (
+				(info.port1_override && startswith(info.port1_override, "heartbeat_trainer."))
+				|| (info.port2_override && startswith(info.port2_override, "heartbeat_trainer."))
+				|| (info.ext_override && startswith(info.ext_override, "heartbeat_trainer."))
+			)
+		) {
+			info.save_type = SAVE_HBPT;
+			info.save_size = atoi(tern_find_path_default(entry, "HeartbeatTrainer\0size\0", (tern_val){.ptrval="512"}, TVAL_PTR).ptrval);
+			info.save_buffer = calloc(info.save_size + 5 + 8, 1);
+			memset(info.save_buffer, 0xFF, info.save_size);
+		}
 	} else {
 		info.port1_override = info.port2_override = info.ext_override = NULL;
 	}
--- a/romdb.h	Wed Feb 10 20:13:27 2021 -0800
+++ b/romdb.h	Mon Feb 15 11:24:06 2021 -0800
@@ -11,6 +11,7 @@
 #define RAM_FLAG_MASK RAM_FLAG_ODD
 #define SAVE_I2C      0x01
 #define SAVE_NOR      0x02
+#define SAVE_HBPT     0x03
 #define SAVE_NONE     0xFF
 
 #include "tern.h"