# HG changeset patch # User Michael Pavone # Date 1613417046 28800 # Node ID 0f54a898db030fb666c84411198b4aa8861926e8 # Parent aa338eb0ededfcf10210be7b746a7c2498027f41 Implement Heartbeat Personal Trainer peripheral and add ROM DB entry for Outback Joey diff -r aa338eb0eded -r 0f54a898db03 blastem.c --- 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) { diff -r aa338eb0eded -r 0f54a898db03 io.c --- 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); diff -r aa338eb0eded -r 0f54a898db03 io.h --- 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; diff -r aa338eb0eded -r 0f54a898db03 rom.db --- 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 + } +} diff -r aa338eb0eded -r 0f54a898db03 romdb.c --- 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; } diff -r aa338eb0eded -r 0f54a898db03 romdb.h --- 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"