diff --git a/doc/vfd_log.txt b/doc/vfd_log.txt new file mode 100644 index 0000000..b522fc2 --- /dev/null +++ b/doc/vfd_log.txt @@ -0,0 +1,140 @@ +1b 0b // boot +1b 21 01 // PowerOn +1b 32 02 // Set Language + +1b 0c // Clear +1b 52 // Stop Scroll +1b 40 00 00 00 00 9f 02 // Set Option +1b 41 00 // Set Speed +1b 50 1e 83 4a 81 5b 83 68 82 f0 82 a9 82 b4 82 b5 82 c4 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // カードをかざしてください +1b 51 // Start Scroll + +1b 0c +1b 52 +1b 40 00 00 00 00 9f 02 +1b 41 00 +1b 50 20 83 4a 81 5b 83 68 82 f0 97 a3 82 b3 82 c8 82 a2 82 c5 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // カードを離さないでください +1b 51 + +1b 0c +1b 52 +1b 40 00 00 00 00 9f 02 +1b 41 00 +1b 50 1c 82 b5 82 ce 82 e7 82 ad 82 a8 91 d2 82 bf 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // しばらくお待ちください +1b 51 + +1b 0c +1b 52 +1b 30 00 00 00 8f 80 94 f5 92 86 82 c5 82 b7 81 63 // 準備中です… + +1b 0c +1b 52 +1b 40 00 00 00 00 9f 02 +1b 41 00 +1b 50 1c 82 b5 82 ce 82 e7 82 ad 82 a8 91 d2 82 bf 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // しばらくお待ちください +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b9 82 f1 // 電子決済できません + +1b 0c +1b 52 +1b 40 00 00 02 00 9f 02 +1b 41 00 +1b 50 2e 6e 61 6e 61 63 6f 20 20 20 20 20 20 8c f0 92 ca 8c 6e 20 20 20 20 20 20 57 41 4f 4e 20 20 20 20 20 20 50 41 53 45 4c 49 20 20 20 20 20 20 // nanaco 交通系 WAON PASELI +1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b7 // 電子決済できます +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b9 82 f1 // 電子決済できません + +1b 0c +1b 52 +1b 40 00 00 02 00 9f 02 +1b 41 00 +1b 50 2e 6e 61 6e 61 63 6f 20 20 20 20 20 20 8c f0 92 ca 8c 6e 20 20 20 20 20 20 57 41 4f 4e 20 20 20 20 20 20 50 41 53 45 4c 49 20 20 20 20 20 20 // nanaco 交通系 WAON PASELI +1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b7 // 電子決済できます +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 40 00 00 00 00 9f 02 +1b 41 00 +1b 50 2c 50 41 53 45 4c 49 8e 78 95 a5 0d 0a 83 4a 81 5b 83 68 82 f0 83 5e 83 62 83 60 82 b5 82 c4 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // PASELI支払 カードをタッチしてください +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 40 00 00 02 00 9f 02 +1b 41 00 +1b 50 2e 6e 61 6e 61 63 6f 20 20 20 20 20 20 8c f0 92 ca 8c 6e 20 20 20 20 20 20 57 41 4f 4e 20 20 20 20 20 20 50 41 53 45 4c 49 20 20 20 20 20 20 // nanaco 交通系 WAON PASELI +1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b7 // 電子決済できます +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 40 00 00 00 00 9f 02 +1b 41 00 +1b 50 2c 50 41 53 45 4c 49 8e 78 95 a5 0d 0a 83 4a 81 5b 83 68 82 f0 83 5e 83 62 83 60 82 b5 82 c4 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // PASELI支払 カードをタッチしてください +1b 30 00 00 02 6e 61 6e 61 63 6f 8e 78 95 a5 20 5c 31 30 30 nanaco支払 \100 +1b 51 + +1b 0c +1b 52 +1b 40 00 00 00 00 9f 02 +1b 41 00 +1b 50 2c 50 41 53 45 4c 49 8e 78 95 a5 0d 0a 83 4a 81 5b 83 68 82 f0 83 5e 83 62 83 60 82 b5 82 c4 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // PASELI支払 カードをタッチしてください +1b 30 00 00 02 6e 61 6e 61 63 6f 8e 63 8d 82 20 5c 31 30 31 nanaco残高 \101 +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 40 00 00 02 00 9f 02 +1b 41 00 +1b 50 2e 6e 61 6e 61 63 6f 20 20 20 20 20 20 8c f0 92 ca 8c 6e 20 20 20 20 20 20 57 41 4f 4e 20 20 20 20 20 20 50 41 53 45 4c 49 20 20 20 20 20 20 // nanaco 交通系 WAON PASELI +1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b7 // 電子決済できます +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 40 00 00 00 00 9f 02 +1b 41 00 +1b 50 2c 50 41 53 45 4c 49 8e 78 95 a5 0d 0a 83 4a 81 5b 83 68 82 f0 83 5e 83 62 83 60 82 b5 82 c4 82 ad 82 be 82 b3 82 a2 20 20 20 20 20 20 // PASELI支払 カードをタッチしてください +1b 51 + +1b 0c +1b 52 + +1b 0c +1b 52 +1b 40 00 00 02 00 9f 02 +1b 41 00 +1b 50 2e 6e 61 6e 61 63 6f 20 20 20 20 20 20 8c f0 92 ca 8c 6e 20 20 20 20 20 20 57 41 4f 4e 20 20 20 20 20 20 50 41 53 45 4c 49 20 20 20 20 20 20 // nanaco 交通系 WAON PASELI +1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b7 // 電子決済できます +1b 51 + +1b 0c +1b 52 \ No newline at end of file diff --git a/tools/segatools-edit/vfd.c b/tools/segatools-edit/vfd.c new file mode 100644 index 0000000..ca2a7f0 --- /dev/null +++ b/tools/segatools-edit/vfd.c @@ -0,0 +1,195 @@ +/* This is some sort of LCD display found on various cabinets. It is driven + directly by amdaemon, and it has something to do with displaying the status + of electronic payments. + + Part number in schematics is "VFD GP1232A02A FUTABA". + + Little else about this board is known. Black-holing the RS232 comms that it + receives seems to be sufficient for the time being. */ + +#include + +#include +#include + +#include "board/vfd.h" + +#include "hook/iohook.h" + +#include "hooklib/uart.h" + +#include "util/dprintf.h" +#include "util/dump.h" + +static HRESULT vfd_handle_irp(struct irp *irp); + +static struct uart vfd_uart; +static uint8_t vfd_written[512]; +static uint8_t vfd_readable[512]; +static uint8_t vfd_enable, vfd_redirect; +static HANDLE com_device; +UINT codepage; + +HRESULT vfd_hook_init(unsigned int port_no) +{ + vfd_enable = GetPrivateProfileIntW(L"vfd", L"enable", 1, L".\\segatools.ini"); + vfd_redirect = GetPrivateProfileIntW(L"vfd", L"redirect", 0, L".\\segatools.ini"); + if (!vfd_enable) + { + return S_FALSE; + } + + char com_name[10]; + // sprintf(com_name, "\\\\.\\COM%d", port_no); + // com_device = CreateFile(com_name, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + // if (com_device != INVALID_HANDLE_VALUE) + // { + // CloseHandle(com_device); + // if (!vfd_redirect) + // { + // return S_FALSE; + // } + // } + + uart_init(&vfd_uart, port_no); + vfd_uart.written.bytes = vfd_written; + vfd_uart.written.nbytes = sizeof(vfd_written); + vfd_uart.readable.bytes = vfd_readable; + vfd_uart.readable.nbytes = sizeof(vfd_readable); + + if (vfd_redirect) // 将 vfd 串口数据重写到另一个 com,用于调试 + { + dprintf("VFD: redirect enable, target COM%d.\n", vfd_redirect); + sprintf(com_name, "\\\\.\\COM%d", vfd_redirect); + com_device = CreateFile(com_name, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (com_device != INVALID_HANDLE_VALUE) + { + dprintf("VFD: CreateFile OK.\n"); + DCB dcbSerialParams = {0}; + dcbSerialParams.DCBlength = sizeof(dcbSerialParams); + GetCommState(com_device, &dcbSerialParams); + dcbSerialParams.BaudRate = 115200; + dcbSerialParams.ByteSize = 8; + dcbSerialParams.StopBits = ONESTOPBIT; + dcbSerialParams.Parity = NOPARITY; + SetCommState(com_device, &dcbSerialParams); + + // COMMTIMEOUTS timeouts = {0}; + // timeouts.ReadIntervalTimeout = 1; + // timeouts.ReadTotalTimeoutConstant = 1; + // timeouts.ReadTotalTimeoutMultiplier = 1; + // timeouts.WriteTotalTimeoutConstant = 1; + // timeouts.WriteTotalTimeoutMultiplier = 1; + // SetCommTimeouts(com_device, &timeouts); + + EscapeCommFunction(com_device, SETDTR); + EscapeCommFunction(com_device, SETRTS); + } + else + { + dprintf("VFD: CreateFile error.\n"); + } + } + codepage = GetACP(); // 获取当前系统的 Code page,用于后续转码显示 + dprintf("VFD: hook enabled.\n"); + return iohook_push_handler(vfd_handle_irp); +} + +static HRESULT vfd_handle_irp(struct irp *irp) +{ + HRESULT hr; + + assert(irp != NULL); + + if (!uart_match_irp(&vfd_uart, irp)) + { + return iohook_invoke_next(irp); + } + + hr = uart_handle_irp(&vfd_uart, irp); + + if (FAILED(hr) || irp->op != IRP_OP_WRITE) + { + return hr; + } + + if (vfd_redirect && com_device != INVALID_HANDLE_VALUE) + { + DWORD bytesWritten; + if (WriteFile(com_device, vfd_uart.written.bytes, vfd_uart.written.pos, &bytesWritten, NULL)) + { + dprintf("VFD: WriteFile %lu.\n", bytesWritten); + } + } + else + { + uint8_t cmd = 0; + uint8_t str_1[512]; // 两行数据的缓冲区 + uint8_t str_2[512]; + uint8_t str_1_len = 0; + uint8_t str_2_len = 0; + for (size_t i = 0; i < vfd_uart.written.pos; i++) + { + if (vfd_uart.written.bytes[i] == 0x1B) + { + i++; + cmd = vfd_uart.written.bytes[i]; + if (cmd == 0x30) + { + i += 3; + } + else if (cmd == 0x50) + { + i++; + } + continue; + } + if (cmd == 0x30) + { + str_1[str_1_len++] = vfd_uart.written.bytes[i]; + } + else if (cmd == 0x50) + { + str_2[str_2_len++] = vfd_uart.written.bytes[i]; + } + } + + if (str_1_len) + { + str_1[str_1_len++] = '\0'; + if (codepage != 932) + { // 如果非日文系统,则转码后输出 https://en.wikipedia.org/wiki/Code_page_932_(Microsoft_Windows) + WCHAR buffer[512]; + MultiByteToWideChar(932, 0, (LPCSTR)str_1, str_1_len, buffer, str_1_len); + char str_recode[str_1_len * 3]; + WideCharToMultiByte(codepage, 0, buffer, str_1_len, str_recode, str_1_len * 3, NULL, NULL); + dprintf("VFD: %s\n", str_recode); + } + else + { + dprintf("VFD: %s\n", str_1); + } + } + + if (str_2_len) + { + str_2[str_2_len++] = '\0'; + if (codepage != 932) + { + WCHAR buffer[512]; + MultiByteToWideChar(932, 0, (LPCSTR)str_2, str_2_len, buffer, str_2_len); + char str_recode[str_2_len * 3]; + WideCharToMultiByte(codepage, 0, buffer, str_2_len, str_recode, str_2_len * 3, NULL, NULL); + dprintf("VFD: %s\n", str_recode); + } + else + { + dprintf("VFD: %s\n", str_2); + } + } + } + + vfd_uart.written.pos = 0; + + return hr; +} diff --git a/tools/vfd_test/vfd_test.ino b/tools/vfd_test/vfd_test.ino new file mode 100644 index 0000000..1d49f33 --- /dev/null +++ b/tools/vfd_test/vfd_test.ino @@ -0,0 +1,256 @@ +// 本程序已于 2023/07/09 停止开发 +// 展示信息的小屏幕,波特率 115200,只会出现在支持 電子決済 的框体,设备仅接收数据,不需要回复 +// 在启用 emoney 时,显示支持的卡片种类和付款情况 +// com 序号在 config_common.json > emoney > display_port 定义 +// https://github.com/djhackersdev/segatools/blob/master/board/vfd.c +// vfd 串口数据没有固定格式,没有长度位 +// 测试程序显示文字用的是 u8g2 库,默认用的是 utf8 编码,无法直接使用 vfd 串口的 shift-jis 编码数据 + +// 字库相关的信息: +// https://github.com/olikraus/u8g2/discussions/1508 +// https://github.com/larryli/u8g2_wqy +// https://github.com/breakstring/u8g2_fontmaker +// https://github.com/createskyblue/Easy-u8g2-font-generate-tools/blob/master/main.py + +#define SerialDevice Serial + +#include +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0); + +static uint8_t state = 0; +static char textBuffer[]; +static uint8_t text_len = 0; +// https://r12a.github.io/app-encodings/ +// https://www.mgo-tec.com/blog-entry-utf8-sjis-wroom-arduino-lib.html +// https://github.com/mgo-tec/UTF8SJIS_for_ESP8266/blob/master/src/UTF8toSJIS.cpp +// https://github.com/mgo-tec/ESP32_SD_UTF8toSJIS + +void setup() { + SerialDevice.begin(115200); + SerialDevice.setTimeout(0); + Wire.setClock(800000); + u8g2.begin(); + u8g2.setFont(u8g2_font_6x12_mf); + u8g2.clearBuffer(); + u8g2.sendBuffer(); +} + +void loop() { + while (SerialDevice.available()) { + uint8_t r = SerialDevice.read(); + if (r == 0x1B) { + r = SerialDevice.read(); + state = 0; + continue; + } + if (!state) { + state = r; + continue; + } + switch (state) { + case 0x0B: // 仅开机出现过 + // 1b 0b + break; + case 0x0C: + // 1b 0c + text_len = 0; + break; + case 0x21: // 仅开机出现过,在 0b 后面 + // 1b 21 01 + break; + case 0x30: + textBuffer[text_len++] = r; + continue; + // 1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b7 // 電子決済できます + // 1b 30 00 00 00 93 64 8e 71 8c 88 8d cf 82 c5 82 ab 82 dc 82 b9 82 f1 // 電子決済できません + // 1b 30 00 00 02 6e 61 6e 61 63 6f 8e 78 95 a5 20 5c 35 30 30 // anaco支払 \500 + case 0x32: // 仅开机出现过,在 21 后面 + // 1b 32 02 + break; + case 0x40: + // 1b 40 00 00 (00|02) 00 9f 02 + uint8_t prm1 = SerialDevice.read(); + uint8_t prm2 = SerialDevice.read(); + uint8_t Line = SerialDevice.read(); + uint8_t BoxSize_x = SerialDevice.read(); + uint8_t BoxSize_y = SerialDevice.read(); + // 缺少一位 + break; + case 0x41: // 在 40 后面,speed 只发现是 00,用途未知 + // 1b 41 00 + uint8_t speed = SerialDevice.read(); + break; + case 0x50: // 长度后面是字符内容 + uint8_t len = SerialDevice.read(); + textBuffer[text_len++] = r; + continue; + case 0x51: // 仅在有输出时,作为最后一条指令发送 + // 1b 51 + break; + case 0x52: // 跟在 0c 后面 + // 1b 52 + break; + } + } +} + +/* +// oled 显示测试 +#include +#include +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0); +// https://github.com/olikraus/u8g2/wiki/fntlistallplain +// https://github.com/olikraus/u8g2/wiki/u8g2reference + +char text1[] = { + 0xe9, 0x9b, 0xbb, + 0xe5, 0xad, 0x90, + 0xe6, 0xb1, 0xba, + 0xe6, 0xb8, 0x88, + 0xe3, 0x81, 0xa7, + 0xe3, 0x81, 0x8d, + 0xe3, 0x81, 0xbe, + 0xe3, 0x81, 0x99, + 0 +}; + +char text2[] = { + 0x6e, 0x61, 0x6e, 0x61, 0x63, 0x6f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xe4, 0xba, 0xa4, 0xe9, 0x80, 0x9a, 0xe7, 0xb3, 0xbb, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x57, 0x41, 0x4f, 0x4e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x50, 0x41, 0x53, 0x45, 0x4c, 0x49, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0 +}; + + +u8g2_uint_t titleW, msgW, DisplayW; // 字符、屏幕宽度 +u8g2_uint_t title_offset, msg_offset; // 字符的偏移位置 + +bool is_display = false; +uint8_t state, title_len, msg_len; +char title_buffer[256]; +char msg_buffer[256]; + +void setup() { + SerialDevice.begin(115200); + SerialDevice.setTimeout(0); + Wire.setClock(800000); + u8g2.begin(); + u8g2.setFont(u8g2_font_unifont_t_japanese3); + u8g2.enableUTF8Print(); + Serial.begin(115200); + + + DisplayW = u8g2.getDisplayWidth(); + // Serial.println(titleW); + // Serial.println(msgW); + // Serial.println(DisplayW); +} + + +void displayTask() { + if (!is_display) { + return; + } + u8g2.clearBuffer(); + if (titleW) { + // SerialDevice.println("titleW"); + if (titleW < DisplayW) { + u8g2.drawUTF8(0, 14, title_buffer); + // u8g2.setCursor(0, 14); + // u8g2.print(text1); + } else { + u8g2_uint_t x = title_offset; + do { + u8g2.drawUTF8(x, 14, title_buffer); + x += titleW; + } while (x < DisplayW); + title_offset -= 1; + if ((u8g2_uint_t)title_offset < (u8g2_uint_t)-titleW) { + title_offset = 0; + } + } + } + + if (msgW) { + // SerialDevice.println("msgW"); + if (msgW < DisplayW) { + u8g2.drawUTF8(0, 30, msg_buffer); + } else { + u8g2_uint_t x = msg_offset; + do { + u8g2.drawUTF8(x, 30, msg_buffer); + x += msgW; + } while (x < DisplayW); + msg_offset -= 1; + if ((u8g2_uint_t)msg_offset < (u8g2_uint_t)-msgW) { + msg_offset = 0; + } + } + } + u8g2.sendBuffer(); + // delay(500); +} + + + +void loop() { + msg_len = 0; + // memset(title_buffer, 0, 256); + // memset(msg_buffer, 0, 256); + // bool pre_display = false; + while (SerialDevice.available()) { + msg_buffer[msg_len++] = SerialDevice.read(); + // uint8_t r = SerialDevice.read(); + // if (r == 0x1B) { + // state = 0; + // continue; + // } + // if (!state) { + // state = SerialDevice.read(); + // } + // switch (state) { + // case 0x0C: + // title_len = 0; + // msg_len = 0; + // titleW = 0; + // msgW = 0; + // is_display = false; + // // memset(title_buffer, 0, 256); + // // memset(msg_buffer, 0, 256); + // u8g2.clearBuffer(); + // u8g2.sendBuffer(); + // SerialDevice.println("clear"); + // break; + // case 0x30: + // title_buffer[title_len++] = r; + // pre_display = true; + // continue; + // case 0x50: + // msg_buffer[msg_len++] = r; + // pre_display = true; + // continue; + // default: + // break; + // } + } + // if (pre_display) { + // if (title_len) { + // title_buffer[title_len++] = 0; + // titleW = u8g2.getUTF8Width(title_buffer); + // title_offset = 0; + // is_display = true; + // SerialDevice.println("pre_display title_len"); + // } + if (msg_len) { + msg_buffer[msg_len++] = 0; + msgW = u8g2.getUTF8Width(msg_buffer); + SerialDevice.println(msgW); + msg_offset = 0; + is_display = true; + SerialDevice.println("pre_display msg_len"); + } + // pre_display = false; + // } + + + displayTask(); +} + +*/ \ No newline at end of file