From a6c4203a71413c218dfd53076b58db1f430645f7 Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Fri, 7 Feb 2025 19:19:52 -0500 Subject: [PATCH] update to latest Meatloaf code add serial console shell add display task for RGB LED STRIP/LCD support --- components/libsmb2/examples/smb2-stat-sync.c | 2 +- include/cbm_defines.h | 6 +- include/debug.h | 9 +- include/esp-idf-arduino.h | 99 + include/pinmap.h | 7 + include/pinmap/fujiloaf-rev0.h | 20 +- include/pinmap/iec-nugget.h | 57 +- lib/FileSystem/fnFileSMB.cpp | 2 +- lib/FileSystem/fnFsLittleFS.cpp | 3 - lib/FileSystem/fnFsSD.cpp | 3 - lib/FileSystem/fnFsSMB.cpp | 4 + lib/TNFSlib/tnfslib.cpp | 18 +- lib/bus/bus.h | 2 - lib/bus/iec/IECBusHandler.cpp | 3048 ++++++++++++++++++ lib/bus/iec/IECBusHandler.h | 221 ++ lib/bus/iec/IECConfig.h | 63 + lib/bus/iec/IECDevice.cpp | 133 + lib/bus/iec/IECDevice.h | 197 ++ lib/bus/iec/IECFileDevice.cpp | 694 ++++ lib/bus/iec/IECFileDevice.h | 131 + lib/bus/iec/IECespidf.h | 94 + lib/bus/iec/iec.cpp | 680 +--- lib/bus/iec/iec.h | 553 +--- lib/bus/iec/ieee-488.cpp | 0 lib/bus/iec/ieee-488.h | 4 - lib/bus/iec/parallel.cpp | 430 --- lib/bus/iec/parallel.h | 130 - lib/bus/iec/protocol/_protocol.cpp | 58 - lib/bus/iec/protocol/_protocol.h | 87 - lib/bus/iec/protocol/cpbstandardserial.cpp | 383 --- lib/bus/iec/protocol/cpbstandardserial.h | 54 - lib/bus/iec/protocol/jiffydos.cpp | 222 -- lib/bus/iec/protocol/jiffydos.h | 93 - lib/bus/iwm/iwm_ll.cpp | 4 + lib/bus/mac/mac.cpp | 2 +- lib/bus/mac/mac_ll.cpp | 4 +- lib/bus/sio/sio.cpp | 2 +- lib/console/Commands/CoreCommands.cpp | 226 ++ lib/console/Commands/CoreCommands.h | 22 + lib/console/Commands/GPIOCommands.cpp | 219 ++ lib/console/Commands/GPIOCommands.h | 14 + lib/console/Commands/NetworkCommands.cpp | 523 +++ lib/console/Commands/NetworkCommands.h | 16 + lib/console/Commands/SystemCommands.cpp | 285 ++ lib/console/Commands/SystemCommands.h | 16 + lib/console/Commands/VFSCommands.cpp | 490 +++ lib/console/Commands/VFSCommands.h | 34 + lib/console/Commands/XFERCommands.cpp | 186 ++ lib/console/Commands/XFERCommands.h | 35 + lib/console/Console.cpp | 311 ++ lib/console/Console.h | 156 + lib/console/ConsoleCommand.h | 41 + lib/console/ConsoleCommandBase.h | 25 + lib/console/ConsoleCommandD.cpp | 34 + lib/console/ConsoleCommandD.h | 51 + lib/console/ESP32Console.h | 11 + lib/console/Helpers/InputParser.cpp | 58 + lib/console/Helpers/InputParser.h | 14 + lib/console/Helpers/PWDHelpers.cpp | 64 + lib/console/Helpers/PWDHelpers.h | 22 + lib/console/OptionsConsoleCommand.cpp | 49 + lib/console/OptionsConsoleCommand.h | 87 + lib/console/cxxopts/cxxopts.hpp | 2713 ++++++++++++++++ lib/console/improv/improv.cpp | 187 ++ lib/console/improv/improv.h | 80 + lib/console/ute/ute.cpp | 2253 +++++++++++++ lib/console/ute/ute.h | 65 + lib/device/adamnet/fuji.cpp | 2 +- lib/device/cx16_i2c/printer.cpp | 2 + lib/device/drivewire/disk.cpp | 2 +- lib/device/drivewire/network.cpp | 14 +- lib/device/iec/clock.cpp | 168 +- lib/device/iec/clock.h | 34 +- lib/device/iec/cpm.cpp | 203 -- lib/device/iec/cpm.h | 53 +- lib/device/iec/drive.cpp | 2255 +++++++------ lib/device/iec/drive.h | 428 ++- lib/device/iec/fuji.cpp | 582 ++-- lib/device/iec/fuji.h | 108 +- lib/device/iec/modem.cpp | 10 +- lib/device/iec/modem.h | 4 +- lib/device/iec/network.cpp | 759 ++--- lib/device/iec/network.h | 251 +- lib/device/iec/printer.cpp | 65 +- lib/device/iec/printer.h | 51 +- lib/device/iwm/cpm.cpp | 2 +- lib/device/iwm/disk.cpp | 6 +- lib/device/iwm/modem.cpp | 2 +- lib/device/iwm/printer.cpp | 2 +- lib/device/mac/floppy.cpp | 8 +- lib/device/rc2014/disk.cpp | 2 +- lib/device/rs232/modem.cpp | 2 + lib/device/rs232/printer.cpp | 2 + lib/device/sio/cassette.cpp | 2 +- lib/device/sio/fuji.cpp | 60 +- lib/device/sio/modem.cpp | 2 + lib/device/sio/printer.cpp | 2 + lib/display/display.cpp | 323 ++ lib/display/display.h | 121 + lib/fnjson/fnjson.cpp | 2 +- lib/hardware/Esp.cpp | 549 ++++ lib/hardware/Esp.h | 118 + lib/hardware/fnSystem.cpp | 4 +- lib/hardware/fnUART.cpp | 6 + lib/hardware/fnWiFi.cpp | 64 +- lib/hardware/fnWiFi.h | 3 +- lib/http/fnHttpClient.cpp | 12 +- lib/http/httpServiceConfigurator.cpp | 2 +- lib/meatloaf/container/d8b.h | 5 +- lib/meatloaf/device/flash.cpp | 58 +- lib/meatloaf/device/flash.h | 22 +- lib/meatloaf/disk/d64.cpp | 131 +- lib/meatloaf/disk/d64.h | 54 +- lib/meatloaf/disk/d71.h | 6 +- lib/meatloaf/disk/d80.h | 5 +- lib/meatloaf/disk/d81.h | 16 +- lib/meatloaf/disk/d82.h | 5 +- lib/meatloaf/file/p00.cpp | 13 - lib/meatloaf/file/p00.h | 15 +- lib/meatloaf/meat_buffer.h | 4 +- lib/meatloaf/meat_media.cpp | 90 +- lib/meatloaf/meat_media.h | 29 +- lib/meatloaf/meatloaf.cpp | 72 +- lib/meatloaf/meatloaf.h | 150 +- lib/meatloaf/network/http.cpp | 77 +- lib/meatloaf/network/http.h | 13 +- lib/meatloaf/network/tcp.h | 9 +- lib/meatloaf/network/tnfs.cpp | 54 +- lib/meatloaf/network/tnfs.h | 10 +- lib/meatloaf/service/csip.cpp | 16 +- lib/meatloaf/service/csip.h | 12 +- lib/meatloaf/service/ml.h | 4 + lib/meatloaf/tape/t64.cpp | 84 +- lib/meatloaf/tape/t64.h | 13 +- lib/meatloaf/tape/tcrt.cpp | 68 +- lib/meatloaf/tape/tcrt.h | 11 +- lib/meatloaf/wrappers/iec_buffer.cpp | 308 +- lib/meatloaf/wrappers/iec_buffer.h | 182 +- lib/media/adam/mediaTypeDDP.cpp | 4 +- lib/media/adam/mediaTypeROM.cpp | 2 +- lib/media/apple/mediaTypeDSK.cpp | 2 +- lib/media/apple/mediaTypeWOZ.cpp | 20 +- lib/media/atari/diskTypeAtr.cpp | 12 +- lib/media/atari/diskTypeAtx.cpp | 22 +- lib/media/atari/diskTypeXex.cpp | 4 +- lib/media/drivewire/mediaTypeDSK.cpp | 2 +- lib/media/drivewire/mediaTypeMRM.cpp | 2 +- lib/media/h89/mediaTypeIMG.cpp | 6 +- lib/media/lynx/mediaTypeROM.cpp | 2 +- lib/media/mac/mediaTypeMOOF.cpp | 18 +- lib/media/rc2014/mediaTypeIMG.cpp | 6 +- lib/media/rs232/diskTypeImg.cpp | 6 +- lib/tcpip/fnTcpClient.cpp | 2 +- lib/utils/string_utils.cpp | 12 +- lib/utils/string_utils.h | 3 +- lib/utils/utils.cpp | 4 +- platformio-sample.ini | 2 + sdkconfig.fujiloaf-rev0 | 286 +- sdkconfig.fujinet-adam-v1 | 7 + src/main.cpp | 51 +- 160 files changed, 17814 insertions(+), 6290 deletions(-) create mode 100644 include/esp-idf-arduino.h create mode 100644 lib/bus/iec/IECBusHandler.cpp create mode 100644 lib/bus/iec/IECBusHandler.h create mode 100644 lib/bus/iec/IECConfig.h create mode 100644 lib/bus/iec/IECDevice.cpp create mode 100644 lib/bus/iec/IECDevice.h create mode 100644 lib/bus/iec/IECFileDevice.cpp create mode 100644 lib/bus/iec/IECFileDevice.h create mode 100644 lib/bus/iec/IECespidf.h delete mode 100644 lib/bus/iec/ieee-488.cpp delete mode 100644 lib/bus/iec/ieee-488.h delete mode 100644 lib/bus/iec/parallel.cpp delete mode 100644 lib/bus/iec/parallel.h delete mode 100644 lib/bus/iec/protocol/_protocol.cpp delete mode 100644 lib/bus/iec/protocol/_protocol.h delete mode 100644 lib/bus/iec/protocol/cpbstandardserial.cpp delete mode 100644 lib/bus/iec/protocol/cpbstandardserial.h delete mode 100644 lib/bus/iec/protocol/jiffydos.cpp delete mode 100644 lib/bus/iec/protocol/jiffydos.h create mode 100644 lib/console/Commands/CoreCommands.cpp create mode 100644 lib/console/Commands/CoreCommands.h create mode 100644 lib/console/Commands/GPIOCommands.cpp create mode 100644 lib/console/Commands/GPIOCommands.h create mode 100644 lib/console/Commands/NetworkCommands.cpp create mode 100644 lib/console/Commands/NetworkCommands.h create mode 100644 lib/console/Commands/SystemCommands.cpp create mode 100644 lib/console/Commands/SystemCommands.h create mode 100644 lib/console/Commands/VFSCommands.cpp create mode 100644 lib/console/Commands/VFSCommands.h create mode 100644 lib/console/Commands/XFERCommands.cpp create mode 100644 lib/console/Commands/XFERCommands.h create mode 100644 lib/console/Console.cpp create mode 100644 lib/console/Console.h create mode 100644 lib/console/ConsoleCommand.h create mode 100644 lib/console/ConsoleCommandBase.h create mode 100644 lib/console/ConsoleCommandD.cpp create mode 100644 lib/console/ConsoleCommandD.h create mode 100644 lib/console/ESP32Console.h create mode 100644 lib/console/Helpers/InputParser.cpp create mode 100644 lib/console/Helpers/InputParser.h create mode 100644 lib/console/Helpers/PWDHelpers.cpp create mode 100644 lib/console/Helpers/PWDHelpers.h create mode 100644 lib/console/OptionsConsoleCommand.cpp create mode 100644 lib/console/OptionsConsoleCommand.h create mode 100644 lib/console/cxxopts/cxxopts.hpp create mode 100644 lib/console/improv/improv.cpp create mode 100644 lib/console/improv/improv.h create mode 100644 lib/console/ute/ute.cpp create mode 100644 lib/console/ute/ute.h create mode 100644 lib/display/display.cpp create mode 100644 lib/display/display.h create mode 100644 lib/hardware/Esp.cpp create mode 100644 lib/hardware/Esp.h diff --git a/components/libsmb2/examples/smb2-stat-sync.c b/components/libsmb2/examples/smb2-stat-sync.c index f33d25957..6bb77f60c 100644 --- a/components/libsmb2/examples/smb2-stat-sync.c +++ b/components/libsmb2/examples/smb2-stat-sync.c @@ -93,7 +93,7 @@ int main(int argc, char *argv[]) t = (time_t)st.smb2_ctime; printf("Ctime:%s", asctime(localtime(&t))); t = (time_t)st.smb2_btime; - printf("Btime:%s", asctime(localtime(&t))); + printf("Btime:%s", asctime(localtime(&t))); smb2_disconnect_share(smb2); smb2_destroy_url(url); diff --git a/include/cbm_defines.h b/include/cbm_defines.h index a2ceb3d2a..5135aa4a1 100644 --- a/include/cbm_defines.h +++ b/include/cbm_defines.h @@ -159,7 +159,7 @@ typedef enum #define TIMING_Tna 32 // Extra delay before first bit is sent #define TIMEOUT_Tne 250 -#define TIMING_Ts 70 // BIT SET-UP TALKER 71us 20us 70us - +#define TIMING_Ts 80 // BIT SET-UP TALKER 71us 20us 70us - #define TIMING_Ts0 40 // BIT SET-UP LISTENER PRE 57us 47us #define TIMING_Ts1 30 // BIT SET-UP LISTENER POST 18us 24us #define TIMING_Tv 20 // DATA VALID VIC20 76us 26us 20us 20us - (Tv and Tpr minimum must be 60μ s for external device to be a talker. ) @@ -204,14 +204,10 @@ typedef enum // Not Inverted #define IEC_ASSERTED true #define IEC_RELEASED false -#define LOW 0x00 -#define HIGH 0x01 #else // Inverted #define IEC_ASSERTED false #define IEC_RELEASED true -#define LOW 0x01 -#define HIGH 0x00 #endif #endif // CBMDEFINES_H diff --git a/include/debug.h b/include/debug.h index b6fe674fe..0e302558a 100644 --- a/include/debug.h +++ b/include/debug.h @@ -26,12 +26,13 @@ #include "../lib/hardware/fnUART.h" #define Serial fnUartDebug - #define Debug_print(...) fnUartDebug.print( __VA_ARGS__ ) - #define Debug_printf(...) fnUartDebug.printf( __VA_ARGS__ ) - #define Debug_println(...) fnUartDebug.println( __VA_ARGS__ ) - #define Debug_printv(format, ...) {fnUartDebug.printf( ANSI_YELLOW "[%s:%u] %s(): " ANSI_GREEN_BOLD format ANSI_RESET "\r\n", __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__);} + #define Debug_print(...) printf( __VA_ARGS__ ) + #define Debug_printf(format, ...) { printf( format, ##__VA_ARGS__ ); } + #define Debug_println(...) { printf( __VA_ARGS__ ); printf( "\r\n" ); } + #define Debug_printv(format, ...) { printf( ANSI_YELLOW "[%s:%d] %s(): " ANSI_GREEN_BOLD format ANSI_RESET "\r\n", __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__);} #define HEAP_CHECK(x) Debug_printf("HEAP CHECK %s " x "\r\n", heap_caps_check_integrity_all(true) ? "PASSED":"FAILED") + #define DEBUG_MEM_LEAK {Debug_printv("Heap[%lu] Low[%lu] Task[%u]", esp_get_free_heap_size(), esp_get_free_internal_heap_size(), uxTaskGetStackHighWaterMark(NULL));} #else // Use util_debug_printf() helper function #include diff --git a/include/esp-idf-arduino.h b/include/esp-idf-arduino.h new file mode 100644 index 000000000..af40657fa --- /dev/null +++ b/include/esp-idf-arduino.h @@ -0,0 +1,99 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2024 David Hansel +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have receikved a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#ifndef IECESPIDF_H +#define IECESPIDF_H + +#include +#include +#include +#include +#include +#include "hal/gpio_hal.h" +#include + +#define ARDUINO 2024 +#define ESP32 + +#define PROGMEM + +#pragma GCC diagnostic ignored "-Wunused-function" +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wvolatile" + +typedef void (*interruptFcn)(void *); + +#define INPUT 0x0 +#define OUTPUT 0x1 +#define INPUT_PULLUP 0x2 +#define LOW 0x0 +#define HIGH 0x1 +#define FALLING GPIO_INTR_NEGEDGE +#define RISING GPIO_INTR_POSEDGE +#define bit(n) (1<<(n)) +#define digitalWrite(pin, v) gpio_set_level((gpio_num_t) pin, v); +#define pinMode(pin, mode) { gpio_reset_pin((gpio_num_t) pin); gpio_set_direction((gpio_num_t)pin, mode==OUTPUT ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT); if( mode==INPUT_PULLUP ) gpio_pullup_en((gpio_num_t) pin); } +#define digitalPinToGPIONumber(digitalPin) (digitalPin) +#define digitalPinToBitMask(pin) (1UL << (digitalPinToGPIONumber(pin)&31)) +#define portInputRegister(port) ((volatile uint32_t*)((port)?GPIO_IN1_REG:GPIO_IN_REG)) +#define portOutputRegister(port) ((volatile uint32_t*)((port)?GPIO_OUT1_REG:GPIO_OUT_REG)) +#define portModeRegister(port) ((volatile uint32_t*)((port)?GPIO_ENABLE1_REG:GPIO_ENABLE_REG)) +#define digitalPinToPort(pin) ((digitalPinToGPIONumber(pin)>31)?1:0) +#define digitalPinToInterrupt(p) ((((uint8_t)digitalPinToGPIONumber(p))(y) ? (x) : (y)) + +static void delayMicroseconds(uint32_t n) +{ + uint32_t s = micros(); + while((micros()-s)= SOC_GPIO_PIN_COUNT) return; + + if (!interrupt_initialized) { + esp_err_t err = gpio_install_isr_service(0 /* ESP_INTR_FLAG_IRAM */); + interrupt_initialized = (err == ESP_OK) || (err == ESP_ERR_INVALID_STATE); + } + + gpio_set_intr_type((gpio_num_t)pin, intr_type); + gpio_isr_handler_add((gpio_num_t)pin, userFunc, NULL); + + gpio_hal_context_t gpiohal; + gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0); + gpio_hal_input_enable(&gpiohal, pin); +} + +static void detachInterrupt(uint8_t pin) +{ + gpio_isr_handler_remove((gpio_num_t)pin); + gpio_set_intr_type((gpio_num_t)pin, GPIO_INTR_DISABLE); +} + +#endif diff --git a/include/pinmap.h b/include/pinmap.h index 015476d52..a04958ef0 100644 --- a/include/pinmap.h +++ b/include/pinmap.h @@ -30,6 +30,13 @@ #include "pinmap/atari2600.h" +/* LED Strip NEW */ +#define RGB_LED_DATA_PIN PIN_LED_RGB +#define RGB_LED_BRIGHTNESS 15 // max mA the LED can use determines brightness +#define RGB_LED_COUNT 5 +#define RGB_LED_TYPE WS2812B +#define RGB_LED_ORDER GRB + #ifndef PIN_DEBUG #define PIN_DEBUG PIN_IEC_SRQ #endif diff --git a/include/pinmap/fujiloaf-rev0.h b/include/pinmap/fujiloaf-rev0.h index 430b64f0d..f876ecfd4 100644 --- a/include/pinmap/fujiloaf-rev0.h +++ b/include/pinmap/fujiloaf-rev0.h @@ -2,7 +2,14 @@ #ifndef PINMAP_FUJILOAF_REV0_H #define PINMAP_FUJILOAF_REV0_H +// https://www.espressif.com.cn/sites/default/files/documentation/esp32-wrover-e_esp32-wrover-ie_datasheet_en.pdf + #ifdef PINMAP_FUJILOAF_REV0 + +// ESP32-WROVER-E-N16R8 +#define FLASH_SIZE 16 +#define PSRAM_SIZE 8 + /* SD Card */ #define PIN_CARD_DETECT GPIO_NUM_35 // fnSystem.h #define PIN_CARD_DETECT_FIX GPIO_NUM_35 // fnSystem.h @@ -29,20 +36,11 @@ #define PIN_LED_WIFI GPIO_NUM_2 // led.cpp #define PIN_LED_BUS GPIO_NUM_12 // 4 FN #define PIN_LED_BT GPIO_NUM_NC // No BT LED - -/* LED Strip NEW */ -#define LEDSTRIP_DATA_PIN GPIO_NUM_4 -#define LEDSTRIP_COUNT 5 -#define LEDSTRIP_BRIGHTNESS 15 // max mA the LED can use determines brightness -#define LEDSTRIP_TYPE WS2812B -#define LEDSTRIP_RGB_ORDER GRB -// LED order on the strip starting with 0 -#define LEDSTRIP_WIFI_NUM 0 -#define LEDSTRIP_BUS_NUM 4 -#define LEDSTRIP_BT_NUM 2 +#define PIN_LED_RGB GPIO_NUM_4 /* Audio Output */ #define PIN_DAC1 GPIO_NUM_25 // samlib.h +#define PIN_I2S GPIO_NUM_25 // Reset line is available #define IEC_HAS_RESET diff --git a/include/pinmap/iec-nugget.h b/include/pinmap/iec-nugget.h index 1a5c9ccf1..258db6f67 100644 --- a/include/pinmap/iec-nugget.h +++ b/include/pinmap/iec-nugget.h @@ -1,15 +1,19 @@ #ifndef PINMAP_IEC_NUGGET_H #define PINMAP_IEC_NUGGET_H +// https://www.wemos.cc/en/latest/d32/d32_pro.html +// https://www.wemos.cc/en/latest/_static/files/sch_d32_pro_v2.0.0.pdf +// https://www.espressif.com.cn/sites/default/files/documentation/esp32-wrover-e_esp32-wrover-ie_datasheet_en.pdf + #ifdef PINMAP_IEC_NUGGET +// ESP32-WROVER-E-N16R8 +#define FLASH_SIZE 16 +#define PSRAM_SIZE 8 + /* SD Card */ -// pins 12-15 are used to interface with the JTAG debugger -// so leave them alone if we're using JTAG -#ifndef JTAG -#define PIN_CARD_DETECT GPIO_NUM_15 // fnSystem.h +#define PIN_CARD_DETECT GPIO_NUM_12 // fnSystem.h #define PIN_CARD_DETECT_FIX GPIO_NUM_15 // fnSystem.h -#endif #define PIN_SD_HOST_CS GPIO_NUM_4 // LOLIN D32 Pro #define PIN_SD_HOST_MISO GPIO_NUM_19 @@ -21,39 +25,42 @@ #define PIN_UART0_TX GPIO_NUM_1 #define PIN_UART1_RX GPIO_NUM_9 #define PIN_UART1_TX GPIO_NUM_10 -#define PIN_UART2_RX GPIO_NUM_33 -#define PIN_UART2_TX GPIO_NUM_21 +#define PIN_UART2_RX GPIO_NUM_NC +#define PIN_UART2_TX GPIO_NUM_NC /* Buttons */ -#define PIN_BUTTON_A GPIO_NUM_NC // keys.cpp +#define PIN_BUTTON_A GPIO_NUM_0 // keys.cpp #define PIN_BUTTON_B GPIO_NUM_NC #define PIN_BUTTON_C GPIO_NUM_NC /* LEDs */ -#define PIN_LED_WIFI GPIO_NUM_5 // led.cpp -#define PIN_LED_BUS GPIO_NUM_2 // 4 FN - -// pins 12-15 are used to interface with the JTAG debugger -// so leave them alone if we're using JTAG -#ifndef JTAG +#define PIN_LED_WIFI GPIO_NUM_2 // led.cpp +#define PIN_LED_BUS GPIO_NUM_5 #define PIN_LED_BT GPIO_NUM_13 -#else -#define PIN_LED_BT GPIO_NUM_5 // LOLIN D32 PRO -#endif +#define PIN_LED_RGB GPIO_NUM_13 /* Audio Output */ #define PIN_DAC1 GPIO_NUM_25 // samlib.h /* Commodore IEC Pins */ -//#define IEC_HAS_RESET // Reset line is available +// CLK & DATA lines in/out are split between two pins +//#define IEC_SPLIT_LINES + +// Line values are inverted (7406 Hex Inverter Buffer) +//#define IEC_INVERTED_LINES -#define PIN_IEC_RESET GPIO_NUM_34 -#define PIN_IEC_ATN GPIO_NUM_32 -#define PIN_IEC_CLK_IN GPIO_NUM_33 -#define PIN_IEC_CLK_OUT GPIO_NUM_33 -#define PIN_IEC_DATA_IN GPIO_NUM_14 -#define PIN_IEC_DATA_OUT GPIO_NUM_14 -#define PIN_IEC_SRQ GPIO_NUM_27 +// Reset line is available +#define IEC_HAS_RESET + // WIRING + // C64 DIN6 D32Pro TFT +#define PIN_IEC_ATN GPIO_NUM_32 // ATN 3 A T-LED 32 10 (PURPLE) +#define PIN_IEC_CLK_IN GPIO_NUM_33 // CLK 4 A T-RST 33 8 (BROWN) +#define PIN_IEC_CLK_OUT GPIO_NUM_33 // +#define PIN_IEC_DATA_IN GPIO_NUM_14 // DATA 5 T-CS 14 2 (BLACK) +#define PIN_IEC_DATA_OUT GPIO_NUM_14 // +#define PIN_IEC_SRQ GPIO_NUM_27 // SRQ 1 T-DC 27 7 (ORANGE) +#define PIN_IEC_RESET GPIO_NUM_34 // RESET 6 A 34 N/C + // GND 2 GND 9 (GREY) /* Modem/Parallel Switch */ /* Unused with Nugget */ diff --git a/lib/FileSystem/fnFileSMB.cpp b/lib/FileSystem/fnFileSMB.cpp index edd852898..d5c005eb4 100644 --- a/lib/FileSystem/fnFileSMB.cpp +++ b/lib/FileSystem/fnFileSMB.cpp @@ -44,7 +44,7 @@ int FileHandlerSMB::seek(long int off, int whence) Debug_printf("%s\n", smb2_get_error(_smb)); return -1; } - Debug_printf("new pos is %lu\n", new_pos); + Debug_printf("new pos is %llu\n", new_pos); return 0; } diff --git a/lib/FileSystem/fnFsLittleFS.cpp b/lib/FileSystem/fnFsLittleFS.cpp index 404592c13..400573829 100755 --- a/lib/FileSystem/fnFsLittleFS.cpp +++ b/lib/FileSystem/fnFsLittleFS.cpp @@ -249,9 +249,6 @@ bool FileSystemLittleFS::start() Debug_printv(" partition size: %u, used: %u, free: %u\r\n", total, used, total-used); */ #endif - - // Create SYSTEM DIR if it doesn't exist - //create_path( SYSTEM_DIR ); } return _started; diff --git a/lib/FileSystem/fnFsSD.cpp b/lib/FileSystem/fnFsSD.cpp index ab931ff62..0baf4aec1 100644 --- a/lib/FileSystem/fnFsSD.cpp +++ b/lib/FileSystem/fnFsSD.cpp @@ -685,9 +685,6 @@ bool FileSystemSDFAT::start() Debug_printf(" partition type: %s\r\n", partition_type()); Debug_printf(" partition size: %llu, used: %llu\r\n", total_bytes(), used_bytes()); */ - - // Create SYSTEM DIR if it doesn't exist - //create_path( SYSTEM_DIR ); } else { diff --git a/lib/FileSystem/fnFsSMB.cpp b/lib/FileSystem/fnFsSMB.cpp index 4cf62da0e..b54e5c658 100644 --- a/lib/FileSystem/fnFsSMB.cpp +++ b/lib/FileSystem/fnFsSMB.cpp @@ -243,9 +243,13 @@ bool FileSystemSMB::dir_open(const char *path, const char *pattern, uint16_t di fs_de->modified_time = (time_t)smb_de->st.smb2_mtime; if (fs_de->isDir) + { Debug_printf(" add entry: \"%s\"\tDIR\n", fs_de->filename); + } else + { Debug_printf(" add entry: \"%s\"\t%lu\n", fs_de->filename, fs_de->size); + } } smb2_closedir(_smb, smb_dir); } diff --git a/lib/TNFSlib/tnfslib.cpp b/lib/TNFSlib/tnfslib.cpp index 6429ee264..344a3d8f7 100644 --- a/lib/TNFSlib/tnfslib.cpp +++ b/lib/TNFSlib/tnfslib.cpp @@ -234,7 +234,7 @@ int tnfs_open(tnfsMountInfo *m_info, const char *filepath, uint16_t open_mode, u else if (open_mode & TNFS_OPENMODE_WRITE_TRUNCATE) pFileInf->file_size = 0; } - Debug_printf("File opened, handle ID: %hd, size: %u, pos: %u\r\n", *file_handle, pFileInf->file_size, pFileInf->file_position); + Debug_printf("File opened, handle ID: %hd, size: %lu, pos: %lu\r\n", *file_handle, pFileInf->file_size, pFileInf->file_position); } result = packet.payload[0]; } @@ -278,14 +278,14 @@ int tnfs_close(tnfsMountInfo *m_info, int16_t file_handle) void _tnfs_cache_dump(const char *title, uint8_t *cache, uint32_t cache_size) { int bytes_per_line = 16; - Debug_printf("\n%s %u\r\n", title, cache_size); + Debug_printf("\n%s %lu\r\n", title, cache_size); for (int j = 0; j < cache_size; j += bytes_per_line) { for (int k = 0; (k + j) < cache_size && k < bytes_per_line; k++) Debug_printf("%02X ", cache[k + j]); - Debug_println(""); + Debug_println("\r\n"); } - Debug_println(""); + Debug_println("\r\n"); } /* @@ -295,7 +295,7 @@ void _tnfs_cache_dump(const char *title, uint8_t *cache, uint32_t cache_size) int _tnfs_read_from_cache(tnfsFileHandleInfo *pFHI, uint8_t *dest, uint16_t dest_size, uint16_t *dest_used) { #ifdef VERBOSE_TNFS - Debug_printf("_tnfs_read_from_cache: buffpos=%d, cache_start=%d, cache_avail=%d, dest_size=%d, dest_used=%d\r\n", + Debug_printf("_tnfs_read_from_cache: buffpos=%lu, cache_start=%lu, cache_avail=%lu, dest_size=%u, dest_used=%u\r\n", pFHI->cached_pos, pFHI->cache_start, pFHI->cache_available, dest_size, *dest_used); #endif @@ -370,7 +370,7 @@ int _tnfs_fill_cache(tnfsMountInfo *m_info, tnfsFileHandleInfo *pFHI) // Note that when we're filling the cache, we're dealing with the "real" file position, // not the cached_position we also keep track of on behalf of the client #ifdef VERBOSE_TNFS - Debug_printf("_TNFS_FILL_CACHE fh=%d, file_position=%d\r\n", pFHI->handle_id, pFHI->file_position); + Debug_printf("_TNFS_FILL_CACHE fh=%d, file_position=%lu\r\n", pFHI->handle_id, pFHI->file_position); #endif int error = 0; @@ -416,7 +416,7 @@ int _tnfs_fill_cache(tnfsMountInfo *m_info, tnfsFileHandleInfo *pFHI) bytes_remaining_to_load -= bytes_read; #ifdef VERBOSE_TNFS - Debug_printf("_tnfs_fill_cache got %u bytes, %u more bytes needed\r\n", bytes_read, bytes_remaining_to_load); + Debug_printf("_tnfs_fill_cache got %u bytes, %lu more bytes needed\r\n", bytes_read, bytes_remaining_to_load); #endif } else if(tnfs_result == TNFS_RESULT_END_OF_FILE) @@ -764,7 +764,7 @@ void _readdirx_fill_response(tnfsDirCacheEntry *pCached, tnfsStat *filestat, cha strftime(t_m, sizeof(t_m), tfmt, localtime(&tt)); tt = filestat->c_time; strftime(t_c, sizeof(t_c), tfmt, localtime(&tt)); - Debug_printf("\t_readdirx_fill_response: dir: %s, size: %u, mtime: %s, ctime: %s \"%s\"\r\n", + Debug_printf("\t_readdirx_fill_response: dir: %s, size: %lu, mtime: %s, ctime: %s \"%s\"\r\n", filestat->isDir ? "Yes" : "no", filestat->filesize, t_m, t_c, dir_entry ); } @@ -1647,7 +1647,7 @@ void _tnfs_debug_packet(const tnfsPacket &pkt, unsigned short payload_size, bool Debug_printf("\t[%02x%02x %02x %02x] ", pkt.session_idh, pkt.session_idl, pkt.sequence_num, pkt.command); for (int i = 0; i < payload_size; i++) Debug_printf("%02x ", pkt.payload[i]); - Debug_println(""); + Debug_println("\r\n"); #endif } diff --git a/lib/bus/bus.h b/lib/bus/bus.h index 9d7157342..e10a2b5e4 100644 --- a/lib/bus/bus.h +++ b/lib/bus/bus.h @@ -13,8 +13,6 @@ #ifdef BUILD_IEC #include "iec/iec.h" -#include "iec/parallel.h" -#include "iec/ieee-488.h" #define SYSTEM_BUS IEC #define FN_BUS_PORT fnUartBUS // TBD #endif diff --git a/lib/bus/iec/IECBusHandler.cpp b/lib/bus/iec/IECBusHandler.cpp new file mode 100644 index 000000000..81f3a1945 --- /dev/null +++ b/lib/bus/iec/IECBusHandler.cpp @@ -0,0 +1,3048 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2023 David Hansel +// +// This implementation is based on the code used in the VICE emulator. +// The corresponding code in VICE (src/serial/serial-iec-device.c) was +// contributed to VICE by me in 2003. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#include "IECBusHandler.h" +#include "IECDevice.h" + +#if defined(ARDUINO) +#include +#elif defined(ESP_PLATFORM) +#include "IECespidf.h" +#endif + +#ifndef ESP_IDF_VERSION_VAL +#define ESP_IDF_VERSION_VAL(x,y,z) 0 +#endif + +#ifndef NOT_AN_INTERRUPT +#define NOT_AN_INTERRUPT -1 +#endif + +#ifndef INTERRUPT_FCN_ARG +#define INTERRUPT_FCN_ARG +#endif + +#if MAX_DEVICES>30 +#error "Maximum allowed number of devices is 30" +#endif + +//#define JDEBUG + +// ---------------- Arduino 8-bit ATMega (UNO R3/Mega/Mini/Micro/Leonardo...) + +#if defined(__AVR__) + +#if defined(__AVR_ATmega32U4__) +// Atmega32U4 does not have a second 8-bit timer (first one is used by Arduino millis()) +// => use lower 8 bit of 16-bit timer 1 +#define timer_init() { TCCR1A=0; TCCR1B=0; } +#define timer_reset() TCNT1L=0 +#define timer_start() TCCR1B |= bit(CS11) +#define timer_stop() TCCR1B &= ~bit(CS11) +#define timer_less_than(us) (TCNT1L < ((uint8_t) (2*(us)))) +#define timer_wait_until(us) while( timer_less_than(us) ) +#else +// use 8-bit timer 2 with /8 prescaler +#define timer_init() { TCCR2A=0; TCCR2B=0; } +#define timer_reset() TCNT2=0 +#define timer_start() TCCR2B |= bit(CS21) +#define timer_stop() TCCR2B &= ~bit(CS21) +#define timer_less_than(us) (TCNT2 < ((uint8_t) (2*(us)))) +#define timer_wait_until(us) while( timer_less_than(us) ) +#endif + +//NOTE: Must disable SUPPORT_DOLPHIN, otherwise no pins left for debugging (except Mega) +#ifdef JDEBUG +#define JDEBUGI() DDRD |= 0x80; PORTD &= ~0x80 // PD7 = pin digital 7 +#define JDEBUG0() PORTD&=~0x80 +#define JDEBUG1() PORTD|=0x80 +#endif + +// ---------------- Arduino Uno R4 + +#elif defined(ARDUINO_UNOR4_MINIMA) || defined(ARDUINO_UNOR4_WIFI) +#ifndef ARDUINO_UNOR4 +#define ARDUINO_UNOR4 +#endif + +// NOTE: this assumes the AGT timer is running at the (Arduino default) 3MHz rate +// and rolling over after 3000 ticks +static unsigned long timer_start_ticks; +static uint16_t timer_ticks_diff(uint16_t t0, uint16_t t1) { return ((t0 < t1) ? 3000 + t0 : t0) - t1; } +#define timer_init() while(0) +#define timer_reset() timer_start_ticks = R_AGT0->AGT +#define timer_start() timer_start_ticks = R_AGT0->AGT +#define timer_stop() while(0) +#define timer_less_than(us) (timer_ticks_diff(timer_start_ticks, R_AGT0->AGT) < ((int) (us*3))) +#define timer_wait_until(us) while( timer_less_than(us) ) + +#ifdef JDEBUG +#define JDEBUGI() pinMode(1, OUTPUT) +#define JDEBUG0() R_PORT3->PODR &= ~bit(2); +#define JDEBUG1() R_PORT3->PODR |= bit(2); +#endif + +// ---------------- Arduino Due + +#elif defined(__SAM3X8E__) + +#define portModeRegister(port) 0 + +static unsigned long timer_start_ticks; +static uint32_t timer_ticks_diff(uint32_t t0, uint32_t t1) { return ((t0 < t1) ? 84000 + t0 : t0) - t1; } +#define timer_init() while(0) +#define timer_reset() timer_start_ticks = SysTick->VAL; +#define timer_start() timer_start_ticks = SysTick->VAL; +#define timer_stop() while(0) +#define timer_less_than(us) (timer_ticks_diff(timer_start_ticks, SysTick->VAL) < ((int) (us*84))) +#define timer_wait_until(us) while( timer_less_than(us) ) + +#ifdef JDEBUG +#define JDEBUGI() pinMode(2, OUTPUT) +#define JDEBUG0() digitalWriteFast(2, LOW); +#define JDEBUG1() digitalWriteFast(2, HIGH); +#endif + +// ---------------- Raspberry Pi Pico + +#elif defined(ARDUINO_ARCH_RP2040) + +// note: micros() call on MBED core is SLOW - using time_us_32() instead +static unsigned long timer_start_us; +#define timer_init() while(0) +#define timer_reset() timer_start_us = time_us_32() +#define timer_start() timer_start_us = time_us_32() +#define timer_stop() while(0) +#define timer_less_than(us) ((time_us_32()-timer_start_us) < ((int) (us+0.5))) +#define timer_wait_until(us) while( timer_less_than(us) ) + +#ifdef JDEBUG +#define JDEBUGI() pinMode(20, OUTPUT) +#define JDEBUG0() gpio_put(20, 0) +#define JDEBUG1() gpio_put(20, 1) +#endif + +// ---------------- ESP32 + +#elif defined(ESP_PLATFORM) + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) +#include "esp_clk.h" +#define esp_cpu_cycle_count_t uint32_t +#define esp_cpu_get_cycle_count esp_cpu_get_ccount +#define esp_rom_get_cpu_ticks_per_us() (esp_clk_cpu_freq()/1000000) +#endif +static DRAM_ATTR esp_cpu_cycle_count_t timer_start_cycles, timer_cycles_per_us; +#define timer_init() timer_cycles_per_us = esp_rom_get_cpu_ticks_per_us() +#define timer_reset() timer_start_cycles = esp_cpu_get_cycle_count() +#define timer_start() timer_start_cycles = esp_cpu_get_cycle_count() +#define timer_stop() while(0) +#define timer_less_than(us) ((esp_cpu_get_cycle_count()-timer_start_cycles) < (uint32_t(us+0.5)*timer_cycles_per_us)) +#define timer_wait_until(us) timer_wait_until_(us+0.5) +FORCE_INLINE_ATTR void timer_wait_until_(uint32_t us) +{ + // using esp_cpu_get_cycle_count() instead of esp_timer_get_time() works much + // better in timing-critical code since it translates into a single CPU instruction + // instead of a library call that may introduce short delays due to flash ROM access + // conflicts with the other core + esp_cpu_cycle_count_t to = us * timer_cycles_per_us; + while( (esp_cpu_get_cycle_count()-timer_start_cycles) < to ); +} + +// interval in which we need to feed the interrupt WDT to stop it from re-booting the system +#define IWDT_FEED_TIME ((CONFIG_ESP_INT_WDT_TIMEOUT_MS-10)*1000) + +// keep track whether interrupts are enabled or not (see comments in waitPinDATA/waitPinCLK) +static bool haveInterrupts = true; +#undef noInterrupts +#undef interrupts +#define noInterrupts() { portDISABLE_INTERRUPTS(); haveInterrupts = false; } +#define interrupts() { haveInterrupts = true; portENABLE_INTERRUPTS(); } + +#if defined(JDEBUG) +#define JDEBUGI() pinMode(12, OUTPUT) +#define JDEBUG0() GPIO.out_w1tc = bit(12) +#define JDEBUG1() GPIO.out_w1ts = bit(12) +#endif + +// ---------------- other (32-bit) platforms + +#else + +static unsigned long timer_start_us; +#define timer_init() while(0) +#define timer_reset() timer_start_us = micros() +#define timer_start() timer_start_us = micros() +#define timer_stop() while(0) +#define timer_less_than(us) ((micros()-timer_start_us) < ((int) (us+0.5))) +#define timer_wait_until(us) while( timer_less_than(us) ) + +#if defined(JDEBUG) && defined(ESP_PLATFORM) +#define JDEBUGI() pinMode(26, OUTPUT) +#define JDEBUG0() GPIO.out_w1tc = bit(26) +#define JDEBUG1() GPIO.out_w1ts = bit(26) +#endif + +#endif + +#ifndef JDEBUG +#define JDEBUGI() +#define JDEBUG0() +#define JDEBUG1() +#endif + +#if defined(__SAM3X8E__) +// Arduino Due +#define pinModeFastExt(pin, reg, bit, dir) { if( (dir)==OUTPUT ) digitalPinToPort(pin)->PIO_OER |= bit; else digitalPinToPort(pin)->PIO_ODR |= bit; } +#define digitalReadFastExt(pin, reg, bit) (*(reg) & (bit)) +#define digitalWriteFastExt(pin, reg, bit, v) { if( v ) *(reg)|=(bit); else (*reg)&=~(bit); } +#elif defined(ARDUINO_ARCH_RP2040) +// Raspberry Pi Pico +#define pinModeFastExt(pin, reg, bit, dir) gpio_set_dir(pin, (dir)==OUTPUT) +#define digitalReadFastExt(pin, reg, bit) gpio_get(pin) +#define digitalWriteFastExt(pin, reg, bit, v) gpio_put(pin, v) +#elif defined(__AVR__) || defined(ARDUINO_UNOR4) +// Arduino 8-bit (Uno R3/Mega/...) +#define pinModeFastExt(pin, reg, bit, dir) { if( (dir)==OUTPUT ) *(reg)|=(bit); else *(reg)&=~(bit); } +#define digitalReadFastExt(pin, reg, bit) (*(reg) & (bit)) +#define digitalWriteFastExt(pin, reg, bit, v) { if( v ) *(reg)|=(bit); else (*reg)&=~(bit); } +#elif defined(ESP_PLATFORM) +// ESP32 +#define pinModeFastExt(pin, reg, bit, dir) { if( (dir)==OUTPUT ) *(reg)|=(bit); else *(reg)&=~(bit); } +#define digitalReadFastExt(pin, reg, bit) (*(reg) & (bit)) +#define digitalWriteFastExt(pin, reg, bit, v) { if( v ) *(reg)|=(bit); else (*reg)&=~(bit); } +#else +#warning "No fast digital I/O macros defined for this platform - code will likely run too slow" +#define pinModeFastExt(pin, reg, bit, dir) pinMode(pin, dir) +#define digitalReadFastExt(pin, reg, bit) digitalRead(pin) +#define digitalWriteFastExt(pin, reg, bit, v) digitalWrite(pin, v) +#endif + +// on ESP32 we need very timing-critical code (i.e. transmitting/receiving data +// under fast-load protocols) to reside in IRAM in order to avoid short delays +// due to flash ROM access conflicts with the other core +#ifndef IRAM_ATTR +#ifdef ESP_PLATFORM +#error "Expected IRAM_ATTR to be defined" +#else +#define IRAM_ATTR +#endif +#endif + + +// delayMicroseconds on some platforms does not work if called when interrupts are disabled +// => define a version that does work on all supported platforms +static IRAM_ATTR void delayMicrosecondsISafe(uint16_t t) +{ + timer_init(); + timer_start(); + while( t>125 ) { timer_wait_until(125); timer_reset(); t -= 125; } + timer_wait_until(t); + timer_stop(); +} + + +// ----------------------------------------------------------------------------------------- + +#define P_ATN 0x80 +#define P_LISTENING 0x40 +#define P_TALKING 0x20 +#define P_DONE 0x10 +#define P_RESET 0x08 + +#define S_JIFFY_ENABLED 0x0001 // JiffyDos support is enabled +#define S_JIFFY_DETECTED 0x0002 // Detected JiffyDos request from host +#define S_JIFFY_BLOCK 0x0004 // Detected JiffyDos block transfer request from host +#define S_DOLPHIN_ENABLED 0x0008 // DolphinDos support is enabled +#define S_DOLPHIN_DETECTED 0x0010 // Detected DolphinDos request from host +#define S_DOLPHIN_BURST_ENABLED 0x0020 // DolphinDos burst mode is enabled +#define S_DOLPHIN_BURST_TRANSMIT 0x0040 // Detected DolphinDos burst transmit request from host +#define S_DOLPHIN_BURST_RECEIVE 0x0080 // Detected DolphinDos burst receive request from host +#define S_EPYX_ENABLED 0x0100 // Epyx FastLoad support is enabled +#define S_EPYX_HEADER 0x0200 // Read EPYX FastLoad header (drive code transmission) +#define S_EPYX_LOAD 0x0400 // Detected Epyx "load" request +#define S_EPYX_SECTOROP 0x0800 // Detected Epyx "sector operation" request + +#define TC_NONE 0 +#define TC_DATA_LOW 1 +#define TC_DATA_HIGH 2 +#define TC_CLK_LOW 3 +#define TC_CLK_HIGH 4 + + +IECBusHandler *IECBusHandler::s_bushandler = NULL; + + +void IRAM_ATTR IECBusHandler::writePinCLK(bool v) +{ + // Emulate open collector behavior: + // - switch pin to INPUT mode (high-Z output) for true + // - switch pun to OUTPUT mode (LOW output) for false + pinModeFastExt(m_pinCLK, m_regCLKmode, m_bitCLK, v ? INPUT : OUTPUT); +} + + +void IRAM_ATTR IECBusHandler::writePinDATA(bool v) +{ + // Emulate open collector behavior: + // - switch pin to INPUT mode (high-Z output) for true + // - switch pun to OUTPUT mode (LOW output) for false + pinModeFastExt(m_pinDATA, m_regDATAmode, m_bitDATA, v ? INPUT : OUTPUT); +} + + +void IRAM_ATTR IECBusHandler::writePinCTRL(bool v) +{ + if( m_pinCTRL!=0xFF ) + digitalWrite(m_pinCTRL, v); +} + + +bool IRAM_ATTR IECBusHandler::readPinATN() +{ + return digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN)!=0; +} + + +bool IRAM_ATTR IECBusHandler::readPinCLK() +{ + return digitalReadFastExt(m_pinCLK, m_regCLKread, m_bitCLK)!=0; +} + + +bool IRAM_ATTR IECBusHandler::readPinDATA() +{ + return digitalReadFastExt(m_pinDATA, m_regDATAread, m_bitDATA)!=0; +} + + +bool IRAM_ATTR IECBusHandler::readPinRESET() +{ + if( m_pinRESET==0xFF ) return true; + return digitalReadFastExt(m_pinRESET, m_regRESETread, m_bitRESET)!=0; +} + + +bool IECBusHandler::waitTimeout(uint16_t timeout, uint8_t cond) +{ + // This function may be called in code where interrupts are disabled. + // Calling micros() when interrupts are disabled does not work on all + // platforms, some return incorrect values, others may re-enable interrupts + // So we use our high-precision timer. However, on some platforms that timer + // can only count up to 127 microseconds so we have to go in small increments. + + timer_init(); + timer_reset(); + timer_start(); + while( true ) + { + switch( cond ) + { + case TC_DATA_LOW: + if( readPinDATA() == LOW ) return true; + break; + + case TC_DATA_HIGH: + if( readPinDATA() == HIGH ) return true; + break; + + case TC_CLK_LOW: + if( readPinCLK() == LOW ) return true; + break; + + case TC_CLK_HIGH: + if( readPinCLK() == HIGH ) return true; + break; + } + + if( ((m_flags & P_ATN)!=0) == readPinATN() ) + { + // ATN changed state => abort with FALSE + return false; + } + else if( timeout<100 ) + { + if( !timer_less_than(timeout) ) + { + // timeout has expired => if there was no condition to wait for + // then return TRUE, otherwise return FALSE (because the condition was not met) + return cond==TC_NONE; + } + } + else if( !timer_less_than(100) ) + { + // subtracting from the timout value like below is not 100% precise (we may wait + // a few microseconds too long because the timer may already have counter further) + // but this function is not meant for SUPER timing-critical code so that's ok. + timer_reset(); + timeout -= 100; + } + } +} + + +void IECBusHandler::waitPinATN(bool state) +{ +#ifdef ESP_PLATFORM + // waiting indefinitely with interrupts disabled on ESP32 is bad because + // the interrupt WDT will reboot the system if we wait too long + // => if interrupts are disabled then briefly enable them before the timeout to "feed" the WDT + uint64_t t = esp_timer_get_time(); + while( readPinATN()!=state ) + { + if( !haveInterrupts && (esp_timer_get_time()-t)>IWDT_FEED_TIME ) + { + interrupts(); noInterrupts(); + t = esp_timer_get_time(); + } + } +#else + while( readPinATN()!=state ); +#endif +} + + +bool IECBusHandler::waitPinDATA(bool state, uint16_t timeout) +{ + // (if timeout is not given it defaults to 1000us) + // if ATN changes (i.e. our internal ATN state no longer matches the ATN signal line) + // or the timeout is met then exit with error condition + if( timeout==0 ) + { +#ifdef ESP_PLATFORM + // waiting indefinitely with interrupts disabled on ESP32 is bad because + // the interrupt WDT will reboot the system if we wait too long + // => if interrupts are disabled then briefly enable them before the timeout to "feed" the WDT + uint64_t t = esp_timer_get_time(); + while( readPinDATA()!=state ) + { + if( ((m_flags & P_ATN)!=0) == readPinATN() ) + return false; + else if( !haveInterrupts && (esp_timer_get_time()-t)>IWDT_FEED_TIME ) + { + interrupts(); noInterrupts(); + t = esp_timer_get_time(); + } + } +#else + // timeout is 0 (no timeout) + while( readPinDATA()!=state ) + if( ((m_flags & P_ATN)!=0) == readPinATN() ) + return false; +#endif + } + else + { + // if waitTimeout for the given condition fails then exit + if( !waitTimeout(timeout, state ? TC_DATA_HIGH : TC_DATA_LOW) ) return false; + } + + // DATA LOW can only be properly detected if ATN went HIGH->LOW + // (m_flags&ATN)==0 and readPinATN()==0) + // since other devices may have pulled DATA LOW + return state || (m_flags & P_ATN) || readPinATN(); +} + + +bool IECBusHandler::waitPinCLK(bool state, uint16_t timeout) +{ + // (if timeout is not given it defaults to 1000us) + // if ATN changes (i.e. our internal ATN state no longer matches the ATN signal line) + // or the timeout is met then exit with error condition + if( timeout==0 ) + { +#ifdef ESP_PLATFORM + // waiting indefinitely with interrupts disabled on ESP32 is bad because + // the interrupt WDT will reboot the system if we wait too long + // => if interrupts are disabled then briefly enable them before the timeout to "feed" the WDT + uint64_t t = esp_timer_get_time(); + while( readPinCLK()!=state ) + { + if( ((m_flags & P_ATN)!=0) == readPinATN() ) + return false; + else if( !haveInterrupts && (esp_timer_get_time()-t)>IWDT_FEED_TIME ) + { + interrupts(); noInterrupts(); + t = esp_timer_get_time(); + } + } +#else + // timeout is 0 (no timeout) + while( readPinCLK()!=state ) + if( ((m_flags & P_ATN)!=0) == readPinATN() ) + return false; +#endif + } + else + { + // if waitTimeout for the given condition fails then exit + if( !waitTimeout(timeout, state ? TC_CLK_HIGH : TC_CLK_LOW) ) return false; + } + + return true; +} + + +void IECBusHandler::sendSRQ() +{ + if( m_pinSRQ!=0xFF ) + { + digitalWrite(m_pinSRQ, LOW); + pinMode(m_pinSRQ, OUTPUT); + delayMicrosecondsISafe(1); + pinMode(m_pinSRQ, INPUT); + } +} + + +IECBusHandler::IECBusHandler(uint8_t pinATN, uint8_t pinCLK, uint8_t pinDATA, uint8_t pinRESET, uint8_t pinCTRL, uint8_t pinSRQ) +#if defined(SUPPORT_DOLPHIN) +#if defined(SUPPORT_DOLPHIN_XRA1405) +#if defined(ESP_PLATFORM) + // ESP32 +: m_pinDolphinSCK(18), + m_pinDolphinCOPI(23), + m_pinDolphinCIPO(19), + m_pinDolphinCS(22), + m_pinDolphinHandshakeTransmit(4), + m_pinDolphinHandshakeReceive(36) +#elif defined(ARDUINO_ARCH_RP2040) + // Raspberry Pi Pico +: m_pinDolphinCS(20), + m_pinDolphinCIPO(16), + m_pinDolphinCOPI(19), + m_pinDolphinSCK(18), + m_pinDolphinHandshakeTransmit(6), + m_pinDolphinHandshakeReceive(15) +#elif defined(__AVR_ATmega328P__) || defined(ARDUINO_UNOR4) + // Arduino UNO, Pro Mini, Micro, Nano +: m_pinDolphinCS(9), + m_pinDolphinCIPO(12), + m_pinDolphinCOPI(11), + m_pinDolphinSCK(13), + m_pinDolphinHandshakeTransmit(7), + m_pinDolphinHandshakeReceive(2) +#else +#error "DolphinDos using XRA1405 not supported on this platform" +#endif +#else // !SUPPORT_DOLPHIN_XRA1405 +#if defined(ESP_PLATFORM) + // ESP32 +: m_pinDolphinHandshakeTransmit(4), + m_pinDolphinHandshakeReceive(36), + m_pinDolphinParallel{13,14,15,16,17,25,26,27} +#elif defined(ARDUINO_ARCH_RP2040) + // Raspberry Pi Pico +: m_pinDolphinHandshakeTransmit(6), + m_pinDolphinHandshakeReceive(15), + m_pinDolphinParallel{7,8,9,10,11,12,13,14} +#elif defined(__SAM3X8E__) + // Arduino Due +: m_pinDolphinHandshakeTransmit(52), + m_pinDolphinHandshakeReceive(53), + m_pinDolphinParallel{51,50,49,48,47,46,45,44} +#elif defined(__AVR_ATmega328P__) || defined(ARDUINO_UNOR4) + // Arduino UNO, Pro Mini, Nano +: m_pinDolphinHandshakeTransmit(7), + m_pinDolphinHandshakeReceive(2), + m_pinDolphinParallel{A0,A1,A2,A3,A4,A5,8,9} +#elif defined(__AVR_ATmega2560__) + // Arduino Mega 2560 +: m_pinDolphinHandshakeTransmit(30), + m_pinDolphinHandshakeReceive(2), + m_pinDolphinParallel{22,23,24,25,26,27,28,29} +#else +#error "DolphinDos not supported on this platform" +#endif +#endif // SUPPORT_DOLPHIN_XRA1405 +#endif // SUPPORT_DOLPHIN +{ + m_numDevices = 0; + m_inTask = false; + m_flags = 0xFF; // 0xFF means: begin() has not yet been called + + m_pinATN = pinATN; + m_pinCLK = pinCLK; + m_pinDATA = pinDATA; + m_pinRESET = pinRESET; + m_pinCTRL = pinCTRL; + m_pinSRQ = pinSRQ; + +#if defined(SUPPORT_JIFFY) || defined(SUPPORT_EPYX) || defined(SUPPORT_DOLPHIN) +#if IEC_DEFAULT_FASTLOAD_BUFFER_SIZE>0 + m_bufferSize = IEC_DEFAULT_FASTLOAD_BUFFER_SIZE; +#else + m_buffer = NULL; + m_bufferSize = 0; +#endif +#endif + +#ifdef IOREG_TYPE + m_bitRESET = digitalPinToBitMask(pinRESET); + m_regRESETread = portInputRegister(digitalPinToPort(pinRESET)); + m_bitATN = digitalPinToBitMask(pinATN); + m_regATNread = portInputRegister(digitalPinToPort(pinATN)); + m_bitCLK = digitalPinToBitMask(pinCLK); + m_regCLKread = portInputRegister(digitalPinToPort(pinCLK)); + m_regCLKwrite = portOutputRegister(digitalPinToPort(pinCLK)); + m_regCLKmode = portModeRegister(digitalPinToPort(pinCLK)); + m_bitDATA = digitalPinToBitMask(pinDATA); + m_regDATAread = portInputRegister(digitalPinToPort(pinDATA)); + m_regDATAwrite = portOutputRegister(digitalPinToPort(pinDATA)); + m_regDATAmode = portModeRegister(digitalPinToPort(pinDATA)); +#endif + + m_atnInterrupt = digitalPinToInterrupt(m_pinATN); +} + + +void IECBusHandler::begin() +{ + JDEBUGI(); + + // set pins to output 0 (when in output mode) + pinMode(m_pinCLK, OUTPUT); digitalWrite(m_pinCLK, LOW); + pinMode(m_pinDATA, OUTPUT); digitalWrite(m_pinDATA, LOW); + + pinMode(m_pinATN, INPUT); + pinMode(m_pinCLK, INPUT); + pinMode(m_pinDATA, INPUT); + if( m_pinCTRL<0xFF ) pinMode(m_pinCTRL, OUTPUT); + if( m_pinRESET<0xFF ) pinMode(m_pinRESET, INPUT); + if( m_pinSRQ<0xFF ) pinMode(m_pinSRQ, INPUT); + m_flags = 0; + + // allow ATN to pull DATA low in hardware + writePinCTRL(LOW); + + // if the ATN pin is capable of interrupts then use interrupts to detect + // ATN requests, otherwise we'll poll the ATN pin in function microTask(). + if( m_atnInterrupt!=NOT_AN_INTERRUPT && s_bushandler==NULL ) + { + s_bushandler = this; + attachInterrupt(m_atnInterrupt, atnInterruptFcn, FALLING); + } + + // call begin() function for all attached devices + for(uint8_t i=0; ibegin(); +} + + +bool IECBusHandler::canServeATN() +{ + return (m_pinCTRL!=0xFF) || (m_atnInterrupt != NOT_AN_INTERRUPT); +} + + +bool IECBusHandler::inTransaction() +{ + return (m_flags & (P_LISTENING|P_TALKING))!=0; +} + + +bool IECBusHandler::attachDevice(IECDevice *dev) +{ + if( m_numDevicesm_devnr, true)==NULL ) + { + dev->m_handler = this; + dev->m_sflags &= ~(S_JIFFY_DETECTED|S_JIFFY_BLOCK|S_DOLPHIN_DETECTED|S_DOLPHIN_BURST_TRANSMIT|S_DOLPHIN_BURST_RECEIVE|S_EPYX_HEADER|S_EPYX_LOAD|S_EPYX_SECTOROP); +#ifdef SUPPORT_DOLPHIN + enableParallelPins(); +#endif + + // if IECBusHandler::begin() has been called already then call the device's + // begin() function now, otherwise it will be called in IECBusHandler::begin() + if( m_flags!=0xFF ) dev->begin(); + + m_devices[m_numDevices] = dev; + m_numDevices++; + return true; + } + else + return false; +} + + +bool IECBusHandler::detachDevice(IECDevice *dev) +{ + for(uint8_t i=0; im_handler = NULL; + m_devices[i] = m_devices[m_numDevices-1]; + m_numDevices--; +#ifdef SUPPORT_DOLPHIN + enableParallelPins(); +#endif + return true; + } + + return false; +} + + +IECDevice *IECBusHandler::findDevice(uint8_t devnr, bool includeInactive) +{ + for(uint8_t i=0; im_devnr && (includeInactive || m_devices[i]->isActive()) ) + return m_devices[i]; + + return NULL; +} + + +void IRAM_ATTR IECBusHandler::atnInterruptFcn(INTERRUPT_FCN_ARG) +{ + if( s_bushandler!=NULL && !s_bushandler->m_inTask & ((s_bushandler->m_flags & P_ATN)==0) ) + s_bushandler->atnRequest(); +} + + +#if (defined(SUPPORT_JIFFY) || defined(SUPPORT_DOLPHIN) || defined(SUPPORT_EPYX)) && !defined(IEC_DEFAULT_FASTLOAD_BUFFER_SIZE) +void IECBusHandler::setBuffer(uint8_t *buffer, uint8_t bufferSize) +{ + m_buffer = bufferSize>0 ? buffer : NULL; + m_bufferSize = bufferSize; +} +#endif + +#ifdef SUPPORT_JIFFY + +// ------------------------------------ JiffyDos support routines ------------------------------------ + + +bool IECBusHandler::enableJiffyDosSupport(IECDevice *dev, bool enable) +{ + if( enable && m_bufferSize>0 ) + dev->m_sflags |= S_JIFFY_ENABLED; + else + dev->m_sflags &= ~S_JIFFY_ENABLED; + + // cancel any current JiffyDos activities + dev->m_sflags &= ~(S_JIFFY_DETECTED|S_JIFFY_BLOCK); + + return (dev->m_sflags & S_JIFFY_ENABLED)!=0; +} + + +bool IRAM_ATTR IECBusHandler::receiveJiffyByte(bool canWriteOk) +{ + uint8_t data = 0; + JDEBUG1(); + timer_init(); + timer_reset(); + + noInterrupts(); + + // signal "ready" by releasing DATA + writePinDATA(HIGH); + + // wait (indefinitely) for either CLK high ("ready-to-send") or ATN low + // NOTE: this must be in a blocking loop since the sender starts transmitting + // the byte immediately after setting CLK high. If we exit the "task" function then + // we may not get back here in time to receive. +#ifdef ESP_PLATFORM + while( !digitalReadFastExt(m_pinCLK, m_regCLKread, m_bitCLK) && digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN) ) + if( !timer_less_than(IWDT_FEED_TIME) ) + { + // briefly enable interrupts to "feed" the WDT, otherwise we'll get re-booted + interrupts(); noInterrupts(); + timer_reset(); + } +#else + while( !digitalReadFastExt(m_pinCLK, m_regCLKread, m_bitCLK) && digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN) ); +#endif + + // start timer (on AVR, lag from CLK high to timer start is between 700...1700ns) + timer_start(); + JDEBUG0(); + + // abort if ATN low + if( !readPinATN() ) + { interrupts(); return false; } + + // bits 4+5 are set by sender 11 cycles after CLK HIGH (FC51) + // wait until 14us after CLK + timer_wait_until(14); + + JDEBUG1(); + if( !readPinCLK() ) data |= bit(4); + if( !readPinDATA() ) data |= bit(5); + JDEBUG0(); + + // bits 6+7 are set by sender 24 cycles after CLK HIGH (FC5A) + // wait until 27us after CLK + timer_wait_until(27); + + JDEBUG1(); + if( !readPinCLK() ) data |= bit(6); + if( !readPinDATA() ) data |= bit(7); + JDEBUG0(); + + // bits 3+1 are set by sender 35 cycles after CLK HIGH (FC62) + // wait until 38us after CLK + timer_wait_until(38); + + JDEBUG1(); + if( !readPinCLK() ) data |= bit(3); + if( !readPinDATA() ) data |= bit(1); + JDEBUG0(); + + // bits 2+0 are set by sender 48 cycles after CLK HIGH (FC6B) + // wait until 51us after CLK + timer_wait_until(51); + + JDEBUG1(); + if( !readPinCLK() ) data |= bit(2); + if( !readPinDATA() ) data |= bit(0); + JDEBUG0(); + + // sender sets EOI status 61 cycles after CLK HIGH (FC76) + // wait until 64us after CLK + timer_wait_until(64); + + // if CLK is high at this point then the sender is signaling EOI + JDEBUG1(); + bool eoi = readPinCLK(); + + // acknowledge receipt + writePinDATA(LOW); + + // sender reads acknowledgement 80 cycles after CLK HIGH (FC82) + // wait until 83us after CLK + timer_wait_until(83); + + JDEBUG0(); + + interrupts(); + + if( canWriteOk ) + { + // pass received data on to the device + m_currentDevice->write(data, eoi); + } + else + { + // canWrite() reported an error + return false; + } + + return true; +} + + +bool IRAM_ATTR IECBusHandler::transmitJiffyByte(uint8_t numData) +{ + uint8_t data = numData>0 ? m_currentDevice->peek() : 0; + + JDEBUG1(); + timer_init(); + timer_reset(); + + noInterrupts(); + + // signal "READY" by releasing CLK + writePinCLK(HIGH); + + // wait (indefinitely) for either DATA high ("ready-to-receive", FBCB) or ATN low + // NOTE: this must be in a blocking loop since the receiver receives the data + // immediately after setting DATA high. If we exit the "task" function then + // we may not get back here in time to transmit. +#ifdef ESP_PLATFORM + while( !digitalReadFastExt(m_pinDATA, m_regDATAread, m_bitDATA) && digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN) ) + if( !timer_less_than(IWDT_FEED_TIME) ) + { + // briefly enable interrupts to "feed" the WDT, otherwise we'll get re-booted + interrupts(); noInterrupts(); + timer_reset(); + } +#else + while( !digitalReadFastExt(m_pinDATA, m_regDATAread, m_bitDATA) && digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN) ); +#endif + + // start timer (on AVR, lag from DATA high to timer start is between 700...1700ns) + timer_start(); + JDEBUG0(); + + // abort if ATN low + if( !readPinATN() ) + { interrupts(); return false; } + + writePinCLK(data & bit(0)); + writePinDATA(data & bit(1)); + JDEBUG1(); + // bits 0+1 are read by receiver 16 cycles after DATA HIGH (FBD5) + + // wait until 16.5 us after DATA + timer_wait_until(16.5); + + JDEBUG0(); + writePinCLK(data & bit(2)); + writePinDATA(data & bit(3)); + JDEBUG1(); + // bits 2+3 are read by receiver 26 cycles after DATA HIGH (FBDB) + + // wait until 27.5 us after DATA + timer_wait_until(27.5); + + JDEBUG0(); + writePinCLK(data & bit(4)); + writePinDATA(data & bit(5)); + JDEBUG1(); + // bits 4+5 are read by receiver 37 cycles after DATA HIGH (FBE2) + + // wait until 39 us after DATA + timer_wait_until(39); + + JDEBUG0(); + writePinCLK(data & bit(6)); + writePinDATA(data & bit(7)); + JDEBUG1(); + // bits 6+7 are read by receiver 48 cycles after DATA HIGH (FBE9) + + // wait until 50 us after DATA + timer_wait_until(50); + JDEBUG0(); + + // numData: + // 0: no data was available to read (error condition, discard this byte) + // 1: this was the last byte of data + // >1: more data is available after this + if( numData>1 ) + { + // CLK=LOW and DATA=HIGH means "at least one more byte" + writePinCLK(LOW); + writePinDATA(HIGH); + } + else + { + // CLK=HIGH and DATA=LOW means EOI (this was the last byte) + // CLK=HIGH and DATA=HIGH means "error" + writePinCLK(HIGH); + writePinDATA(numData==0); + } + + // EOI/error status is read by receiver 59 cycles after DATA HIGH (FBEF) + // receiver sets DATA low 63 cycles after initial DATA HIGH (FBF2) + timer_wait_until(60); + + // receiver signals "done" by pulling DATA low (FBF2) + JDEBUG1(); + if( !waitPinDATA(LOW) ) { interrupts(); return false; } + JDEBUG0(); + + interrupts(); + + if( numData>0 ) + { + // success => discard transmitted byte (was previously read via peek()) + m_currentDevice->read(); + return true; + } + else + return false; +} + + +bool IRAM_ATTR IECBusHandler::transmitJiffyBlock(uint8_t *buffer, uint8_t numBytes) +{ + JDEBUG1(); + timer_init(); + + // wait (indefinitely) until receiver is not holding DATA low anymore (FB07) + // NOTE: this must be in a blocking loop since the receiver starts counting + // up the EOI timeout immediately after setting DATA HIGH. If we had exited the + // "task" function then it might be more than 200us until we get back here + // to pull CLK low and the receiver will interpret that delay as EOI. + while( !readPinDATA() ) + if( !readPinATN() ) + { JDEBUG0(); return false; } + + // receiver will be in "new data block" state at this point, + // waiting for us (FB0C) to release CLK + if( numBytes==0 ) + { + // nothing to send => signal EOI by keeping DATA high + // and pulsing CLK high-low + writePinDATA(HIGH); + writePinCLK(HIGH); + if( !waitTimeout(100) ) return false; + writePinCLK(LOW); + if( !waitTimeout(100) ) return false; + JDEBUG0(); + return false; + } + + // signal "ready to send" by pulling DATA low and releasing CLK + writePinDATA(LOW); + writePinCLK(HIGH); + + // delay to make sure receiver has seen DATA=LOW - even though receiver + // is in a tight loop (at FB0C), a VIC "bad line" may steal 40-50us. + if( !waitTimeout(60) ) return false; + + noInterrupts(); + + for(uint8_t i=0; i= ESP_IDF_VERSION_VAL(5, 0, 0)) + +#include +pcnt_unit_handle_t esp32_pulse_count_unit = NULL; +pcnt_channel_handle_t esp32_pulse_count_channel = NULL; +volatile static bool _handshakeReceived = false; +static bool handshakeIRQ(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx) { _handshakeReceived = true; return false; } +#define DOLPHIN_HANDSHAKE_USE_INTERRUPT + +#elif !defined(__AVR_ATmega328P__) && !defined(__AVR_ATmega2560__) + +volatile static bool _handshakeReceived = false; +static void IRAM_ATTR handshakeIRQ(INTERRUPT_FCN_ARG) { _handshakeReceived = true; } +#define DOLPHIN_HANDSHAKE_USE_INTERRUPT + +#endif + + +#ifdef DOLPHIN_HANDSHAKE_USE_INTERRUPT + +bool IRAM_ATTR IECBusHandler::parallelCableDetect() +{ + // DolphinDos cable detection happens at the end of the ATN sequence + // during which interrupts are disabled so we can't use parallelBusHandshakeReceived() + // which relies on the IRQ handler above + +#if defined(IOREG_TYPE) + volatile const IOREG_TYPE *regHandshakeReceive = portInputRegister(digitalPinToPort(m_pinDolphinHandshakeReceive)); + volatile IOREG_TYPE bitHandshakeReceive = digitalPinToBitMask(m_pinDolphinHandshakeReceive); +#endif + + // wait for handshake signal going LOW until ATN goes high + while( !digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN) ) + { + if( !digitalReadFastExt(m_pinDolphinHandshakeReceive, regHandshakeReceive, bitHandshakeReceive) ) return true; + if( !digitalReadFastExt(m_pinDolphinHandshakeReceive, regHandshakeReceive, bitHandshakeReceive) ) return true; + if( !digitalReadFastExt(m_pinDolphinHandshakeReceive, regHandshakeReceive, bitHandshakeReceive) ) return true; + } + + return false; +} + +#else + +bool IRAM_ATTR IECBusHandler::parallelCableDetect() +{ + // clear any previous handshakes + parallelBusHandshakeReceived(); + + // wait for handshake + while( !readPinATN() ) + if( parallelBusHandshakeReceived() ) + return true; + + return false; +} + + +#endif + +#ifdef SUPPORT_DOLPHIN_XRA1405 + +void IECBusHandler::setDolphinDosPins(uint8_t pinHT, uint8_t pinHR, uint8_t pinSCK, uint8_t pinCOPI, uint8_t pinCIPO, uint8_t pinCS) +{ + m_pinDolphinHandshakeTransmit = pinHT; + m_pinDolphinHandshakeReceive = pinHR; + m_pinDolphinCOPI = pinCOPI; + m_pinDolphinCIPO = pinCIPO; + m_pinDolphinSCK = pinSCK; + m_pinDolphinCS = pinCS; +} + +#else + +void IECBusHandler::setDolphinDosPins(uint8_t pinHT, uint8_t pinHR,uint8_t pinD0, uint8_t pinD1, uint8_t pinD2, uint8_t pinD3, uint8_t pinD4, uint8_t pinD5, uint8_t pinD6, uint8_t pinD7) +{ + m_pinDolphinHandshakeTransmit = pinHT; + m_pinDolphinHandshakeReceive = pinHR; + m_pinDolphinParallel[0] = pinD0; + m_pinDolphinParallel[1] = pinD1; + m_pinDolphinParallel[2] = pinD2; + m_pinDolphinParallel[3] = pinD3; + m_pinDolphinParallel[4] = pinD4; + m_pinDolphinParallel[5] = pinD5; + m_pinDolphinParallel[6] = pinD6; + m_pinDolphinParallel[7] = pinD7; +} + +#endif + +bool IECBusHandler::enableDolphinDosSupport(IECDevice *dev, bool enable) +{ + if( enable && m_bufferSize>=DOLPHIN_PREBUFFER_BYTES && + !isDolphinPin(m_pinATN) && !isDolphinPin(m_pinCLK) && !isDolphinPin(m_pinDATA) && + !isDolphinPin(m_pinRESET) && !isDolphinPin(m_pinCTRL) && +#ifdef SUPPORT_DOLPHIN_XRA1405 + m_pinDolphinCS!=0xFF && m_pinDolphinSCK!=0xFF && m_pinDolphinCOPI!=0xFF && m_pinDolphinCIPO!=0xFF && +#else + m_pinDolphinParallel[0]!=0xFF && m_pinDolphinParallel[1]!=0xFF && + m_pinDolphinParallel[2]!=0xFF && m_pinDolphinParallel[3]!=0xFF && + m_pinDolphinParallel[4]!=0xFF && m_pinDolphinParallel[5]!=0xFF && + m_pinDolphinParallel[6]!=0xFF && m_pinDolphinParallel[6]!=0xFF && +#endif + m_pinDolphinHandshakeTransmit!=0xFF && m_pinDolphinHandshakeReceive!=0xFF && + digitalPinToInterrupt(m_pinDolphinHandshakeReceive)!=NOT_AN_INTERRUPT ) + { + dev->m_sflags |= S_DOLPHIN_ENABLED|S_DOLPHIN_BURST_ENABLED; + } + else + dev->m_sflags &= ~(S_DOLPHIN_ENABLED|S_DOLPHIN_BURST_ENABLED); + + // cancel any current DolphinDos activities + dev->m_sflags &= ~(S_DOLPHIN_DETECTED|S_DOLPHIN_BURST_TRANSMIT|S_DOLPHIN_BURST_RECEIVE); + + // make sure pins for parallel cable are enabled/disabled as needed + enableParallelPins(); + + return (dev->m_sflags & S_DOLPHIN_ENABLED)!=0; +} + + +bool IECBusHandler::isDolphinPin(uint8_t pin) +{ + if( pin==m_pinDolphinHandshakeTransmit || pin==m_pinDolphinHandshakeReceive ) + return true; + +#ifdef SUPPORT_DOLPHIN_XRA1405 + if( pin==m_pinDolphinCS || pin==m_pinDolphinCOPI || pin==m_pinDolphinCIPO || pin==m_pinDolphinSCK ) + return true; +#else + for(int i=0; i<8; i++) + if( pin==m_pinDolphinParallel[i] ) + return true; +#endif + + return false; +} + + +void IECBusHandler::enableParallelPins() +{ + uint8_t i = 0; + for(i=0; im_sflags & S_DOLPHIN_ENABLED ) + break; + + if( i= ESP_IDF_VERSION_VAL(5, 0, 0)) + // use pulse counter on handshake receive line (utilizing its glitch filter) + if( esp32_pulse_count_unit==NULL ) + { + pcnt_unit_config_t unit_config = {.low_limit = -1, .high_limit = 1, .flags = 0}; + ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &esp32_pulse_count_unit)); + pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = 250 }; + ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(esp32_pulse_count_unit, &filter_config)); + pcnt_chan_config_t chan_config; + memset(&chan_config, 0, sizeof(pcnt_chan_config_t)); + chan_config.edge_gpio_num = m_pinDolphinHandshakeReceive; + chan_config.level_gpio_num = -1; + ESP_ERROR_CHECK(pcnt_new_channel(esp32_pulse_count_unit, &chan_config, &esp32_pulse_count_channel)); + ESP_ERROR_CHECK(pcnt_channel_set_edge_action(esp32_pulse_count_channel, PCNT_CHANNEL_EDGE_ACTION_HOLD, PCNT_CHANNEL_EDGE_ACTION_INCREASE)); + pcnt_event_callbacks_t cbs = { .on_reach = handshakeIRQ }; + ESP_ERROR_CHECK(pcnt_unit_add_watch_point(esp32_pulse_count_unit, 1)); + ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(esp32_pulse_count_unit, &cbs, NULL)); + ESP_ERROR_CHECK(pcnt_unit_enable(esp32_pulse_count_unit)); + ESP_ERROR_CHECK(pcnt_unit_clear_count(esp32_pulse_count_unit)); + ESP_ERROR_CHECK(pcnt_unit_start(esp32_pulse_count_unit)); + } +#elif defined(DOLPHIN_HANDSHAKE_USE_INTERRUPT) + attachInterrupt(digitalPinToInterrupt(m_pinDolphinHandshakeReceive), handshakeIRQ, FALLING); +#endif + + // initialize parallel bus pins +#ifdef SUPPORT_DOLPHIN_XRA1405 + digitalWrite(m_pinDolphinCS, HIGH); + pinMode(m_pinDolphinCS, OUTPUT); + digitalWrite(m_pinDolphinCS, HIGH); +#if defined(ESP_PLATFORM) && !defined(ARDUINO) + // for ESP32 ESPIDF, SPI settings are specified in "begin()" instead of "beginTransaction()" + // (we use 16MHz since at 26MHz we run into timing issues during receive, the frequency + // does not matter too much since we only send 16 bits of data at a time) + SPI.begin(m_pinDolphinSCK, m_pinDolphinCIPO, m_pinDolphinCOPI, SPISettings(16000000, MSBFIRST, SPI_MODE0)); +#elif defined(ESP_PLATFORM) && defined(ARDUINO) + // SPI for ESP32 under Arduino requires pin assignments in "begin" call + SPI.begin(m_pinDolphinSCK, m_pinDolphinCIPO, m_pinDolphinCOPI); +#else + SPI.begin(); +#endif + setParallelBusModeInput(); + m_inTransaction = 0; +#else + for(int i=0; i<8; i++) pinMode(m_pinDolphinParallel[i], INPUT); +#endif + } + else + { +#if defined(ESP_PLATFORM) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) + // disable pulse counter on handshake receive line + if( esp32_pulse_count_unit!=NULL ) + { + pcnt_unit_stop(esp32_pulse_count_unit); + pcnt_unit_disable(esp32_pulse_count_unit); + pcnt_del_channel(esp32_pulse_count_channel); + pcnt_del_unit(esp32_pulse_count_unit); + esp32_pulse_count_unit = NULL; + esp32_pulse_count_channel = NULL; + } +#elif defined(DOLPHIN_HANDSHAKE_USE_INTERRUPT) + detachInterrupt(digitalPinToInterrupt(m_pinDolphinHandshakeReceive)); +#endif + } +} + + +bool IRAM_ATTR IECBusHandler::parallelBusHandshakeReceived() +{ +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega2560__) + // see comment in function enableDolphinDosSupport + if( EIFR & m_handshakeReceivedBit ) + { + EIFR |= m_handshakeReceivedBit; + return true; + } + else + return false; +#else + if( _handshakeReceived ) + { + _handshakeReceived = false; + return true; + } + else + return false; +#endif +} + + +void IRAM_ATTR IECBusHandler::parallelBusHandshakeTransmit() +{ + // Emulate open collector behavior: + // - switch pin to INPUT mode (high-Z output) for true + // - switch pun to OUTPUT mode (LOW output) for false + pinModeFastExt(m_pinDolphinHandshakeTransmit, m_regDolphinHandshakeTransmitMode, m_bitDolphinHandshakeTransmit, OUTPUT); + delayMicrosecondsISafe(1); + pinModeFastExt(m_pinDolphinHandshakeTransmit, m_regDolphinHandshakeTransmitMode, m_bitDolphinHandshakeTransmit, INPUT); +} + + +void IRAM_ATTR IECBusHandler::startParallelTransaction() +{ +#ifdef SUPPORT_DOLPHIN_XRA1405 + if( m_inTransaction==0 ) + { +#if defined(ESP_PLATFORM) && !defined(ARDUINO) + // for ESPIDF, SPI settings are specified in "begin()" instead of "beginTransaction()" + SPI.beginTransaction(); +#else + SPI.beginTransaction(SPISettings(16000000, MSBFIRST, SPI_MODE0)); +#endif + } + + m_inTransaction++; +#endif +} + + +void IRAM_ATTR IECBusHandler::endParallelTransaction() +{ +#ifdef SUPPORT_DOLPHIN_XRA1405 + if( m_inTransaction==1 ) SPI.endTransaction(); + if( m_inTransaction>0 ) m_inTransaction--; +#endif +} + + +#pragma GCC push_options +#pragma GCC optimize ("O2") +uint8_t IRAM_ATTR IECBusHandler::readParallelData() +{ + uint8_t res = 0; +#ifdef SUPPORT_DOLPHIN_XRA1405 + res = XRA1405_ReadReg(0x00); // GSR1, GPIO State Register for P0-P7 +#else + // loop unrolled for performance + if( digitalReadFastExt(m_pinDolphinParallel[0], m_regDolphinParallelRead[0], m_bitDolphinParallel[0]) ) res |= 0x01; + if( digitalReadFastExt(m_pinDolphinParallel[1], m_regDolphinParallelRead[1], m_bitDolphinParallel[1]) ) res |= 0x02; + if( digitalReadFastExt(m_pinDolphinParallel[2], m_regDolphinParallelRead[2], m_bitDolphinParallel[2]) ) res |= 0x04; + if( digitalReadFastExt(m_pinDolphinParallel[3], m_regDolphinParallelRead[3], m_bitDolphinParallel[3]) ) res |= 0x08; + if( digitalReadFastExt(m_pinDolphinParallel[4], m_regDolphinParallelRead[4], m_bitDolphinParallel[4]) ) res |= 0x10; + if( digitalReadFastExt(m_pinDolphinParallel[5], m_regDolphinParallelRead[5], m_bitDolphinParallel[5]) ) res |= 0x20; + if( digitalReadFastExt(m_pinDolphinParallel[6], m_regDolphinParallelRead[6], m_bitDolphinParallel[6]) ) res |= 0x40; + if( digitalReadFastExt(m_pinDolphinParallel[7], m_regDolphinParallelRead[7], m_bitDolphinParallel[7]) ) res |= 0x80; +#endif + return res; +} + + +void IRAM_ATTR IECBusHandler::writeParallelData(uint8_t data) +{ +#ifdef SUPPORT_DOLPHIN_XRA1405 + XRA1405_WriteReg(0x02, data); // OCR1, GPIO Output Control Register for P0-P7 +#else + // loop unrolled for performance + digitalWriteFastExt(m_pinDolphinParallel[0], m_regDolphinParallelWrite[0], m_bitDolphinParallel[0], data & 0x01); + digitalWriteFastExt(m_pinDolphinParallel[1], m_regDolphinParallelWrite[1], m_bitDolphinParallel[1], data & 0x02); + digitalWriteFastExt(m_pinDolphinParallel[2], m_regDolphinParallelWrite[2], m_bitDolphinParallel[2], data & 0x04); + digitalWriteFastExt(m_pinDolphinParallel[3], m_regDolphinParallelWrite[3], m_bitDolphinParallel[3], data & 0x08); + digitalWriteFastExt(m_pinDolphinParallel[4], m_regDolphinParallelWrite[4], m_bitDolphinParallel[4], data & 0x10); + digitalWriteFastExt(m_pinDolphinParallel[5], m_regDolphinParallelWrite[5], m_bitDolphinParallel[5], data & 0x20); + digitalWriteFastExt(m_pinDolphinParallel[6], m_regDolphinParallelWrite[6], m_bitDolphinParallel[6], data & 0x40); + digitalWriteFastExt(m_pinDolphinParallel[7], m_regDolphinParallelWrite[7], m_bitDolphinParallel[7], data & 0x80); +#endif +} + + +void IRAM_ATTR IECBusHandler::setParallelBusModeInput() +{ +#ifdef SUPPORT_DOLPHIN_XRA1405 + XRA1405_WriteReg(0x06, 0xFF); // GCR1, GPIO Configuration Register for P0-P7 +#else + // set parallel bus data pins to input mode + for(int i=0; i<8; i++) + pinModeFastExt(m_pinDolphinParallel[i], m_regDolphinParallelMode[i], m_bitDolphinParallel[i], INPUT); +#endif +} + + +void IRAM_ATTR IECBusHandler::setParallelBusModeOutput() +{ +#ifdef SUPPORT_DOLPHIN_XRA1405 + XRA1405_WriteReg(0x06, 0x00); // GCR1, GPIO Configuration Register for P0-P7 +#else + // set parallel bus data pins to output mode + for(int i=0; i<8; i++) + pinModeFastExt(m_pinDolphinParallel[i], m_regDolphinParallelMode[i], m_bitDolphinParallel[i], OUTPUT); +#endif +} +#pragma GCC pop_options + + +bool IECBusHandler::waitParallelBusHandshakeReceived() +{ + while( !parallelBusHandshakeReceived() ) + if( !readPinATN() ) + return false; + + return true; +} + + +void IECBusHandler::enableDolphinBurstMode(IECDevice *dev, bool enable) +{ + if( enable ) + dev->m_sflags |= S_DOLPHIN_BURST_ENABLED; + else + dev->m_sflags &= ~S_DOLPHIN_BURST_ENABLED; + + dev->m_sflags &= ~(S_DOLPHIN_BURST_TRANSMIT|S_DOLPHIN_BURST_RECEIVE); +} + +void IECBusHandler::dolphinBurstReceiveRequest(IECDevice *dev) +{ + dev->m_sflags |= S_DOLPHIN_BURST_RECEIVE; + m_timeoutStart = micros(); +} + +void IECBusHandler::dolphinBurstTransmitRequest(IECDevice *dev) +{ + dev->m_sflags |= S_DOLPHIN_BURST_TRANSMIT; + m_timeoutStart = micros(); +} + +bool IECBusHandler::receiveDolphinByte(bool canWriteOk) +{ + // NOTE: we only get here if sender has already signaled ready-to-send + // by releasing CLK + bool eoi = false; + + // we have buffered bytes (see comment below) that need to be + // sent on to the higher level handler before we can receive more. + // There are two ways to get to m_dolphinCtr==DOLPHIN_PREBUFFER_BYTES: + // 1) the host never sends a XZ burst request and just keeps sending data + // 2) the host sends a burst request but we reject it + // note that we must wait for the host to be ready to send the next data + // byte before we can empty our buffer, otherwise we will already empty + // it before the host sends the burst (XZ) request + if( m_secondary==0x61 && m_dolphinCtr > 0 && m_dolphinCtr <= DOLPHIN_PREBUFFER_BYTES ) + { + // send next buffered byte on to higher level + m_currentDevice->write(m_buffer[m_dolphinCtr-1], false); + m_dolphinCtr--; + return true; + } + + noInterrupts(); + + // signal "ready" + writePinDATA(HIGH); + + // wait for CLK low + if( !waitPinCLK(LOW, 100) ) + { + // exit if waitPinCLK returned because of falling edge on ATN + if( !readPinATN() ) { interrupts(); return false; } + + // sender did not set CLK low within 100us after we set DATA high + // => it is signaling EOI + // acknowledge we received it by setting DATA low for 60us + eoi = true; + writePinDATA(LOW); + if( !waitTimeout(60) ) { interrupts(); return false; } + writePinDATA(HIGH); + + // keep waiting for CLK low + if( !waitPinCLK(LOW) ) { interrupts(); return false; } + } + + // get data + if( canWriteOk ) + { + // read data from parallel bus + uint8_t data = readParallelData(); + + // confirm receipt + writePinDATA(LOW); + + interrupts(); + + // when executing a SAVE command, DolphinDos first sends two bytes of data, + // and then the "XZ" burst request. If the transmission happens in burst mode then + // that data is going to be sent again and the initial data is discarded. + // (MultiDubTwo actually sends garbage bytes for the initial two bytes) + // so we can't pass the first two bytes on yet because we don't yet know if this is + // going to be a burst transmission. If it is NOT a burst then we need to send them + // later (see beginning of this function). If it is a burst then we discard them. + // Note that the SAVE command always operates on channel 1 (secondary address 0x61) + // so we only do the buffering in that case. + if( m_secondary==0x61 && m_dolphinCtr > DOLPHIN_PREBUFFER_BYTES ) + { + m_buffer[m_dolphinCtr-DOLPHIN_PREBUFFER_BYTES-1] = data; + m_dolphinCtr--; + } + else + { + // pass received data on to the device + m_currentDevice->write(data, eoi); + } + + return true; + } + else + { + // canWrite reported an error + interrupts(); + return false; + } +} + + +bool IECBusHandler::transmitDolphinByte(uint8_t numData) +{ + // Note: receiver starts a 50us timeout after setting DATA high + // (ready-to-receive) waiting for CLK low (data valid). If we take + // longer than those 50us the receiver will interpret that as EOI + // (last byte of data). So we need to take precautions: + // - disable interrupts between setting CLK high and setting CLK low + // - get the data byte to send before setting CLK high + // - wait for DATA high in a blocking loop + uint8_t data = numData>0 ? m_currentDevice->peek() : 0xFF; + + startParallelTransaction(); + + // prepare data (bus is still in INPUT mode so the data will not be visible yet) + // (doing it now saves time to meed the 50us timeout after DATA low) + writeParallelData(data); + + noInterrupts(); + + // signal "ready-to-send" (CLK=1) + writePinCLK(HIGH); + + // wait for "ready-for-data" (DATA=0) + JDEBUG1(); + if( !waitPinDATA(HIGH, 0) ) { atnRequest(); interrupts(); endParallelTransaction(); return false; } + JDEBUG0(); + + if( numData==0 ) + { + // if we have nothing to send then there was some kind of error + // aborting here will signal the error condition to the receiver + interrupts(); + endParallelTransaction(); + return false; + } + else if( numData==1 ) + { + // last data byte => keep CLK high (signals EOI) and wait for receiver to + // confirm EOI by HIGH->LOW->HIGH pulse on DATA + bool ok = (waitPinDATA(LOW) && waitPinDATA(HIGH)); + if( !ok ) { atnRequest(); interrupts(); endParallelTransaction(); return false; } + } + + // output data on parallel bus + JDEBUG1(); + setParallelBusModeOutput(); + JDEBUG0(); + + // set CLK low (signal "data ready") + writePinCLK(LOW); + + interrupts(); + endParallelTransaction(); + + // discard data byte in device (read by peek() before) + m_currentDevice->read(); + + // remember initial bytes of data sent (see comment in transmitDolphinBurst) + if( m_secondary==0x60 && m_dolphinCtr send handshake + parallelBusHandshakeTransmit(); + } + else if( m_currentDevice->write(m_buffer, n, eoi)==n ) + { + // data written successfully => send handshake + parallelBusHandshakeTransmit(); + n = 0; + } + else + { + // error while writing data => release DATA to signal error condition and exit + writePinDATA(HIGH); + return false; + } + } + + return true; +} + + +bool IECBusHandler::transmitDolphinBurst() +{ + // NOTE: we only get here if sender has already signaled ready-to-receive + // by pulling DATA low + + // send handshake to confirm burst transmission (Dolphin kernal EEDA) + parallelBusHandshakeTransmit(); + + // give the host some time to see our confirmation + // if we send the next handshake too quickly then the host will see only one, + // the host will be busy printing the load address after seeing the confirmation + // so nothing is lost by waiting a good long time before the next handshake + delayMicroseconds(1000); + + // switch parallel bus to output + setParallelBusModeOutput(); + + // when loading a file, DolphinDos switches to burst mode by sending "XQ" after + // the transmission has started. The kernal does so after the first two bytes + // were sent, MultiDubTwo after one byte. After swtiching to burst mode, the 1541 + // then re-transmits the bytes that were already sent. + for(uint8_t i=0; iread(m_buffer, m_bufferSize))>0 ) + { + startParallelTransaction(); + for(uint8_t i=0; i release bus and CLK line and return + setParallelBusModeInput(); + writePinCLK(HIGH); + endParallelTransaction(); + return false; + } + } + endParallelTransaction(); + } + + // switch parallel bus back to input + setParallelBusModeInput(); + + // after seeing our end-of-data and confirmit it, receiver waits for 2ms + // for us to send the handshake (below) => no interrupts, otherwise we may + // exceed the timeout + noInterrupts(); + + // signal end-of-data + writePinCLK(HIGH); + + // wait for receiver to confirm + if( !waitPinDATA(HIGH) ) { interrupts(); return false; } + + // send handshake + parallelBusHandshakeTransmit(); + + interrupts(); + + return true; +} + +#endif + +#ifdef SUPPORT_EPYX + +// ------------------------------------ Epyx FastLoad support routines ------------------------------------ + + +bool IECBusHandler::enableEpyxFastLoadSupport(IECDevice *dev, bool enable) +{ + if( enable && m_bufferSize>=32 ) + dev->m_sflags |= S_EPYX_ENABLED; + else + dev->m_sflags &= ~S_EPYX_ENABLED; + + // cancel any current requests + dev->m_sflags &= ~(S_EPYX_HEADER|S_EPYX_LOAD|S_EPYX_SECTOROP); + + return (dev->m_sflags & S_EPYX_ENABLED)!=0; +} + + +void IECBusHandler::epyxLoadRequest(IECDevice *dev) +{ + if( dev->m_sflags & S_EPYX_ENABLED ) + dev->m_sflags |= S_EPYX_HEADER; +} + + +bool IECBusHandler::receiveEpyxByte(uint8_t &data) +{ + bool clk = HIGH; + for(uint8_t i=0; i<8; i++) + { + // wait for next bit ready + // can't use timeout because interrupts are disabled and (on some platforms) the + // micros() function does not work in this case + clk = !clk; + if( !waitPinCLK(clk, 0) ) return false; + + // read next (inverted) bit + JDEBUG1(); + data >>= 1; + if( !readPinDATA() ) data |= 0x80; + JDEBUG0(); + } + + return true; +} + + +bool IRAM_ATTR IECBusHandler::transmitEpyxByte(uint8_t data) +{ + // receiver expects all data bits to be inverted + data = ~data; + + // prepare timer + timer_init(); + timer_reset(); + + // wait (indefinitely) for either DATA high ("ready-to-send") or ATN low + // NOTE: this must be in a blocking loop since the sender starts transmitting + // the byte immediately after setting CLK high. If we exit the "task" function then + // we may not get back here in time to receive. +#ifdef ESP_PLATFORM + while( !digitalReadFastExt(m_pinDATA, m_regDATAread, m_bitDATA) && digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN) ) + if( !timer_less_than(IWDT_FEED_TIME) ) + { + // briefly enable interrupts to "feed" the WDT, otherwise we'll get re-booted + interrupts(); noInterrupts(); + timer_reset(); + } +#else + while( !digitalReadFastExt(m_pinDATA, m_regDATAread, m_bitDATA) && digitalReadFastExt(m_pinATN, m_regATNread, m_bitATN) ); +#endif + + // start timer + timer_start(); + JDEBUG1(); + + // abort if ATN low + if( !readPinATN() ) { JDEBUG0(); return false; } + + JDEBUG0(); + writePinCLK(data & bit(7)); + writePinDATA(data & bit(5)); + JDEBUG1(); + // bits 5+7 are read by receiver 15 cycles after DATA HIGH + + // wait until 17 us after DATA + timer_wait_until(17); + + JDEBUG0(); + writePinCLK(data & bit(6)); + writePinDATA(data & bit(4)); + JDEBUG1(); + // bits 4+6 are read by receiver 25 cycles after DATA HIGH + + // wait until 27 us after DATA + timer_wait_until(27); + + JDEBUG0(); + writePinCLK(data & bit(3)); + writePinDATA(data & bit(1)); + JDEBUG1(); + // bits 1+3 are read by receiver 35 cycles after DATA HIGH + + // wait until 37 us after DATA + timer_wait_until(37); + + JDEBUG0(); + writePinCLK(data & bit(2)); + writePinDATA(data & bit(0)); + JDEBUG1(); + // bits 0+2 are read by receiver 45 cycles after DATA HIGH + + // wait until 47 us after DATA + timer_wait_until(47); + + // release DATA and give it time to stabilize, also some + // buffer time if we got slightly delayed when waiting before + writePinDATA(HIGH); + timer_wait_until(52); + + // wait for DATA low, receiver signaling "not ready" + if( !waitPinDATA(LOW, 0) ) return false; + + JDEBUG0(); + return true; +} + + +#ifdef SUPPORT_EPYX_SECTOROPS + +// NOTE: most calls to waitPinXXX() within this code happen while +// interrupts are disabled and therefore must use the ",0" (no timeout) +// form of the call - timeouts are dealt with using the micros() function +// which does not work properly when interrupts are disabled. + +bool IECBusHandler::startEpyxSectorCommand(uint8_t command) +{ + // interrupts are assumed to be disabled when we get here + // and will be re-enabled before we exit + // both CLK and DATA must be released (HIGH) before entering + uint8_t track, sector, data; + + if( command==0x81 ) + { + // V1 sector write + // wait for DATA low (no timeout), however we exit if ATN goes low, + // interrupts are enabled while waiting (same as in 1541 code) + interrupts(); + if( !waitPinDATA(LOW, 0) ) return false; + noInterrupts(); + + // release CLK + writePinCLK(HIGH); + } + + // receive track and sector + // (command==1 means write sector, otherwise read sector) + if( !receiveEpyxByte(track) ) { interrupts(); return false; } + if( !receiveEpyxByte(sector) ) { interrupts(); return false; } + + // V1 of the cartridge has two different uploads for read and write + // and therefore does not send the command separately + if( command==0 && !receiveEpyxByte(command) ) { interrupts(); return false; } + + if( (command&0x7f)==1 ) + { + // sector write operation => receive data + for(int i=0; i<256; i++) + if( !receiveEpyxByte(m_buffer[i]) ) + { interrupts(); return false; } + } + + // pull CLK low to signal "not ready" + writePinCLK(LOW); + + // we can allow interrupts again + interrupts(); + + // pass data on to the device + if( (command&0x7f)==1 ) + if( !m_currentDevice->epyxWriteSector(track, sector, m_buffer) ) + { interrupts(); return false; } + + // m_buffer size is guaranteed to be >=32 + m_buffer[0] = command; + m_buffer[1] = track; + m_buffer[2] = sector; + + m_currentDevice->m_sflags |= S_EPYX_SECTOROP; + return true; +} + + +bool IECBusHandler::finishEpyxSectorCommand() +{ + // this was set in receiveEpyxSectorCommand + uint8_t command = m_buffer[0]; + uint8_t track = m_buffer[1]; + uint8_t sector = m_buffer[2]; + + // receive data from the device + if( (command&0x7f)!=1 ) + if( !m_currentDevice->epyxReadSector(track, sector, m_buffer) ) + return false; + + // all timing is clocked by the computer so we can't afford + // interrupts to delay execution as long as we are signaling "ready" + noInterrupts(); + + // release CLK to signal "ready" + writePinCLK(HIGH); + + if( command==0x81 ) + { + // V1 sector write => receive new track/sector + return startEpyxSectorCommand(0x81); // startEpyxSectorCommand() re-enables interrupts + } + else + { + // V1 sector read or V2/V3 read/write => release CLK to signal "ready" + if( (command&0x7f)!=1 ) + { + // sector read operation => send data + for(int i=0; i<256; i++) + if( !transmitEpyxByte(m_buffer[i]) ) + { interrupts(); return false; } + } + else + { + // release DATA and wait for computer to pull it LOW + writePinDATA(HIGH); + if( !waitPinDATA(LOW, 0) ) { interrupts(); return false; } + } + + // release DATA and toggle CLK until DATA goes high or ATN goes low. + // This provides a "heartbeat" for the computer so it knows we're still running + // the EPYX sector command code. If the computer does not see this heartbeat + // it will re-upload the code when it needs it. + // The EPYX code running on a real 1541 drive does not have this timeout but + // we need it because otherwise we're stuck in an endless loop with interrupts + // disabled until the computer either pulls ATN low or releases DATA + // We can not enable interrupts because the time between DATA high + // and the start of transmission for the next track/sector/command block + // is <400us without any chance for us to signal "not ready. + // A (not very nice) interrupt routing may take longer than that. + // We could just always quit and never send the heartbeat but then operations + // like "copy disk" would have to re-upload the code for ever single sector. + // Wait for DATA high, time out after 30000 * ~16us (~500ms) + timer_init(); + timer_reset(); + timer_start(); + for(unsigned int i=0; i<30000; i++) + { + writePinCLK(LOW); + if( !readPinATN() ) break; + interrupts(); + timer_wait_until(8); + noInterrupts(); + writePinCLK(HIGH); + if( readPinDATA() ) break; + timer_wait_until(16); + timer_reset(); + } + + // abort if we timed out (DATA still low) or ATN is pulled + if( !readPinDATA() || !readPinATN() ) { interrupts(); return false; } + + // wait (DATA high pulse from sender can be up to 90us) + if( !waitTimeout(100) ) { interrupts(); return false; } + + // if DATA is still high (or ATN is low) then done, otherwise repeat for another sector + if( readPinDATA() || !readPinATN() ) + { interrupts(); return false; } + else + return startEpyxSectorCommand((command&0x80) ? command : 0); // startEpyxSectorCommand() re-enables interrupts + } +} + +#endif + +bool IECBusHandler::receiveEpyxHeader() +{ + // all timing is clocked by the computer so we can't afford + // interrupts to delay execution as long as we are signaling "ready" + noInterrupts(); + + // pull CLK low to signal "ready for header" + writePinCLK(LOW); + + // wait for sender to set DATA low, signaling "ready" + if( !waitPinDATA(LOW, 0) ) { interrupts(); return false; } + + // release CLK line + writePinCLK(HIGH); + + // receive fastload routine upload (256 bytes) and compute checksum + uint8_t data, checksum = 0; + for(int i=0; i<256; i++) + { + if( !receiveEpyxByte(data) ) { interrupts(); return false; } + checksum += data; + } + + if( checksum==0x26 /* V1 load file */ || + checksum==0x86 /* V2 load file */ || + checksum==0xAA /* V3 load file */ ) + { + // LOAD FILE operation + // receive file name and open file + uint8_t n; + if( receiveEpyxByte(n) && n>0 && n<=32 ) + { + // file name arrives in reverse order + for(uint8_t i=n; i>0; i--) + if( !receiveEpyxByte(m_buffer[i-1]) ) + { interrupts(); return false; } + + // pull CLK low to signal "not ready" + writePinCLK(LOW); + + // can allow interrupts again + interrupts(); + + // initiate DOS OPEN command in the device (open channel #0) + m_currentDevice->listen(0xF0); + + // send file name (in proper order) to the device + for(uint8_t i=0; icanWrite())<0 ) + if( !readPinATN() ) + return false; + + // fail if it can not + if( ok==0 ) return false; + + // send next file name character + m_currentDevice->write(m_buffer[i], iunlisten(); + + m_currentDevice->m_sflags |= S_EPYX_LOAD; + return true; + } + } +#ifdef SUPPORT_EPYX_SECTOROPS + else if( checksum==0x0B /* V1 sector read */ ) + return startEpyxSectorCommand(0x82); // startEpyxSectorCommand re-enables interrupts + else if( checksum==0xBA /* V1 sector write */ ) + return startEpyxSectorCommand(0x81); // startEpyxSectorCommand re-enables interrupts + else if( checksum==0xB8 /* V2 and V3 sector read or write */ ) + return startEpyxSectorCommand(0); // startEpyxSectorCommand re-enables interrupts +#endif +#if 0 + else if( Serial ) + { + interrupts(); + Serial.print(F("Unknown EPYX fastload routine, checksum is 0x")); + Serial.println(checksum, HEX); + } +#endif + + interrupts(); + return false; +} + + +bool IECBusHandler::transmitEpyxBlock() +{ + // set channel number for read() call below + m_currentDevice->talk(0); + + // get data + m_inTask = false; + uint8_t n = m_currentDevice->read(m_buffer, m_bufferSize); + m_inTask = true; + if( (m_flags & P_ATN) || !readPinATN() ) return false; + + noInterrupts(); + + // release CLK to signal "ready" + writePinCLK(HIGH); + + // transmit length of this data block + if( !transmitEpyxByte(n) ) { interrupts(); return false; } + + // transmit the data block + for(uint8_t i=0; i0; +} + + +#endif + +// ------------------------------------ IEC protocol support routines ------------------------------------ + + +bool IECBusHandler::receiveIECByteATN(uint8_t &data) +{ + // wait for CLK=1 + if( !waitPinCLK(HIGH, 0) ) return false; + + // release DATA ("ready-for-data") + writePinDATA(HIGH); + + // wait for CLK=0 + // must wait indefinitely since other devices may be holding DATA low until + // they are ready, bus master will start sending as soon as all devices have + // released DATA + if( !waitPinCLK(LOW, 0) ) return false; + + // receive data bits + data = 0; + for(uint8_t i=0; i<8; i++) + { + // wait for CLK=1, signaling data is ready + JDEBUG1(); + +#ifdef SUPPORT_JIFFY + // JiffyDos protocol detection + if( (i==7) && (&data==&m_primary) && !waitPinCLK(HIGH, 200) ) + { + IECDevice *dev = findDevice((data>>1)&0x1F); + JDEBUG0(); + if( (dev!=NULL) && (dev->m_sflags&S_JIFFY_ENABLED) ) + { + JDEBUG1(); + // when sending final bit of primary address byte under ATN, host + // delayed CLK=1 by more than 200us => JiffyDOS protocol detection + // => if JiffyDOS support is enabled and we are being addressed then + // respond that we support the protocol by pulling DATA low for 80us + dev->m_sflags |= S_JIFFY_DETECTED; + writePinDATA(LOW); + if( !waitTimeout(80) ) return false; + writePinDATA(HIGH); + } + } +#endif + + if( !waitPinCLK(HIGH) ) return false; + JDEBUG0(); + + // read DATA bit + data >>= 1; + if( readPinDATA() ) data |= 0x80; + + // wait for CLK=0, signaling "data not ready" + if( !waitPinCLK(LOW) ) return false; + } + + // Acknowledge receipt by pulling DATA low + writePinDATA(LOW); + +#if defined(SUPPORT_DOLPHIN) + // DolphinDos parallel cable detection: + // after receiving secondary address, wait for either: + // HIGH->LOW edge (1us pulse) on incoming parallel handshake signal, + // if received pull outgoing parallel handshake signal LOW to confirm + // LOW->HIGH edge on ATN, + // if so then timeout, host does not support DolphinDos + + IECDevice *dev = findDevice(m_primary & 0x1F); + if( dev!=NULL && (dev->m_sflags & S_DOLPHIN_ENABLED) && (&data==&m_secondary) ) + if( parallelCableDetect() ) + { + dev->m_sflags |= S_DOLPHIN_DETECTED; + parallelBusHandshakeTransmit(); + } +#endif + + return true; +} + + +bool IECBusHandler::receiveIECByte(bool canWriteOk) +{ + // NOTE: we only get here if sender has already signaled ready-to-send + // by releasing CLK + bool eoi = false; + + noInterrupts(); + + // release DATA ("ready-for-data") + writePinDATA(HIGH); + + // wait for sender to set CLK=0 ("ready-to-send") + if( !waitPinCLK(LOW, 200) ) + { + // exit if waitPinCLK returned because of falling edge on ATN + if( !readPinATN() ) { interrupts(); return false; } + + // sender did not set CLK=0 within 200us after we set DATA=1 + // => it is signaling EOI (not so if we are under ATN) + // acknowledge we received it by setting DATA=0 for 80us + eoi = true; + writePinDATA(LOW); + if( !waitTimeout(80) ) { interrupts(); return false; } + writePinDATA(HIGH); + + // keep waiting for CLK=0 + if( !waitPinCLK(LOW) ) { interrupts(); return false; } + } + + // receive data bits + uint8_t data = 0; + for(uint8_t i=0; i<8; i++) + { + // wait for CLK=1, signaling data is ready + if( !waitPinCLK(HIGH) ) { interrupts(); return false; } + + // read DATA bit + data >>= 1; + if( readPinDATA() ) data |= 0x80; + + // wait for CLK=0, signaling "data not ready" + if( !waitPinCLK(LOW) ) { interrupts(); return false; } + } + + interrupts(); + + if( canWriteOk ) + { + // acknowledge receipt by pulling DATA low + writePinDATA(LOW); + + // pass received data on to the device + m_currentDevice->write(data, eoi); + return true; + } + else + { + // canWrite() reported an error + return false; + } +} + + +bool IECBusHandler::transmitIECByte(uint8_t numData) +{ + // check whether ready-to-receive was already signaled by the + // receiver before we signaled ready-to-send. The 1541 ROM + // disassembly (E919-E924) suggests that this signals a "verify error" + // condition and we should send EOI. Note that the C64 kernal does not + // actually do this signaling during a "verify" operation so I don't + // know whether my interpretation here is correct. However, some + // programs (e.g. "copy 190") lock up if we don't handle this case. + bool verifyError = readPinDATA(); + + noInterrupts(); + + // signal "ready-to-send" (CLK=1) + writePinCLK(HIGH); + + // wait (indefinitely, no timeout) for DATA HIGH ("ready-to-receive") + // NOTE: this must be in a blocking loop since the receiver starts counting + // up the EOI timeout immediately after setting DATA HIGH. If we had exited the + // "task" function then it might be more than 200us until we get back here + // to pull CLK low and the receiver will interpret that delay as EOI. + if( !waitPinDATA(HIGH, 0) ) { interrupts(); return false; } + + if( numData==1 || verifyError ) + { + // only this byte left to send => signal EOI by keeping CLK=1 + // wait for receiver to acknowledge EOI by setting DATA=0 then DATA=1 + // if we got here by "verifyError" then wait indefinitely because we + // didn't enter the "wait for DATA high" state above + if( !waitPinDATA(LOW, verifyError ? 0 : 1000) ) { interrupts(); return false; } + if( !waitPinDATA(HIGH) ) { interrupts(); return false; } + } + + // if we have nothing to send then there was some kind of error + // => aborting at this stage will signal the error condition to the receiver + // (e.g. "File not found" for LOAD) + if( numData==0 ) { interrupts(); return false; } + + // signal "data not valid" (CLK=0) + writePinCLK(LOW); + + interrupts(); + + // get the data byte from the device + uint8_t data = m_currentDevice->read(); + + // transmit the byte + for(uint8_t i=0; i<8; i++) + { + // signal "data not valid" (CLK=0) + writePinCLK(LOW); + + // set bit on DATA line + writePinDATA((data & 1)!=0); + + // hold for 80us + if( !waitTimeout(80) ) return false; + + // signal "data valid" (CLK=1) + writePinCLK(HIGH); + + // hold for 60us + if( !waitTimeout(60) ) return false; + + // next bit + data >>= 1; + } + + // pull CLK=0 and release DATA=1 to signal "busy" + writePinCLK(LOW); + writePinDATA(HIGH); + + // wait for receiver to signal "busy" + if( !waitPinDATA(LOW) ) return false; + + return true; +} + + +// called when a falling edge on ATN is detected, either by the pin change +// interrupt handler or by polling within the microTask function +void IRAM_ATTR IECBusHandler::atnRequest() +{ + // check if ATN is actually LOW, if not then just return (stray interrupt request) + if( readPinATN() ) return; + + // falling edge on ATN detected (bus master addressing all devices) + m_flags |= P_ATN; + m_flags &= ~P_DONE; + m_currentDevice = NULL; + + // ignore anything for 100us after ATN falling edge +#ifdef ESP_PLATFORM + // calling "micros()" (aka esp_timer_get_time()) within an interrupt handler + // on ESP32 appears to sometimes return incorrect values. This was observed + // when running Meatloaf on a LOLIN D32 board. So we just note that the + // timeout needs to be started and will actually set m_timeoutStart outside + // of the interrupt handler within the task() function + m_timeoutStart = 0xFFFFFFFF; +#else + m_timeoutStart = micros(); +#endif + + // release CLK (in case we were holding it LOW before) + writePinCLK(HIGH); + + // set DATA=0 ("I am here"). If nobody on the bus does this within 1ms, + // busmaster will assume that "Device not present" + writePinDATA(LOW); + + // disable the hardware that allows ATN to pull DATA low + writePinCTRL(HIGH); + +#ifdef SUPPORT_JIFFY + for(uint8_t i=0; im_sflags &= ~(S_JIFFY_DETECTED|S_JIFFY_BLOCK); +#endif +#ifdef SUPPORT_DOLPHIN + for(uint8_t i=0; im_sflags &= ~(S_DOLPHIN_BURST_TRANSMIT|S_DOLPHIN_BURST_RECEIVE|S_DOLPHIN_DETECTED); +#endif +#ifdef SUPPORT_EPYX + for(uint8_t i=0; im_sflags &= ~(S_EPYX_HEADER|S_EPYX_LOAD|S_EPYX_SECTOROP); +#endif +} + + +void IECBusHandler::task() +{ + // don't do anything if begin() hasn't been called yet + if( m_flags==0xFF ) return; + + // prevent interrupt handler from calling atnRequest() + m_inTask = true; + + // ------------------ check for activity on RESET pin ------------------- + + if( readPinRESET() ) + m_flags |= P_RESET; + else if( (m_flags & P_RESET)!=0 ) + { + // falling edge on RESET pin + m_flags = 0; + + // release CLK and DATA, allow ATN to pull DATA low in hardware + writePinCLK(HIGH); + writePinDATA(HIGH); + writePinCTRL(LOW); + + // call "reset" function for attached devices + for(uint8_t i=0; ireset(); + } + + // ------------------ check for activity on ATN pin ------------------- + + if( !(m_flags & P_ATN) && !readPinATN() ) + { + // falling edge on ATN (bus master addressing all devices) + atnRequest(); + } + +#ifdef ESP_PLATFORM + // see comment in atnRequest function + if( (m_flags & P_ATN)!=0 && !readPinATN() && + (m_timeoutStart==0xFFFFFFFF ? (m_timeoutStart=micros(),false) : (micros()-m_timeoutStart)>100) && + readPinCLK() ) +#else + if( (m_flags & P_ATN)!=0 && !readPinATN() && (micros()-m_timeoutStart)>100 && readPinCLK() ) +#endif + { + // we are under ATN, have waited 100us and the host has released CLK + // => no more interrupts until the ATN sequence is finished. If we allowed interrupts + // and a long interrupt occurred close to the end of the sequence then we may miss + // a quick ATN low->high->low sequence, i.e completely missing the start of a new + // ATN request. + noInterrupts(); + + // P_DONE flag may have gotten set again after it was reset in atnRequest() + m_flags &= ~P_DONE; + + m_primary = 0; + if( receiveIECByteATN(m_primary) && ((m_primary == 0x3f) || (m_primary == 0x5f) || (findDevice((unsigned int) m_primary & 0x1f)!=NULL)) ) + { + // this is either UNLISTEN or UNTALK or we were addressed + // => receive the secondary address, assume 0 if not sent + if( (m_primary == 0x3f) || (m_primary == 0x5f) || !receiveIECByteATN(m_secondary) ) m_secondary = 0; + + // wait until ATN is released + waitPinATN(HIGH); + m_flags &= ~P_ATN; + + // allow ATN to pull DATA low in hardware + writePinCTRL(LOW); + + if( (m_primary & 0xE0)==0x20 && (m_currentDevice = findDevice(m_primary & 0x1F))!=NULL ) + { + // we were told to listen + m_currentDevice->listen(m_secondary); + m_flags &= ~P_TALKING; + m_flags |= P_LISTENING; +#ifdef SUPPORT_DOLPHIN + // see comments in function receiveDolphinByte + if( m_secondary==0x61 ) m_dolphinCtr = 2*DOLPHIN_PREBUFFER_BYTES; +#endif + // set DATA=0 ("I am here") + writePinDATA(LOW); + } + else if( (m_primary & 0xE0)==0x40 && (m_currentDevice = findDevice(m_primary & 0x1F))!=NULL ) + { + // we were told to talk +#ifdef SUPPORT_JIFFY + if( (m_currentDevice->m_sflags & S_JIFFY_DETECTED)!=0 && m_secondary==0x61 ) + { + // in JiffyDOS, secondary 0x61 when talking enables "block transfer" mode + m_secondary = 0x60; + m_currentDevice->m_sflags |= S_JIFFY_BLOCK; + } +#endif + m_currentDevice->talk(m_secondary); + m_flags &= ~P_LISTENING; + m_flags |= P_TALKING; +#ifdef SUPPORT_DOLPHIN + // see comments in function transmitDolphinByte + if( m_secondary==0x60 ) m_dolphinCtr = 0; +#endif + // wait for bus master to set CLK=1 (and DATA=0) for role reversal + if( waitPinCLK(HIGH) ) + { + // now set CLK=0 and DATA=1 + writePinCLK(LOW); + writePinDATA(HIGH); + + // wait 80us before transmitting first byte of data + delayMicrosecondsISafe(80); + m_timeoutDuration = 0; + } + } + else if( (m_primary == 0x3f) && (m_flags & P_LISTENING) ) + { + // all devices were told to stop listening + m_flags &= ~P_LISTENING; + for(uint8_t i=0; iunlisten(); + } + else if( m_primary == 0x5f && (m_flags & P_TALKING) ) + { + // all devices were told to stop talking + m_flags &= ~P_TALKING; + for(uint8_t i=0; iuntalk(); + } + + if( !(m_flags & (P_LISTENING | P_TALKING)) ) + { + // we're neither listening nor talking => release CLK/DATA + writePinCLK(HIGH); + writePinDATA(HIGH); + } + } + else + { + // either we were not addressed or there was an error receiving the primary address + delayMicrosecondsISafe(150); + writePinCLK(HIGH); + writePinDATA(HIGH); + waitPinATN(HIGH); + + // if someone else was told to start talking then we must stop + if( (m_primary & 0xE0)==0x40 ) m_flags &= ~P_TALKING; + + // allow ATN to pull DATA low in hardware + writePinCTRL(LOW); + } + + interrupts(); + + if( (m_flags & P_LISTENING)!=0 ) + { + // a device is supposed to listen, check if it can accept data + // (meanwhile allow atnRequest to be called in interrupt) + IECDevice *dev = m_currentDevice; + m_inTask = false; + dev->task(); + bool canWrite = (dev->canWrite()>0); + m_inTask = true; + + // m_currentDevice could have been reset to NULL while m_inTask was 'false' + if( m_currentDevice!=NULL && !canWrite ) + { + // device can't accept data => signal error by releasing DATA line + writePinDATA(HIGH); + m_flags |= P_DONE; + } + } + } + else if( (m_flags & P_ATN)!=0 && readPinATN() ) + { + // host has released ATN + m_flags &= ~P_ATN; + } + +#ifdef SUPPORT_DOLPHIN + // ------------------ DolphinDos burst transfer handling ------------------- + + for(uint8_t devidx=0; devidxm_sflags & S_DOLPHIN_BURST_TRANSMIT)!=0 && (micros()-m_timeoutStart)>200 && !readPinDATA() ) + { + // if we are in burst transmit mode, give other devices 200us to release + // the DATA line and wait for the host to pull DATA LOW + + // pull CLK line LOW (host should have released it by now) + writePinCLK(LOW); + + m_currentDevice = m_devices[devidx]; + if( m_currentDevice->m_sflags & S_DOLPHIN_BURST_ENABLED ) + { + // transmit data in burst mode + transmitDolphinBurst(); + + // close the file (usually the host sends these but not in burst mode) + m_currentDevice->listen(0xE0); + m_currentDevice->unlisten(); + + // check whether ATN has been asserted and handle if necessary + if( !readPinATN() ) atnRequest(); + } + else + { + // switch to regular transmit mode + m_flags = P_TALKING; + m_currentDevice->m_sflags |= S_DOLPHIN_DETECTED; + m_secondary = 0x60; + } + + if( m_currentDevice!=NULL ) m_currentDevice->m_sflags &= ~S_DOLPHIN_BURST_TRANSMIT; + } + else if( (m_devices[devidx]->m_sflags&S_DOLPHIN_BURST_RECEIVE)!=0 && (micros()-m_timeoutStart)>500 && !readPinCLK() ) + { + // if we are in burst receive mode, wait 500us to make sure host has released CLK after + // sending "XZ" burst request (Dolphin kernal ef82), and wait for it to pull CLK low again + // (if we don't wait at first then we may read CLK=0 already before the host has released it) + + m_currentDevice = m_devices[devidx]; + if( m_currentDevice->m_sflags & S_DOLPHIN_BURST_ENABLED ) + { + // transmit data in burst mode + receiveDolphinBurst(); + + // check whether ATN has been asserted and handle if necessary + if( !readPinATN() ) atnRequest(); + } + else + { + // switch to regular receive mode + m_flags = P_LISTENING; + m_currentDevice->m_sflags |= S_DOLPHIN_DETECTED; + m_secondary = 0x61; + + // see comment in function receiveDolphinByte + m_dolphinCtr = (2*DOLPHIN_PREBUFFER_BYTES)-m_dolphinCtr; + + // signal NOT ready to receive + writePinDATA(LOW); + } + + if( m_currentDevice!=NULL ) m_currentDevice->m_sflags &= ~S_DOLPHIN_BURST_RECEIVE; + } +#endif + +#ifdef SUPPORT_EPYX + // ------------------ Epyx FastLoad transfer handling ------------------- + + for(uint8_t devidx=0; devidxm_sflags & S_EPYX_HEADER) && readPinDATA() ) + { + m_currentDevice = m_devices[devidx]; + m_currentDevice->m_sflags &= ~S_EPYX_HEADER; + if( !receiveEpyxHeader() ) + { + // transmission error + writePinCLK(HIGH); + writePinDATA(HIGH); + } + } + else if( m_devices[devidx]->m_sflags & S_EPYX_LOAD ) + { + m_currentDevice = m_devices[devidx]; + if( !transmitEpyxBlock() ) + { + // either end-of-data or transmission error => we are done + writePinCLK(HIGH); + writePinDATA(HIGH); + + // close the file (was opened in receiveEpyxHeader) + m_currentDevice->listen(0xE0); + m_currentDevice->unlisten(); + + // no more data to send + m_currentDevice->m_sflags &= ~S_EPYX_LOAD; + } + } +#ifdef SUPPORT_EPYX_SECTOROPS + else if( m_devices[devidx]->m_sflags & S_EPYX_SECTOROP ) + { + m_currentDevice = m_devices[devidx]; + if( !finishEpyxSectorCommand() ) + { + // either no more operations or transmission error => we are done + writePinCLK(HIGH); + writePinDATA(HIGH); + + // no more sector operations + m_currentDevice->m_sflags &= ~S_EPYX_SECTOROP; + } + } +#endif +#endif + + // ------------------ receiving data ------------------- + + if( (m_flags & (P_ATN|P_LISTENING|P_DONE))==P_LISTENING && (m_currentDevice!=NULL) ) + { + // we are not under ATN, are in "listening" mode and not done with the transaction + + // check if we can write (also gives devices a chance to + // execute time-consuming tasks while bus master waits for ready-for-data) + IECDevice *dev = m_currentDevice; + m_inTask = false; + int8_t numData = dev->canWrite(); + m_inTask = true; + + if( m_currentDevice==NULL ) + { /* m_currentDevice was reset while we were stuck in "canRead" */ } + else if( !readPinATN() ) + { + // a falling edge on ATN happened while we were stuck in "canWrite" + atnRequest(); + } +#ifdef SUPPORT_JIFFY + else if( (m_currentDevice->m_sflags & S_JIFFY_DETECTED)!=0 && numData>=0 ) + { + // receiving under JiffyDOS protocol + if( !receiveJiffyByte(numData>0) ) + { + // receive failed => release DATA + // and stop listening. This will signal + // an error condition to the sender + writePinDATA(HIGH); + m_flags |= P_DONE; + } + } +#endif +#ifdef SUPPORT_DOLPHIN + else if( (m_currentDevice->m_sflags & S_DOLPHIN_DETECTED)!=0 && numData>=0 ) + { + // receiving under DolphinDOS protocol + if( !readPinCLK() ) + { /* CLK is still low => sender is not ready yet */ } + else if( !receiveDolphinByte(numData>0) ) + { + // receive failed => release DATA + // and stop listening. This will signal + // an error condition to the sender + writePinDATA(HIGH); + m_flags |= P_DONE; + } + } +#endif + else if( numData>=0 && readPinCLK() ) + { + // either under ATN (in which case we always accept data) + // or canWrite() result was non-negative + // CLK high signals sender is ready to transmit + if( !receiveIECByte(numData>0) ) + { + // receive failed => transaction is done + m_flags |= P_DONE; + } + } + } + + // ------------------ transmitting data ------------------- + + if( (m_flags & (P_ATN|P_TALKING|P_DONE))==P_TALKING && (m_currentDevice!=NULL) ) + { + // we are not under ATN, are in "talking" mode and not done with the transaction + +#ifdef SUPPORT_JIFFY + if( (m_currentDevice->m_sflags & S_JIFFY_BLOCK)!=0 ) + { + // JiffyDOS block transfer mode + m_inTask = false; + uint8_t numData = m_currentDevice->read(m_buffer, m_bufferSize); + m_inTask = true; + + // delay to make sure receiver sees our CLK LOW and enters "new data block" state. + // If a possible VIC "bad line" occurs right after reading bits 6+7 it may take + // the receiver up to 160us after reading bits 6+7 (at FB71) to checking for CLK low (at FB54). + // If we make it back into transmitJiffyBlock() during that time period + // then we may already set CLK HIGH again before receiver sees the CLK LOW, + // preventing the receiver from going into "new data block" state + while( (micros()-m_timeoutStart)<175 ); + + if( (m_flags & P_ATN) || !readPinATN() || !transmitJiffyBlock(m_buffer, numData) ) + { + // either a transmission error, no more data to send or falling edge on ATN + m_flags |= P_DONE; + } + else + { + // remember time when previous transmission finished + m_timeoutStart = micros(); + } + } + else +#endif + { + // check if we can read (also gives devices a chance to + // execute time-consuming tasks while bus master waits for ready-to-send) + IECDevice *dev = m_currentDevice; + m_inTask = false; + int8_t numData = dev->canRead(); + m_inTask = true; + + if( m_currentDevice==NULL ) + { /* m_currentDevice was reset while we were stuck in "canRead" */ } + else if( !readPinATN() ) + { + // a falling edge on ATN happened while we were stuck in "canRead" + atnRequest(); + } + else if( (micros()-m_timeoutStart) do nothing + } +#ifdef SUPPORT_JIFFY + else if( (m_currentDevice->m_sflags & S_JIFFY_DETECTED)!=0 ) + { + // JiffyDOS byte-by-byte transfer mode + if( !transmitJiffyByte(numData) ) + { + // either a transmission error, no more data to send or falling edge on ATN + m_flags |= P_DONE; + } + } +#endif +#ifdef SUPPORT_DOLPHIN + else if( (m_currentDevice->m_sflags & S_DOLPHIN_DETECTED)!=0 ) + { + // DolphinDOS byte-by-byte transfer mode + if( !transmitDolphinByte(numData) ) + { + // either a transmission error, no more data to send or falling edge on ATN + writePinCLK(HIGH); + m_flags |= P_DONE; + } + } +#endif + else + { + // regular IEC transfer + if( transmitIECByte(numData) ) + { + // delay before next transmission ("between bytes time") + m_timeoutStart = micros(); + m_timeoutDuration = 200; + } + else + { + // either a transmission error, no more data to send or falling edge on ATN + m_flags |= P_DONE; + } + } + } + } + + // allow the interrupt handler to call atnRequest() again + m_inTask = false; + + // if ATN is low and we don't have P_ATN then we missed the falling edge, + // make sure to process it before we leave + if( m_atnInterrupt!=NOT_AN_INTERRUPT && !readPinATN() && !(m_flags & P_ATN) ) { noInterrupts(); atnRequest(); interrupts(); } + + // call "task" function for attached devices + for(uint8_t i=0; itask(); +} diff --git a/lib/bus/iec/IECBusHandler.h b/lib/bus/iec/IECBusHandler.h new file mode 100644 index 000000000..5583c3a8f --- /dev/null +++ b/lib/bus/iec/IECBusHandler.h @@ -0,0 +1,221 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2023 David Hansel +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have receikved a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#ifndef IECBUSHANDLER_H +#define IECBUSHANDLER_H + +#include "IECConfig.h" +#include + +#if defined(__AVR__) +#define IOREG_TYPE uint8_t +#elif defined(ARDUINO_UNOR4_MINIMA) || defined(ARDUINO_UNOR4_WIFI) +#define IOREG_TYPE uint16_t +#elif defined(__SAM3X8E__) || defined(ESP_PLATFORM) +#define IOREG_TYPE uint32_t +#endif + +#if defined(ESP_PLATFORM) && !defined(ARDUINO) +#define INTERRUPT_FCN_ARG void * +#else +#define INTERRUPT_FCN_ARG +#endif + +class IECDevice; + +class IECBusHandler +{ + public: + // pinATN should preferrably be a pin that can handle external interrupts + // (e.g. 2 or 3 on the Arduino UNO), if not then make sure the task() function + // gets called at least once evey millisecond, otherwise "device not present" + // errors may result + IECBusHandler(uint8_t pinATN, uint8_t pinCLK, uint8_t pinDATA, uint8_t pinRESET = 0xFF, uint8_t pinCTRL = 0xFF, uint8_t pinSRQ = 0xFF); + + // must be called once at startup before the first call to "task", devnr + // is the IEC bus device number that this device should react to + void begin(); + + bool attachDevice(IECDevice *dev); + bool detachDevice(IECDevice *dev); + + // task must be called periodically to handle IEC bus communication + // if the ATN signal is NOT on an interrupt-capable pin then task() must be + // called at least once every millisecond, otherwise less frequent calls are + // ok but bus communication will be slower if called less frequently. + void task(); + +#if (defined(SUPPORT_JIFFY) || defined(SUPPORT_DOLPHIN) || defined(SUPPORT_EPYX)) && !defined(IEC_DEFAULT_FASTLOAD_BUFFER_SIZE) + // if IEC_DEFAULT_FASTLOAD_BUFFER_SIZE is set to 0 then the buffer space used + // by fastload protocols can be set dynamically using the setBuffer function. + void setBuffer(uint8_t *buffer, uint8_t bufferSize); +#endif + +#ifdef SUPPORT_JIFFY + bool enableJiffyDosSupport(IECDevice *dev, bool enable); +#endif + +#ifdef SUPPORT_EPYX + bool enableEpyxFastLoadSupport(IECDevice *dev, bool enable); + void epyxLoadRequest(IECDevice *dev); +#endif + +#ifdef SUPPORT_DOLPHIN + // call this BEFORE begin() if you do not want to use the default pins for the DolphinDos cable +#ifdef SUPPORT_DOLPHIN_XRA1405 + void setDolphinDosPins(uint8_t pinHT, uint8_t pinHR, uint8_t pinSCK, uint8_t pinCOPI, uint8_t pinCIPO, uint8_t pinCS); +#else + void setDolphinDosPins(uint8_t pinHT, uint8_t pinHR, uint8_t pinD0, uint8_t pinD1, uint8_t pinD2, uint8_t pinD3, + uint8_t pinD4, uint8_t pinD5, uint8_t pinD6, uint8_t pinD7); +#endif + + bool enableDolphinDosSupport(IECDevice *dev, bool enable); + void enableDolphinBurstMode(IECDevice *dev, bool enable); + void dolphinBurstReceiveRequest(IECDevice *dev); + void dolphinBurstTransmitRequest(IECDevice *dev); +#endif + + IECDevice *findDevice(uint8_t devnr, bool includeInactive = false); + bool canServeATN(); + bool inTransaction(); + void sendSRQ(); + + IECDevice *m_currentDevice; + IECDevice *m_devices[MAX_DEVICES]; + + uint8_t m_numDevices; + int m_atnInterrupt; + uint8_t m_pinATN, m_pinCLK, m_pinDATA, m_pinRESET, m_pinSRQ, m_pinCTRL; + + private: + inline bool readPinATN(); + inline bool readPinCLK(); + inline bool readPinDATA(); + inline bool readPinRESET(); + inline void writePinCLK(bool v); + inline void writePinDATA(bool v); + void writePinCTRL(bool v); + bool waitTimeout(uint16_t timeout, uint8_t cond = 0); + bool waitPinDATA(bool state, uint16_t timeout = 1000); + bool waitPinCLK(bool state, uint16_t timeout = 1000); + void waitPinATN(bool state); + void atnRequest(); + bool receiveIECByteATN(uint8_t &data); + bool receiveIECByte(bool canWriteOk); + bool transmitIECByte(uint8_t numData); + + volatile uint16_t m_timeoutDuration; + volatile uint32_t m_timeoutStart; + volatile bool m_inTask; + volatile uint8_t m_flags; + uint8_t m_primary, m_secondary; + +#ifdef IOREG_TYPE + volatile IOREG_TYPE *m_regCLKwrite, *m_regCLKmode, *m_regDATAwrite, *m_regDATAmode; + volatile const IOREG_TYPE *m_regATNread, *m_regCLKread, *m_regDATAread, *m_regRESETread; + IOREG_TYPE m_bitATN, m_bitCLK, m_bitDATA, m_bitRESET; +#endif + +#ifdef SUPPORT_JIFFY + bool receiveJiffyByte(bool canWriteOk); + bool transmitJiffyByte(uint8_t numData); + bool transmitJiffyBlock(uint8_t *buffer, uint8_t numBytes); +#endif + +#ifdef SUPPORT_DOLPHIN + bool transmitDolphinByte(uint8_t numData); + bool receiveDolphinByte(bool canWriteOk); + bool transmitDolphinBurst(); + bool receiveDolphinBurst(); + + void startParallelTransaction(); + void endParallelTransaction(); + bool parallelBusHandshakeReceived(); + bool waitParallelBusHandshakeReceived(); + void parallelBusHandshakeTransmit(); + void setParallelBusModeInput(); + void setParallelBusModeOutput(); + bool parallelCableDetect(); + uint8_t readParallelData(); + void writeParallelData(uint8_t data); + void enableParallelPins(); + bool isDolphinPin(uint8_t pin); + +#ifdef SUPPORT_DOLPHIN_XRA1405 + uint8_t m_pinDolphinSCK, m_pinDolphinCOPI, m_pinDolphinCIPO, m_pinDolphinCS, m_inTransaction; + uint8_t XRA1405_ReadReg(uint8_t reg); + void XRA1405_WriteReg(uint8_t reg, uint8_t data); + +#ifdef IOREG_TYPE + volatile IOREG_TYPE *m_regDolphinCS; + IOREG_TYPE m_bitDolphinCS; +#endif + +#else // !SUPPORT_DOLPHIN_XRA1405 + + uint8_t m_pinDolphinParallel[8]; +#ifdef IOREG_TYPE + volatile IOREG_TYPE *m_regDolphinParallelMode[8], *m_regDolphinParallelWrite[8]; + volatile const IOREG_TYPE *m_regDolphinParallelRead[8]; + IOREG_TYPE m_bitDolphinParallel[8]; +#endif + +#endif // SUPPORT_DOLPHIN_XRA1405 + + uint8_t m_pinDolphinHandshakeTransmit; + uint8_t m_pinDolphinHandshakeReceive; + uint8_t m_dolphinCtr; + +#ifdef IOREG_TYPE + volatile IOREG_TYPE *m_regDolphinHandshakeTransmitMode; + IOREG_TYPE m_bitDolphinHandshakeTransmit; +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega2560__) + IOREG_TYPE m_handshakeReceivedBit = 0; +#endif +#endif // IOREG_TYPE +#endif // SUPPORT_DOLPHIN + +#ifdef SUPPORT_EPYX + bool receiveEpyxByte(uint8_t &data); + bool transmitEpyxByte(uint8_t data); + bool receiveEpyxHeader(); + bool transmitEpyxBlock(); +#ifdef SUPPORT_EPYX_SECTOROPS + bool startEpyxSectorCommand(uint8_t command); + bool finishEpyxSectorCommand(); +#endif +#endif + +#if defined(SUPPORT_JIFFY) || defined(SUPPORT_DOLPHIN) || defined(SUPPORT_EPYX) + uint8_t m_bufferSize; +#if IEC_DEFAULT_FASTLOAD_BUFFER_SIZE>0 +#if defined(SUPPORT_EPYX) && defined(SUPPORT_EPYX_SECTOROPS) + uint8_t m_buffer[256]; +#else + uint8_t m_buffer[IEC_DEFAULT_FASTLOAD_BUFFER_SIZE]; +#endif +#else + uint8_t *m_buffer; +#endif +#endif + + static IECBusHandler *s_bushandler; + static void atnInterruptFcn(INTERRUPT_FCN_ARG); +}; + +#endif diff --git a/lib/bus/iec/IECConfig.h b/lib/bus/iec/IECConfig.h new file mode 100644 index 000000000..6df1565be --- /dev/null +++ b/lib/bus/iec/IECConfig.h @@ -0,0 +1,63 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2024 David Hansel +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have receikved a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#ifndef IECCONFIG_H +#define IECCONFIG_H +#include "../../include/pinmap.h" + +// comment or un-comment these #defines to completely enable/disable support +// for the corresponding fast-load protocols +#define SUPPORT_JIFFY +#define SUPPORT_EPYX +#if defined(PIN_XRA1405_CS) && defined(PIN_PARALLEL_PC2) && defined(PIN_PARALLEL_FLAG2) +//#define SUPPORT_DOLPHIN +//#define SUPPORT_DOLPHIN_XRA1405 +#endif + +// support Epyx FastLoad sector operations (disk editor, disk copy, file copy) +// if this is enabled then the buffer in the setBuffer() call must have a size of +// at least 256 bytes. Note that the "bufferSize" argument is a byte and therefore +// capped at 255 bytes. Make sure the buffer itself has >=256 bytes and use a +// bufferSize argument of 255 or less +//#define SUPPORT_EPYX_SECTOROPS + +// defines the maximum number of devices that the bus handler will be +// able to support - set to 4 by default but can be increased to up to 30 devices +#define MAX_DEVICES 30 + +// sets the default size of the fastload buffer. If this is set to 0 then fastload +// protocols can only be used if the IECBusHandler::setBuffer() function is +// called to define the buffer. +#if defined(SUPPORT_JIFFY) || defined(SUPPORT_DOLPHIN) || defined(SUPPORT_EPYX) +#define IEC_DEFAULT_FASTLOAD_BUFFER_SIZE 128 +#endif + +// buffer size for IECFileDevice when receiving data. On channel 15, any command +// longer than this (received in a single transaction) will be discarded. +// For other channels, the device's write() function will be called once the +// buffer is full. Every instance of IECFileDevice will allocate this buffer +// so it should be kept small on platforms with little RAM (e.g. Arduino UNO) +#define IECFILEDEVICE_WRITE_BUFFER_SIZE 128 + +// buffer size for IECFileDevice transmitting data on channel 15, if +// IECFileDevice::setStatus() is called with data longer than this it will be clipped. +// every instance of IECFileDevice will allocate this buffer so it should be +// kept small on platforms with little RAM (e.g. Arduino UNO) +#define IECFILEDEVICE_STATUS_BUFFER_SIZE 128 + +#endif diff --git a/lib/bus/iec/IECDevice.cpp b/lib/bus/iec/IECDevice.cpp new file mode 100644 index 000000000..1c73c661f --- /dev/null +++ b/lib/bus/iec/IECDevice.cpp @@ -0,0 +1,133 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2024 David Hansel +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have receikved a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#include "IECDevice.h" +#include "IECBusHandler.h" + +#if defined(ARDUINO) +#include +#elif defined(ESP_PLATFORM) +#include "IECespidf.h" +#endif + +IECDevice::IECDevice(uint8_t devnr) +{ + m_devnr = devnr; + m_handler = NULL; + m_sflags = 0; + //m_isActive = true; +} + +void IECDevice::setDeviceNumber(uint8_t devnr) +{ + m_devnr = devnr; +} + + +void IECDevice::sendSRQ() +{ + if( m_handler ) m_handler->sendSRQ(); +} + + +#ifdef SUPPORT_JIFFY +bool IECDevice::enableJiffyDosSupport(bool enable) +{ + return m_handler ? m_handler->enableJiffyDosSupport(this, enable) : false; +} +#endif + + +#ifdef SUPPORT_DOLPHIN +bool IECDevice::enableDolphinDosSupport(bool enable) +{ + return m_handler ? m_handler->enableDolphinDosSupport(this, enable) : false; +} + +void IECDevice::enableDolphinBurstMode(bool enable) +{ + if( m_handler ) m_handler->enableDolphinBurstMode(this, enable); +} + +void IECDevice::dolphinBurstReceiveRequest() +{ + if( m_handler ) m_handler->dolphinBurstReceiveRequest(this); +} + +void IECDevice::dolphinBurstTransmitRequest() +{ + if( m_handler ) m_handler->dolphinBurstTransmitRequest(this); +} +#endif + +#ifdef SUPPORT_EPYX +bool IECDevice::enableEpyxFastLoadSupport(bool enable) +{ + return m_handler ? m_handler->enableEpyxFastLoadSupport(this, enable) : false; +} + +void IECDevice::epyxLoadRequest() +{ + if( m_handler ) m_handler->epyxLoadRequest(this); +} + +#endif + + +// default implementation of "buffer read" function which can/should be overridden +// (for efficiency) by devices using the JiffyDos, Epyx FastLoad or DolphinDos protocol +#if defined(SUPPORT_JIFFY) || defined(SUPPORT_EPYX) || defined(SUPPORT_DOLPHIN) +uint8_t IECDevice::read(uint8_t *buffer, uint8_t bufferSize) +{ + uint8_t i; + for(i=0; i + +class IECBusHandler; + +class IECDevice +{ + friend class IECBusHandler; + + public: + // pinATN should preferrably be a pin that can handle external interrupts + // (e.g. 2 or 3 on the Arduino UNO), if not then make sure the task() function + // gets called at least once evey millisecond, otherwise "device not present" + // errors may result + IECDevice(uint8_t devnr = 0xFF); + + // call this to change the device number + void setDeviceNumber(uint8_t devnr); + +#ifdef SUPPORT_JIFFY + // call this to enable or disable JiffyDOS support for your device. + bool enableJiffyDosSupport(bool enable); +#endif + +#ifdef SUPPORT_DOLPHIN + // call this to enable or disable DolphinDOS support for your device. + // this function will fail if any of the pins used for ATN/CLK/DATA/RESET + // are the same as the pins used for the parallel cable + bool enableDolphinDosSupport(bool enable); +#endif + +#ifdef SUPPORT_EPYX + // call this to enable or disable Expyx FastLoad support for your device. + bool enableEpyxFastLoadSupport(bool enable); +#endif + + + /** + * @brief is device active (turned on?) + */ + bool device_active = true; + + // this can be overloaded by derived classes + virtual bool isActive() { return device_active; } + + // if isActive() is not overloaded then use this to activate/deactivate a device + void setActive(bool b) { device_active = b; } + + protected: + // called when IECBusHandler::begin() is called + virtual void begin() {} + + // called IECBusHandler::task() is called + virtual void task() {} + + // called on falling edge of RESET line + virtual void reset() {} + + // called when bus master sends TALK command + // talk() must return within 1 millisecond + virtual void talk(uint8_t secondary) {} + + // called when bus master sends LISTEN command + // listen() must return within 1 millisecond + virtual void listen(uint8_t secondary) {} + + // called when bus master sends UNTALK command + // untalk() must return within 1 millisecond + virtual void untalk() {} + + // called when bus master sends UNLISTEN command + // unlisten() must return within 1 millisecond + virtual void unlisten() {} + + // called before a write() call to determine whether the device + // is ready to receive data. + // canWrite() is allowed to take an indefinite amount of time + // canWrite() should return: + // <0 if more time is needed before data can be accepted (call again later), blocks IEC bus + // 0 if no data can be accepted (error) + // >0 if at least one byte of data can be accepted + virtual int8_t canWrite() { return 0; } + + // called before a read() call to see how many bytes are available to read + // canRead() is allowed to take an indefinite amount of time + // canRead() should return: + // <0 if more time is needed before we can read (call again later), blocks IEC bus + // 0 if no data is available to read (error) + // 1 if one byte of data is available + // >1 if more than one byte of data is available + virtual int8_t canRead() { return 0; } + + // called when the device received data + // write() will only be called if the last call to canWrite() returned >0 + // write() must return within 1 millisecond + // the "eoi" parameter will be "true" if sender signaled that this is the last + // data byte of a transmission + virtual void write(uint8_t data, bool eoi) {} + + // called when the device is sending data + // read() will only be called if the last call to canRead() returned >0 + // read() is allowed to take an indefinite amount of time + virtual uint8_t read() { return 0; } + +#if defined(SUPPORT_JIFFY) || defined(SUPPORT_DOLPHIN) + // called when the device is sending data using JiffyDOS byte-by-byte protocol + // peek() will only be called if the last call to canRead() returned >0 + // peek() should return the next character that will be read with read() + // peek() is allowed to take an indefinite amount of time + virtual uint8_t peek() { return 0; } +#endif + +#ifdef SUPPORT_DOLPHIN + // called when the device is sending data using the DolphinDos burst transfer (SAVE protocol) + // should write all the data in the buffer and return the number of bytes written + // returning less than bufferSize signals an error condition + // the "eoi" parameter will be "true" if sender signaled that this is the final part of the transmission + // write() is allowed to take an indefinite amount of time + // the default implementation within IECDevice uses the canWrite() and write(data,eoi) functions, + // which is not efficient. + // it is highly recommended to override this function in devices supporting DolphinDos + virtual uint8_t write(uint8_t *buffer, uint8_t bufferSize, bool eoi); +#endif + +#if defined(SUPPORT_JIFFY) || defined(SUPPORT_DOLPHIN) || defined(SUPPORT_EPYX) + // called when the device is sending data using the JiffyDOS block transfer + // or DolphinDos burst transfer (LOAD protocols) + // - should fill the buffer with as much data as possible (up to bufferSize) + // - must return the number of bytes put into the buffer + // read() is allowed to take an indefinite amount of time + // the default implementation within IECDevice uses the canRead() and read() functions, + // which is not efficient. + // it is highly recommended to override this function in devices supporting JiffyDos or DolphinDos. + virtual uint8_t read(uint8_t *buffer, uint8_t bufferSize); +#endif + +#if defined(SUPPORT_EPYX) && defined(SUPPORT_EPYX_SECTOROPS) + // these functions are experimental, they are called when the Epyx Cartridge uses + // sector read/write operations (disk editor, disk copy or file copy). + virtual bool epyxReadSector(uint8_t track, uint8_t sector, uint8_t *buffer) { return false; } + virtual bool epyxWriteSector(uint8_t track, uint8_t sector, uint8_t *buffer) { return false; } +#endif + +#ifdef SUPPORT_DOLPHIN + // call this to enable or disable DolphinDOS burst transmission mode + // On the 1541, this gets enabled/disabled by the "XF+"/"XF-" command + // (the IECFileDevice class handles this automatically) + void enableDolphinBurstMode(bool enable); + + // call this when a DolphinDOS burst recive request ("XZ") is received + // on the command channel (the IECFileDevice class handles this automatically) + void dolphinBurstReceiveRequest(); + + // call this when a DolphinDOS burst transmit request ("XQ") is received + // on the command channel (the IECFileDevice class handles this automatically) + void dolphinBurstTransmitRequest(); +#endif + +#ifdef SUPPORT_EPYX + // call this after receiving the EPYX fast-load routine upload (via M-W and M-E) + // (the IECFileDevice class handles this automatically) + void epyxLoadRequest(); +#endif + + // send pulse on SRQ line (if SRQ pin was set in IECBusHandler constructor) + void sendSRQ(); + + protected: + //bool m_isActive; + uint8_t m_devnr; + uint16_t m_sflags; + IECBusHandler *m_handler; +}; + +#endif + diff --git a/lib/bus/iec/IECFileDevice.cpp b/lib/bus/iec/IECFileDevice.cpp new file mode 100644 index 000000000..1f81ee198 --- /dev/null +++ b/lib/bus/iec/IECFileDevice.cpp @@ -0,0 +1,694 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2024 David Hansel +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#include "IECFileDevice.h" +#include "IECBusHandler.h" + +#if defined(ARDUINO) +#include +#elif defined(ESP_PLATFORM) +#include "IECespidf.h" +#endif + +#define DEBUG 0 + +#if DEBUG>0 + +void print_hex(uint8_t data) +{ + static const PROGMEM char hex[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + Serial.write(pgm_read_byte_near(hex+(data/16))); + Serial.write(pgm_read_byte_near(hex+(data&15))); +} + + +static uint8_t dbgbuf[16], dbgnum = 0; + +void dbg_print_data() +{ + if( dbgnum>0 ) + { + for(uint8_t i=0; i0 + /*if( !Serial )*/ Serial.begin(115200); + for(int i=0; !Serial && i<5; i++) delay(1000); + Serial.print(F("START:IECFileDevice, devnr=")); Serial.println(m_devnr); +#endif + + bool ok; +#ifdef SUPPORT_JIFFY + ok = IECDevice::enableJiffyDosSupport(true); +#if DEBUG>0 + Serial.print(F("JiffyDos support ")); Serial.println(ok ? F("enabled") : F("disabled")); +#endif +#endif +#ifdef SUPPORT_DOLPHIN + ok = IECDevice::enableDolphinDosSupport(true); +#if DEBUG>0 + Serial.print(F("DolphinDos support ")); Serial.println(ok ? F("enabled") : F("disabled")); +#endif +#endif +#ifdef SUPPORT_EPYX + ok = IECDevice::enableEpyxFastLoadSupport(true); + m_epyxCtr = 0; +#if DEBUG>0 + Serial.print(F("Epyx FastLoad support ")); Serial.println(ok ? F("enabled") : F("disabled")); +#endif +#endif + + m_statusBufferPtr = 0; + m_statusBufferLen = 0; + m_writeBufferLen = 0; + memset(m_readBufferLen, 0, 15); + m_cmd = IFD_NONE; + m_channel = 0xFF; + m_opening = false; + + // calling fileTask() may result in significant time spent accessing the + // disk during which we can not respond to ATN requests within the required + // 1000us (interrupts are disabled during disk access). We have two options: + // 1) call fileTask() from within the canWrite() and canRead() functions + // which are allowed to block indefinitely. Doing so has two downsides: + // - receiving a disk command via OPEN 1,x,15,"CMD" will NOT execute + // it right away because there is no call to canRead/canWrite after + // the "unlisten" call that finishes the transmission. The command will + // execute once the next operation (even just a status query) starts. + // - if the bus master pulls ATN low in the middle of a transmission + // (does not usually happen) we may not respond fast enough which may + // result in a "Device not present" error. + // 2) add some minimal hardware that immediately pulls DATA low when ATN + // goes low (this is what the C1541 disk drive does). This will make + // the bus master wait until we release DATA when we are actually ready + // to communicate. In that case we can process fileTask() here which + // mitigates both issues with the first approach. The hardware needs + // one additional output pin (pinCTRL) used to enable/disable the + // override of the DATA line. + // + // if we have the extra hardware then m_pinCTRL!=0xFF + m_canServeATN = m_handler->canServeATN(); + + IECDevice::begin(); +} + + +uint8_t IECFileDevice::getStatusData(char *buffer, uint8_t bufferSize) +{ + // call the getStatus() function that returns a null-terminated string + m_statusBuffer[0] = 0; + getStatus(m_statusBuffer, bufferSize); + m_statusBuffer[bufferSize-1] = 0; + return strlen(m_statusBuffer); +} + + +int8_t IECFileDevice::canRead() +{ +#if DEBUG>2 + Serial.write('c');Serial.write('R'); +#endif + + // see comment in IECFileDevice constructor + if( !m_canServeATN ) + { + // for IFD_OPEN, fileTask() resets the channel to 0xFF which is a problem when we call it from + // here because we have already received the LISTEN after the UNLISTEN that + // initiated the OPEN and so m_channel will not be set again => remember and restore it here + if( m_cmd==IFD_OPEN ) + { uint8_t c = m_channel; fileTask(); m_channel = c; } + else + fileTask(); + } + + if( m_channel==15 ) + { + if( m_statusBufferPtr==m_statusBufferLen ) + { + m_statusBufferPtr = 0; + m_statusBufferLen = getStatusData(m_statusBuffer, IECFILEDEVICE_STATUS_BUFFER_SIZE); +#if DEBUG>0 + Serial.print(F("STATUS")); +#if MAX_DEVICES>1 + Serial.write('#'); Serial.print(m_devnr); +#endif + Serial.write(':'); Serial.write(' '); + Serial.println(m_statusBuffer); + for(uint8_t i=0; i 15 || m_readBufferLen[m_channel]==-128 ) + { + return 0; // invalid channel or OPEN failed for channel + } + else + { + fillReadBuffer(); +#if DEBUG>2 + print_hex(m_readBufferLen[m_channel]); +#endif + return m_readBufferLen[m_channel]; + } +} + + +uint8_t IECFileDevice::peek() +{ + uint8_t data = 0; + + if( m_channel==15 ) + data = m_statusBuffer[m_statusBufferPtr]; + else if( m_channel < 15 ) + data = m_readBuffer[m_channel][0]; + +#if DEBUG>1 + Serial.write('P'); print_hex(data); +#endif + + return data; +} + + +uint8_t IECFileDevice::read() +{ + uint8_t data = 0; + + if( m_channel==15 ) + data = m_statusBuffer[m_statusBufferPtr++]; + else if( m_channel<15 ) + { + data = m_readBuffer[m_channel][0]; + if( m_readBufferLen[m_channel]==2 ) + { + m_readBuffer[m_channel][0] = m_readBuffer[m_channel][1]; + m_readBufferLen[m_channel] = 1; + } + else + m_readBufferLen[m_channel] = 0; + } + +#if DEBUG>1 + Serial.write('R'); print_hex(data); +#endif + + return data; +} + + +uint8_t IECFileDevice::read(uint8_t *buffer, uint8_t bufferSize) +{ + uint8_t res = 0; + + // get data from our own 2-uint8_t buffer (if any) + // properly deal with the case where bufferSize==1 + while( m_readBufferLen[m_channel]>0 && res0 + for(uint8_t i=0; i2 + Serial.write('c');Serial.write('W'); +#endif + + // see comment in IECFileDevice constructor + if( !m_canServeATN ) + { + // for IFD_OPEN, fileTask() resets the channel to 0xFF which is a problem when we call it from + // here because we have already received the TALK after the UNLISTEN that + // initiated the OPEN and so m_channel will not be set again => remember and restore it here + if( m_cmd==IFD_OPEN ) + { uint8_t c = m_channel; fileTask(); m_channel = c; } + else + fileTask(); + } + + if( m_channel == 15 || m_opening ) + { + return 1; // command channel or opening file + } + else if( m_channel > 15 || m_readBufferLen[m_channel]==-128 ) + { + return 0; // invalid channel or OPEN failed + } + else + { + // if write buffer is full then send it on now + if( m_writeBufferLen==IECFILEDEVICE_WRITE_BUFFER_SIZE-1 ) + emptyWriteBuffer(); + + return (m_writeBufferLen do not add Serial.print or function call that may take longer! + // (at 115200 baud we can send 10 characters in less than 1 ms) + + m_eoi |= eoi; + if( m_writeBufferLen1 + Serial.write('W'); print_hex(data); +#endif +} + + +uint8_t IECFileDevice::write(uint8_t *buffer, uint8_t bufferSize, bool eoi) +{ + if( m_channel < 15 ) + { + // first pass on data that has been buffered (if any), if that is not + // possible then return indicating that nothing of the new data has been sent + emptyWriteBuffer(); + if( m_writeBufferLen>0 ) return 0; + + // now pass on new data + m_eoi |= eoi; + uint8_t nn = write(m_channel, buffer, bufferSize, m_eoi); +#if DEBUG>0 + for(uint8_t i=0; i1 + Serial.write('T'); print_hex(secondary); +#endif + + m_channel = secondary & 0x0F; + m_eoi = false; +} + + +void IECFileDevice::untalk() +{ +#if DEBUG>1 + Serial.write('t'); +#endif + + // no current channel + m_channel = 0xFF; +} + + +void IECFileDevice::listen(uint8_t secondary) +{ +#if DEBUG>1 + Serial.write('L'); print_hex(secondary); +#endif + m_channel = secondary & 0x0F; + m_eoi = false; + + if( m_channel==15 ) + m_writeBufferLen = 0; + else if( (secondary & 0xF0) == 0xF0 ) + { + m_opening = true; + m_writeBufferLen = 0; + } + else if( (secondary & 0xF0) == 0xE0 ) + { + m_cmd = IFD_CLOSE; + } +} + + +void IECFileDevice::unlisten() +{ +#if DEBUG>1 + Serial.write('l'); Serial.write('0'+m_channel); +#endif + + if( m_channel==15 ) + { + if( m_writeBufferLen>0 ) + { + if( m_writeBuffer[m_writeBufferLen-1]==13 ) m_writeBufferLen--; + m_writeBuffer[m_writeBufferLen]=0; + m_cmd = IFD_EXEC; + } + + m_channel = 0xFF; + } + else if( m_opening ) + { + m_opening = false; + m_writeBuffer[m_writeBufferLen] = 0; + m_cmd = IFD_OPEN; + // m_channel gets set to 0xFF after IFD_OPEN is processed + } + else if( m_writeBufferLen>0 ) + { + m_cmd = IFD_WRITE; + // m_channel gets set to 0xFF after IFD_WRITE is processed + } +} + + +#if defined(SUPPORT_EPYX) && defined(SUPPORT_EPYX_SECTOROPS) +bool IECFileDevice::epyxReadSector(uint8_t track, uint8_t sector, uint8_t *buffer) +{ +#if DEBUG>0 + dbg_print_data(); + Serial.print("Read track "); Serial.print(track); Serial.print(" sector "); Serial.println(sector); + for(int i=0; i<256; i++) dbg_data(buffer[i]); + dbg_print_data(); + Serial.flush(); + return true; +#else + return false; +#endif +} + + +bool IECFileDevice::epyxWriteSector(uint8_t track, uint8_t sector, uint8_t *buffer) +{ +#if DEBUG>0 + dbg_print_data(); + Serial.print("Write track "); Serial.print(track); Serial.print(" sector "); Serial.println(sector); Serial.flush(); + for(int i=0; i<256; i++) dbg_data(buffer[i]); + dbg_print_data(); + return true; +#else + return false; +#endif +} +#endif + + +void IECFileDevice::fillReadBuffer() +{ + while( m_readBufferLen[m_channel]<2 && !m_eoi ) + { + uint8_t n = 2-m_readBufferLen[m_channel]; + n = read(m_channel, m_readBuffer[m_channel]+m_readBufferLen[m_channel], n, &m_eoi); + if( n==0 ) m_eoi = true; +#if DEBUG==1 + for(uint8_t i=0; i0 ) + { + uint8_t n = write(m_channel, m_writeBuffer, m_writeBufferLen, m_eoi); +#if DEBUG==1 + for(uint8_t i=0; i0 + for(uint8_t i=0; m_writeBuffer[i]; i++) dbg_data(m_writeBuffer[i]); + dbg_print_data(); + Serial.print(F("OPEN #")); +#if MAX_DEVICES>1 + Serial.print(m_devnr); Serial.write('#'); +#endif + Serial.print(m_channel); Serial.print(F(": ")); Serial.println((const char *) m_writeBuffer); +#endif + bool ok = open(m_channel, (const char *) m_writeBuffer); + + m_readBufferLen[m_channel] = ok ? 0 : -128; + m_writeBufferLen = 0; + m_channel = 0xFF; + break; + } + + case IFD_CLOSE: + { +#if DEBUG>0 + dbg_print_data(); + Serial.print(F("CLOSE #")); +#if MAX_DEVICES>1 + Serial.print(m_devnr); Serial.write('#'); +#endif + Serial.println(m_channel); +#endif + // note: any data that cannot be sent on at this point is lost! + emptyWriteBuffer(); + m_writeBufferLen = 0; + + close(m_channel); + m_readBufferLen[m_channel] = 0; + m_channel = 0xFF; + break; + } + + case IFD_WRITE: + { + // note: any data that cannot be sent on at this point is lost! + emptyWriteBuffer(); + m_writeBufferLen = 0; + m_channel = 0xFF; + break; + } + + case IFD_EXEC: + { + bool handled = false; + const char *cmd = (const char *) m_writeBuffer; + +#if DEBUG>0 +#if defined(SUPPORT_DOLPHIN) + // Printing debug output here may delay our response to DolphinDos + // 'XQ' and 'XZ' commands (burst mode request) too long and cause + // the C64 to time out, causing the transmission to hang + if( cmd[0]!='X' || (cmd[1]!='Q' && cmd[1]!='Z') ) +#endif + { + for(uint8_t i=0; i0 + Serial.println(F("EPYX FASTLOAD DETECTED")); +#endif + epyxLoadRequest(); + m_epyxCtr = 0; + } +#endif +#ifdef SUPPORT_DOLPHIN + if( strcmp_P(cmd, PSTR("XQ"))==0 ) + { dolphinBurstTransmitRequest(); m_channel = 0; handled = true; m_eoi = false; } + else if( strcmp_P(cmd, PSTR("XZ"))==0 ) + { dolphinBurstReceiveRequest(); m_channel = 1; handled = true; m_eoi = false; } + else if( strcmp_P(cmd, PSTR("XF+"))==0 ) + { enableDolphinBurstMode(true); setStatus(NULL, 0); handled = true; } + else if( strcmp_P(cmd, PSTR("XF-"))==0 ) + { enableDolphinBurstMode(false); setStatus(NULL, 0); handled = true; } +#endif + if( !handled ) execute(cmd, m_writeBufferLen); + m_writeBufferLen = 0; + break; + } + } + + m_cmd = IFD_NONE; +} + + +bool IECFileDevice::checkMWcmd(uint16_t addr, uint8_t len, uint8_t checksum) const +{ + // check buffer length and M-W command + if( m_writeBufferLen>8)&0xFF) || m_writeBuffer[5]!=len ) + return false; + + // check checksum + uint8_t c = 0; + for(uint8_t i=0; i0 + Serial.print(F("SETSTATUS ")); +#if MAX_DEVICES>1 + Serial.write('#'); Serial.print(m_devnr); Serial.write(' '); +#endif + Serial.println(dataLen); +#endif + + m_statusBufferPtr = 0; + m_statusBufferLen = min((uint8_t) IECFILEDEVICE_STATUS_BUFFER_SIZE, dataLen); + memcpy(m_statusBuffer, data, m_statusBufferLen); +} + + +void IECFileDevice::clearStatus() +{ + setStatus(NULL, 0); +} + + +void IECFileDevice::reset() +{ +#if DEBUG>0 +#if MAX_DEVICES>1 + Serial.write('#'); Serial.print(m_devnr); Serial.write(' '); +#endif + Serial.println(F("RESET")); +#endif + + m_statusBufferPtr = 0; + m_statusBufferLen = 0; + m_writeBufferLen = 0; + memset(m_readBufferLen, 0, 15); + m_channel = 0xFF; + m_cmd = IFD_NONE; + m_opening = false; + +#ifdef SUPPORT_EPYX + m_epyxCtr = 0; +#endif + + IECDevice::reset(); +} + + +void IECFileDevice::task() +{ + // see comment in IECFileDevice constructor + if( m_canServeATN ) fileTask(); +} diff --git a/lib/bus/iec/IECFileDevice.h b/lib/bus/iec/IECFileDevice.h new file mode 100644 index 000000000..8818d0a96 --- /dev/null +++ b/lib/bus/iec/IECFileDevice.h @@ -0,0 +1,131 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2024 David Hansel +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#ifndef IECFILEDEVICE_H +#define IECFILEDEVICE_H + +#include "IECDevice.h" + + +class IECFileDevice : public IECDevice +{ + public: + IECFileDevice(uint8_t devnr = 0xFF); + + protected: + // --- override the following functions in your device class: + + // called during IECBusHandler::begin() + virtual void begin(); + + // called during IECBusHandler::task() + virtual void task(); + + // open file "name" on channel + virtual bool open(uint8_t channel, const char *name) = 0; + + // close file on channel + virtual void close(uint8_t channel) = 0; + + // write bufferSize bytes to file on channel, returning the number of bytes written + // Returning less than bufferSize signals "cannot receive more data" for this file. + // If eoi is true then the sender has signaled that this is the final data for this transmission. + virtual uint8_t write(uint8_t channel, uint8_t *buffer, uint8_t bufferSize, bool eoi) = 0; + + // read up to bufferSize bytes from file in channel, returning the number of bytes read + // returning 0 will signal end-of-file to the receiver. Returning 0 + // for the FIRST call after open() signals an error condition + // (e.g. C64 load command will show "file not found") + // If returning a data length >0 then the device may signal end-of-data AFTER transmitting + // the data by setting *eoi to true. + virtual uint8_t read(uint8_t channel, uint8_t *buffer, uint8_t bufferSize, bool *eoi) = 0; + + // called when the bus master reads from channel 15, the status + // buffer is currently empty and getStatusData() is not overloaded. + // This should populate buffer with an appropriate status message, + // bufferSize is the maximum allowed length of the message + // the data in the buffer should be a null-terminated string + virtual void getStatus(char *buffer, uint8_t bufferSize) { *buffer=0; } + + // called when the bus master reads from channel 15 and the status + // buffer is currently empty, this should + // - fill buffer with up to bufferSize bytes of data + // - return the number of data bytes stored in the buffer + // The default implementation of getStatusData just calls getStatus(). + virtual uint8_t getStatusData(char *buffer, uint8_t bufferSize); + + // called when the bus master sends data (i.e. a command) to channel 15 + // command is a 0-terminated string representing the command to execute + // commandLen contains the full length of the received command (useful if + // the command itself may contain zeros) + virtual void execute(const char *command, uint8_t cmdLen) {} + + // called on falling edge of RESET line + virtual void reset(); + + // can be called by derived class to set the status buffer + void setStatus(const char *data, uint8_t dataLen); + + // can be called by derived class to clear the status buffer, causing readStatus() + // to be called again the next time the status channel is queried + void clearStatus(); + + // clear the internal read buffer of the given channel, calling this will ensure + // that the next TALK command will immediately call "read" to get new data instead + // of first sending the contents of the buffer + void clearReadBuffer(uint8_t channel); + +#if defined(SUPPORT_EPYX) && defined(SUPPORT_EPYX_SECTOROPS) + virtual bool epyxReadSector(uint8_t track, uint8_t sector, uint8_t *buffer); + virtual bool epyxWriteSector(uint8_t track, uint8_t sector, uint8_t *buffer); +#endif + + private: + + virtual void talk(uint8_t secondary); + virtual void listen(uint8_t secondary); + virtual void untalk(); + virtual void unlisten(); + virtual int8_t canWrite(); + virtual int8_t canRead(); + virtual void write(uint8_t data, bool eoi); + virtual uint8_t write(uint8_t *buffer, uint8_t bufferSize, bool eoi); + virtual uint8_t read(); + virtual uint8_t read(uint8_t *buffer, uint8_t bufferSize); + virtual uint8_t peek(); + + void fillReadBuffer(); + void emptyWriteBuffer(); + void fileTask(); + bool checkMWcmd(uint16_t addr, uint8_t len, uint8_t checksum) const; + + bool m_opening, m_eoi, m_canServeATN; + uint8_t m_channel, m_cmd; + uint8_t m_writeBuffer[IECFILEDEVICE_WRITE_BUFFER_SIZE]; + + uint8_t m_readBuffer[15][2]; + int8_t m_statusBufferLen, m_statusBufferPtr, m_writeBufferLen, m_readBufferLen[15]; + char m_statusBuffer[IECFILEDEVICE_STATUS_BUFFER_SIZE]; + +#ifdef SUPPORT_EPYX + uint8_t m_epyxCtr; +#endif +}; + + +#endif diff --git a/lib/bus/iec/IECespidf.h b/lib/bus/iec/IECespidf.h new file mode 100644 index 000000000..56c6905a7 --- /dev/null +++ b/lib/bus/iec/IECespidf.h @@ -0,0 +1,94 @@ +// ----------------------------------------------------------------------------- +// Copyright (C) 2024 David Hansel +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have receikved a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, +// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// ----------------------------------------------------------------------------- + +#ifndef IECESPIDF_H +#define IECESPIDF_H + +#include +#include +#include +#include +#include +#include "hal/gpio_hal.h" +#include + +#pragma GCC diagnostic ignored "-Wunused-function" +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wvolatile" + +typedef void (*interruptFcn)(void *); + +#define INPUT 0x0 +#define OUTPUT 0x1 +#define INPUT_PULLUP 0x2 +#define LOW 0x0 +#define HIGH 0x1 +#define FALLING GPIO_INTR_NEGEDGE +#define RISING GPIO_INTR_POSEDGE +#define bit(n) (1<<(n)) +#define digitalWrite(pin, v) gpio_set_level((gpio_num_t) pin, v); +#define pinMode(pin, mode) { gpio_reset_pin((gpio_num_t) pin); gpio_set_direction((gpio_num_t)pin, mode==OUTPUT ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT); if( mode==INPUT_PULLUP ) gpio_pullup_en((gpio_num_t) pin); } +#define digitalPinToGPIONumber(digitalPin) (digitalPin) +#define digitalPinToBitMask(pin) (1UL << (digitalPinToGPIONumber(pin)&31)) +#define portInputRegister(port) ((volatile uint32_t*)((port)?GPIO_IN1_REG:GPIO_IN_REG)) +#define portOutputRegister(port) ((volatile uint32_t*)((port)?GPIO_OUT1_REG:GPIO_OUT_REG)) +#define portModeRegister(port) ((volatile uint32_t*)((port)?GPIO_ENABLE1_REG:GPIO_ENABLE_REG)) +#define digitalPinToPort(pin) ((digitalPinToGPIONumber(pin)>31)?1:0) +#define digitalPinToInterrupt(p) ((((uint8_t)digitalPinToGPIONumber(p))(y) ? (x) : (y)) + +static void delayMicroseconds(uint32_t n) +{ + uint32_t s = micros(); + while((micros()-s)= SOC_GPIO_PIN_COUNT) return; + + if (!interrupt_initialized) { + esp_err_t err = gpio_install_isr_service(0 /* ESP_INTR_FLAG_IRAM */); + interrupt_initialized = (err == ESP_OK) || (err == ESP_ERR_INVALID_STATE); + } + + gpio_set_intr_type((gpio_num_t)pin, intr_type); + gpio_isr_handler_add((gpio_num_t)pin, userFunc, NULL); + + gpio_hal_context_t gpiohal; + gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0); + gpio_hal_input_enable(&gpiohal, pin); +} + +static void detachInterrupt(uint8_t pin) +{ + gpio_isr_handler_remove((gpio_num_t)pin); + gpio_set_intr_type((gpio_num_t)pin, GPIO_INTR_DISABLE); +} + +#endif diff --git a/lib/bus/iec/iec.cpp b/lib/bus/iec/iec.cpp index 9377ab80c..f3600b678 100644 --- a/lib/bus/iec/iec.cpp +++ b/lib/bus/iec/iec.cpp @@ -1,6 +1,7 @@ #ifdef BUILD_IEC #include "iec.h" +#include "../../device/iec/fuji.h" #include #include @@ -11,643 +12,120 @@ #include "../../include/debug.h" #include "../../include/pinmap.h" -#include "../../include/cbm_defines.h" -#include "led.h" -#include "protocol/_protocol.h" -#include "protocol/cpbstandardserial.h" -#include "protocol/jiffydos.h" -#include "string_utils.h" -#include "utils.h" +#include "../../hardware/led.h" -#define MAIN_STACKSIZE 4096 -#define MAIN_PRIORITY 20 +#define MAIN_STACKSIZE 32768 +#define MAIN_PRIORITY 17 #define MAIN_CPUAFFINITY 1 -#define IEC_ALLDEV 31 -#define IEC_SET_STATE(x) ({ _state = x; }) - -using namespace Protocol; - systemBus IEC; -static void IRAM_ATTR cbm_on_atn_isr_forwarder(void *arg) -{ - systemBus *b = (systemBus *)arg; - - b->cbm_on_atn_isr_handler(); -} - -void IRAM_ATTR systemBus::cbm_on_atn_isr_handler() -{ - //IEC_ASSERT(PIN_IEC_SRQ); - if (IEC_IS_ASSERTED(PIN_IEC_ATN)) - { - // Go to listener mode and get command - IEC_RELEASE(PIN_IEC_CLK_OUT); - IEC_ASSERT(PIN_IEC_DATA_OUT); - - flags = CLEAR; - flags |= ATN_ASSERTED; - IEC_SET_STATE(BUS_ACTIVE); - - // Commands are always sent using standard serial - if (detected_protocol != PROTOCOL_SERIAL) - { - detected_protocol = PROTOCOL_SERIAL; - protocol = selectProtocol(); - } - } - else if (_state == BUS_RELEASE) - { - releaseLines(); - IEC_SET_STATE(BUS_IDLE); - } - //IEC_RELEASE(PIN_DEBUG); -} - -static void IRAM_ATTR cbm_on_clk_isr_forwarder(void *arg) -{ - systemBus *b = (systemBus *)arg; - - b->cbm_on_clk_isr_handler(); -} - -void IRAM_ATTR systemBus::cbm_on_clk_isr_handler() -{ - //IEC_ASSERT(PIN_IEC_SRQ); - int atn, val; - int cmd, dev; - - if (_state < BUS_ACTIVE) - return; - - atn = IEC_IS_ASSERTED(PIN_IEC_ATN); - gpio_intr_disable(PIN_IEC_CLK_IN); - - val = protocol->receiveByte(); - if (flags & ERROR) - goto done; - - if (atn) - { - cmd = val & 0xe0; - dev = val & 0x1f; - - switch (cmd) - { - case IEC_LISTEN: - case IEC_TALK: - if (dev == IEC_ALLDEV || !isDeviceEnabled(dev)) - { - if (dev == IEC_ALLDEV) - { - // Handle releaseLines() when ATN is released outside of this - // interrupt to prevent watchdog timeout - IEC_SET_STATE(BUS_RELEASE); - } - else - { - IEC_SET_STATE(BUS_IDLE); - protocol->transferDelaySinceLast(TIMING_Tbb); - releaseLines(); - } - sendInput(); - } - else - { - newIO(val); -#ifdef JIFFYDOS - if (flags & JIFFYDOS_ACTIVE) - { - detected_protocol = PROTOCOL_JIFFYDOS; - protocol = selectProtocol(); - } +systemBus::systemBus() : IECBusHandler(PIN_IEC_ATN, PIN_IEC_CLK_OUT, PIN_IEC_DATA_OUT, + PIN_IEC_RESET==GPIO_NUM_NC ? 0xFF : PIN_IEC_RESET, + 0xFF, + PIN_IEC_SRQ==GPIO_NUM_NC ? 0xFF : PIN_IEC_SRQ) +{ +#ifdef SUPPORT_DOLPHIN +#ifdef SUPPORT_DOLPHIN_XRA1405 + setDolphinDosPins(PIN_PARALLEL_FLAG2 == GPIO_NUM_NC ? 0xFF : PIN_PARALLEL_FLAG2, + PIN_PARALLEL_PC2 == GPIO_NUM_NC ? 0xFF : PIN_PARALLEL_PC2, + PIN_SD_HOST_SCK == GPIO_NUM_NC ? 0xFF : PIN_SD_HOST_SCK, + PIN_SD_HOST_MOSI == GPIO_NUM_NC ? 0xFF : PIN_SD_HOST_MOSI, + PIN_SD_HOST_MISO == GPIO_NUM_NC ? 0xFF : PIN_SD_HOST_MISO, + PIN_XRA1405_CS == GPIO_NUM_NC ? 0xFF : PIN_XRA1405_CS); +#else +#error "Can only support DolphinDos using XRA1405 port expander" #endif - } - break; - - case IEC_REOPEN: - /* Take a break driver 8. We can reach our destination, but we're still a ways away */ - if (iec_curCommand) - { - channelIO(val); - if (iec_curCommand->primary == IEC_TALK) - { - IEC_SET_STATE(BUS_IDLE); - turnAround(); - sendInput(); - } - } - break; - - case IEC_CLOSE: - if (iec_curCommand) - { - channelIO(val); - if (dev == 0x00) - sendInput(); - } - break; - - default: - break; - } - } - else if (iec_curCommand) - iec_curCommand->payload += val & 0xff; - -done: - gpio_intr_enable(PIN_IEC_CLK_IN); - //IEC_RELEASE(PIN_DEBUG); - return; -} - -void IRAM_ATTR systemBus::newIO(int val) -{ - iec_curCommand = new IECData(); - iec_curCommand->primary = val & 0xe0; - iec_curCommand->device = val & 0x1f; - iec_curCommand->payload = ""; - - return; -} - -void IRAM_ATTR systemBus::channelIO(int val) -{ - iec_curCommand->secondary = val & 0xf0; - iec_curCommand->channel = val & 0x0f; - return; -} - -void IRAM_ATTR systemBus::sendInput(void) -{ - BaseType_t woken; - - // IEC_ASSERT(PIN_DEBUG); - if (iec_curCommand) - xQueueSendFromISR(iec_commandQueue, &iec_curCommand, &woken); - iec_curCommand = nullptr; - // IEC_RELEASE(PIN_DEBUG); - - return; -} - -/** - * Static callback function for the interrupt rate limiting timer. It sets the interruptProceed - * flag to true. This is set to false when the interrupt is serviced. - */ -static void onTimer(void *info) -{ - systemBus *parent = (systemBus *)info; - // portENTER_CRITICAL_ISR(&parent->timerMux); - parent->interruptSRQ = !parent->interruptSRQ; - // portEXIT_CRITICAL_ISR(&parent->timerMux); -} - -#if 0 -static void ml_iec_intr_task(void* arg) -{ - while ( true ) - { - if ( IEC.enabled ) - IEC.service(); - - taskYIELD(); - } -} #endif - -void systemBus::init_gpio(gpio_num_t _pin) -{ - PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[_pin], PIN_FUNC_GPIO); - gpio_set_direction(_pin, GPIO_MODE_INPUT); - gpio_pullup_en(_pin); - gpio_set_pull_mode(_pin, GPIO_PULLUP_ONLY); - gpio_set_level(_pin, 0); - return; } +// static void ml_iec_intr_task(void* arg) +// { +// while ( true ) +// { +// IEC.service(); +// taskYIELD(); // Allow other tasks to run +// } +// } + +// void init_gpio(gpio_num_t _pin) +// { +// PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[_pin], PIN_FUNC_GPIO); +// gpio_set_direction(_pin, GPIO_MODE_INPUT); +// gpio_pullup_en(_pin); +// gpio_set_pull_mode(_pin, GPIO_PULLUP_ONLY); +// gpio_set_level(_pin, 0); +// return; +// } + void systemBus::setup() { - Debug_printf("IEC systemBus::setup()\r\n"); - - flags = CLEAR; - protocol = selectProtocol(); - - // initial pin modes in GPIO - init_gpio(PIN_IEC_ATN); - init_gpio(PIN_IEC_CLK_IN); - init_gpio(PIN_IEC_CLK_OUT); - init_gpio(PIN_IEC_DATA_IN); - init_gpio(PIN_IEC_DATA_OUT); - init_gpio(PIN_IEC_SRQ); -#ifdef IEC_HAS_RESET - init_gpio(PIN_IEC_RESET); + Debug_printf("IEC systemBus::setup()\r\n"); + begin(); +#ifdef SUPPORT_JIFFY + Debug_printf("JiffyDOS protocol supported\r\n"); #endif - -#ifdef IEC_INVERTED_LINES -#warning intr_type likely needs to be fixed! +#ifdef SUPPORT_EPYX + Debug_printf("Epyx FastLoad protocol supported\r\n"); +#endif +#ifdef SUPPORT_DOLPHIN + Debug_printf("DolphinDOS protocol supported\r\n"); #endif - iec_commandQueue = xQueueCreate(10, sizeof(IECData *)); - - // Start task - // xTaskCreatePinnedToCore(ml_iec_intr_task, "ml_iec_intr_task", 4096, NULL, 20, NULL, 1); - - // Setup interrupt for ATN - gpio_config_t io_conf = { - .pin_bit_mask = (1ULL << PIN_IEC_ATN), // bit mask of the pins that you want to set - .mode = GPIO_MODE_INPUT, // set as input mode - .pull_up_en = GPIO_PULLUP_DISABLE, // disable pull-up mode - .pull_down_en = GPIO_PULLDOWN_DISABLE, // disable pull-down mode - .intr_type = GPIO_INTR_ANYEDGE // interrupt of any edge - }; - gpio_config(&io_conf); - gpio_isr_handler_add((gpio_num_t)PIN_IEC_ATN, cbm_on_atn_isr_forwarder, this); +// // initial pin modes in GPIO +// init_gpio(PIN_IEC_ATN); +// init_gpio(PIN_IEC_CLK_IN); +// init_gpio(PIN_IEC_CLK_OUT); +// init_gpio(PIN_IEC_DATA_IN); +// init_gpio(PIN_IEC_DATA_OUT); +// init_gpio(PIN_IEC_SRQ); +// #ifdef IEC_HAS_RESET +// init_gpio(PIN_IEC_RESET); +// #endif - // Setup interrupt config for CLK - io_conf = { - .pin_bit_mask = (1ULL << PIN_IEC_CLK_IN), // bit mask of the pins that you want to set - .mode = GPIO_MODE_INPUT, // set as input mode - .pull_up_en = GPIO_PULLUP_DISABLE, // disable pull-up mode - .pull_down_en = GPIO_PULLDOWN_DISABLE, // disable pull-down mode #ifdef IEC_INVERTED_LINES - .intr_type = GPIO_INTR_NEGEDGE // interrupt of falling edge -#else - .intr_type = GPIO_INTR_POSEDGE // interrupt of rising edge +#warning intr_type likely needs to be fixed! #endif - }; - gpio_config(&io_conf); - gpio_isr_handler_add((gpio_num_t)PIN_IEC_CLK_IN, cbm_on_clk_isr_forwarder, this); - - // Start SRQ timer service - //timer_start_srq(); -} -void IRAM_ATTR systemBus::service() -{ - IECData *received; - - if (!xQueueReceive(iec_commandQueue, &received, 0)) - return; - - received->debugPrint(); - - auto d = deviceById(received->device); - if (d != nullptr) - { - d->commanddata = *received; - d->process(); - } - - // Command was processed, clear it out - delete received; - - return; -} + // Start task + // Create a new high-priority task to handle the main service loop + // This is assigned to CPU1; the WiFi task ends up on CPU0 + //xTaskCreatePinnedToCore(ml_iec_intr_task, "ml_iec_intr_task", MAIN_STACKSIZE, NULL, MAIN_PRIORITY, NULL, MAIN_CPUAFFINITY); -/** - * Start the Interrupt rate limiting timer - */ -void systemBus::timer_start_srq() -{ - esp_timer_create_args_t tcfg; - tcfg.arg = this; - tcfg.callback = onTimer; - tcfg.dispatch_method = esp_timer_dispatch_t::ESP_TIMER_TASK; - tcfg.name = nullptr; - esp_timer_create(&tcfg, &rateTimerHandle); - esp_timer_start_periodic(rateTimerHandle, timerRate * 1000); } -/** - * Stop the Interrupt rate limiting timer - */ -void systemBus::timer_stop_srq() -{ - // Delete existing timer - if (rateTimerHandle != nullptr) - { - Debug_println("Deleting existing rateTimer\r\n"); - esp_timer_stop(rateTimerHandle); - esp_timer_delete(rateTimerHandle); - rateTimerHandle = nullptr; - } -} -std::shared_ptr systemBus::selectProtocol() +void systemBus::service() { - // Debug_printv("protocol[%d]", detected_protocol); - - switch (detected_protocol) + task(); + + bool error = false, active = false; + for(int i = 0; i < MAX_DISK_DEVICES; i++) { -#ifdef JIFFYDOS - case PROTOCOL_JIFFYDOS: - { - auto p = std::make_shared(); - return std::static_pointer_cast(p); - } -#endif -#ifdef PARALLEL_BUS - case PROTOCOL_DOLPHINDOS: - { - auto p = std::make_shared(); - return std::static_pointer_cast(p); - } -#endif - default: - { -#ifdef PARALLEL_BUS - PARALLEL.state = PBUS_IDLE; -#endif - auto p = std::make_shared(); - return std::static_pointer_cast(p); - } + iecDrive *d = &(theFuji.get_disks(i)->disk_dev); + error |= d->hasError(); + active |= d->getNumOpenChannels()>0; } -} -systemBus virtualDevice::get_bus() { return IEC; } - -device_state_t virtualDevice::process() -{ - switch ((bus_command_t)commanddata.primary) + if( error ) { - case bus_command_t::IEC_LISTEN: - switch (commanddata.secondary) + static bool flashState = false; + static uint32_t prevFlash = 0; + if( (fnSystem.millis()-prevFlash) > 250 ) { - case bus_command_t::IEC_OPEN: - payload = commanddata.payload; - state = openChannel(/*commanddata.channel, payload*/); - break; - case bus_command_t::IEC_CLOSE: - state = closeChannel(/*commanddata.channel*/); - break; - case bus_command_t::IEC_REOPEN: - payload = commanddata.payload; - state = writeChannel(/*commanddata.channel, payload*/); - break; + flashState = !flashState; + fnLedManager.set(eLed::LED_BUS, flashState); + prevFlash = fnSystem.millis(); } - break; - - case bus_command_t::IEC_TALK: - if (commanddata.secondary == bus_command_t::IEC_REOPEN) - state = readChannel(/*commanddata.channel*/); - break; - - default: - break; - } - - return state; -} - -// This is only used in iecDrive, and it has its own implementation -void virtualDevice::iec_talk_command_buffer_status() -{ - // Removed implementation as it wasn't being used -} - -void virtualDevice::dumpData() -{ - Debug_printf("%9s: %02X\r\n", "Primary", commanddata.primary); - Debug_printf("%9s: %02u\r\n", "Device", commanddata.device); - Debug_printf("%9s: %02X\r\n", "Secondary", commanddata.secondary); - Debug_printf("%9s: %02u\r\n", "Channel", commanddata.channel); - Debug_printf("%9s: %s\r\n", "Payload", commanddata.payload.c_str()); -} - -void systemBus::assert_interrupt() -{ - if (interruptSRQ) - IEC_ASSERT(PIN_IEC_SRQ); - else - IEC_RELEASE(PIN_IEC_SRQ); -} - -bool systemBus::sendByte(const char c, bool eoi) { return protocol->sendByte(c, eoi); } - -size_t systemBus::sendBytes(const char *buf, size_t len, bool eoi) -{ - size_t i; - - for (i = 0; i < len; i++) - { - if (!sendByte(buf[i], eoi && i == (len - 1))) - break; - } - - return i; -} - -size_t systemBus::sendBytes(std::string s, bool eoi) { return sendBytes(s.c_str(), s.size(), eoi); } - -bool IRAM_ATTR systemBus::turnAround() -{ - /* - TURNAROUND - An unusual sequence takes place following ATN if the computer - wishes the remote device to become a talker. This will usually - take place only after a Talk command has been sent. Immediately - after ATN is RELEASED, the selected device will be behaving like a - listener. After all, it's been listening during the ATN cycle, and - the computer has been a talker. At this instant, we have "wrong - way" logic; the device is asserting the Data line, and the - computer is asserting the Clock line. We must turn this around. - - Here's the sequence: - - 1. The computer asserts the Data line (it's already there), as - well as releases the Clock line. - - 2. When the device sees the Clock line is releaseed, it releases - the Data line (which stays asserted anyway since the computer - is asserting it) and then asserts the Clock line. - - We're now in our starting position, with the talker (that's the - device) asserting the Clock, and the listener (the computer) - asserting the Data line. The computer watches for this state; - only when it has gone through the cycle correctly will it be ready - to receive data. And data will be signalled, of course, with the - usual sequence: the talker releases the Clock line to signal that - it's ready to send. - */ - - // Wait for ATN to be released - if (protocol->waitForSignals(PIN_IEC_ATN, IEC_RELEASED, 0, 0, FOREVER) == TIMED_OUT) - { - flags |= ERROR; - return false; - } - - // Wait for CLK to be released - if (protocol->waitForSignals(PIN_IEC_CLK_IN, IEC_RELEASED, 0, 0, TIMEOUT_Ttlta) == TIMED_OUT) - { - flags |= ERROR; - return false; // return error because timeout - } - IEC_RELEASE(PIN_IEC_DATA_OUT); - usleep(TIMING_Ttca); - IEC_ASSERT(PIN_IEC_CLK_OUT); - - // 80us minimum delay after TURNAROUND - // *** IMPORTANT! - usleep(TIMING_Tda); - - return true; -} // turnAround - -void systemBus::reset_all_our_devices() -{ - // TODO iterate through our bus and send reset to each device. -} - -void systemBus::setBitTiming(std::string set, int p1, int p2, int p3, int p4) -{ - uint8_t i = 0; // Send - if (mstr::equals(set, (char *) "r")) i = 1; - if (p1) protocol->bit_pair_timing[i][0] = p1; - if (p2) protocol->bit_pair_timing[i][1] = p2; - if (p3) protocol->bit_pair_timing[i][2] = p3; - if (p4) protocol->bit_pair_timing[i][3] = p4; - - Debug_printv("i[%d] timing[%d][%d][%d][%d]", i, - protocol->bit_pair_timing[i][0], - protocol->bit_pair_timing[i][1], - protocol->bit_pair_timing[i][2], - protocol->bit_pair_timing[i][3]); -} - -void IRAM_ATTR systemBus::releaseLines(bool wait) -{ - // Release lines - IEC_RELEASE(PIN_IEC_CLK_OUT); - IEC_RELEASE(PIN_IEC_DATA_OUT); - IEC_SET_STATE(BUS_IDLE); - - // Wait for ATN to release and quit - if (wait) - { - Debug_printv("Waiting for ATN to release"); - protocol->waitForSignals(PIN_IEC_ATN, IEC_RELEASED, 0, 0, TIMEOUT_DEFAULT); } + else + fnLedManager.set(eLed::LED_BUS, active); } -void IRAM_ATTR systemBus::senderTimeout() -{ - releaseLines(); - IEC_SET_STATE(BUS_ERROR); - - usleep(TIMING_EMPTY); - IEC_ASSERT(PIN_IEC_DATA_OUT); -} // senderTimeout - -void systemBus::addDevice(virtualDevice *pDevice, int device_id) -{ - if (!pDevice) - { - Debug_printf("systemBus::addDevice() pDevice == nullptr! returning.\r\n"); - return; - } - - // TODO, add device shortcut pointer logic like others - Serial.printf("Device #%02d Ready!\r\n", device_id); - - pDevice->_devnum = device_id; - _daisyChain.push_front(pDevice); - enabledDevices |= 1UL << device_id; -} - -void systemBus::remDevice(virtualDevice *pDevice) -{ - if (!pDevice) - { - Debug_printf("system Bus::remDevice() pDevice == nullptr! returning\r\n"); - return; - } - - _daisyChain.remove(pDevice); - enabledDevices &= ~(1UL << pDevice->_devnum); -} - -bool systemBus::isDeviceEnabled(const uint8_t device_id) { return (enabledDevices & (1 << device_id)); } // isDeviceEnabled - -void systemBus::changeDeviceId(virtualDevice *pDevice, int device_id) -{ - if (!pDevice) - { - Debug_printf("systemBus::changeDeviceId() pDevice == nullptr! returning.\r\n"); - return; - } - - for (auto devicep : _daisyChain) - { - if (devicep == pDevice) - devicep->_devnum = device_id; - } -} - -virtualDevice *systemBus::deviceById(int device_id) -{ - for (auto devicep : _daisyChain) - { - if (devicep->_devnum == device_id) - return devicep; - } - return nullptr; -} void systemBus::shutdown() { - shuttingDown = true; - - for (auto devicep : _daisyChain) - { - Debug_printf("Shutting down device #%02d\r\n", devicep->id()); - devicep->shutdown(); - } - Debug_printf("All devices shut down.\r\n"); -} - -enum -{ - IECOpenChannel = 0, - IECCloseChannel, - IECReadChannel, - IECWriteChannel, -}; - -static const char *IECCommandNames[] = { - "UNKNOWN", "LISTEN", "TALK", "REOPEN", "OPEN", "CLOSE", "READ", "WRITE", -}; - -int IECData::channelCommand() -{ - switch (primary) - { - case IEC_LISTEN: - switch (secondary) - { - case IEC_OPEN: - return IECOpenChannel; - case IEC_CLOSE: - return IECCloseChannel; - case IEC_REOPEN: - return IECWriteChannel; - } - break; - - case IEC_TALK: - return IECReadChannel; - } - - return 0; } -void IECData::debugPrint() -{ - Debug_printf("IEC %2i %-5s %2i: %-6s [%02X %02X]\r\n", device, IECCommandNames[channelCommand() + 4], channel, IECCommandNames[primary >> 5], primary, secondary); - if (payload.size()) - Debug_printf("%s", util_hexdump(payload.data(), payload.size()).c_str()); -} #endif /* BUILD_IEC */ diff --git a/lib/bus/iec/iec.h b/lib/bus/iec/iec.h index 5fdded415..fddcf2312 100644 --- a/lib/bus/iec/iec.h +++ b/lib/bus/iec/iec.h @@ -1,49 +1,10 @@ #ifndef IEC_H #define IEC_H -// This code uses code from the Meatloaf Project: -// Meatloaf - A Commodore 64/128 multi-device emulator -// https://github.com/idolpx/meatloaf -// Copyright(C) 2020 James Johnston -// -// Meatloaf is free software : you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Meatloaf is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Meatloaf. If not, see . - -// -// https://www.pagetable.com/?p=1135 -// https://pagetable.com/c64ref/c64disasm/#ED40 -// http://unusedino.de/ec64/technical/misc/c1541/romlisting.html#E85B -// https://eden.mose.org.uk/gitweb/?p=rom-reverse.git;a=blob;f=src/vic-1541-sfd.asm;hb=HEAD -// https://www.pagetable.com/docs/Inside%20Commodore%20DOS.pdf -// http://www.ffd2.com/fridge/docs/1541dis.html#E853 -// http://unusedino.de/ec64/technical/aay/c1541/ -// http://unusedino.de/ec64/technical/aay/c1581/ -// http://www.bitcity.de/1541%20Serial%20Interface.htm -// http://www.bitcity.de/theory.htm -// https://comp.sys.cbm.narkive.com/ebz1uFEx/annc-vip-the-virtual-iec-peripheral -// https://www.djupdal.org/cbm/iecata/ -// https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/dedic_gpio.html -// https://esp32.com/viewtopic.php?t=27963 -// https://github.com/alwint3r/esp32-read-multiple-gpio-pins -// https://github.com/alwint3r/esp32-set-clear-multiple-gpio -// https://www.commodore.ca/wp-content/uploads/2018/11/Commodore-IEC-Serial-Bus-Manual-C64-Plus4.txt -// - #include #include #include #include -#include #include #include #include @@ -51,17 +12,22 @@ #include #include #include -#include - #include "fnSystem.h" +#include + #include "../../../include/debug.h" #include "../../../include/pinmap.h" +//#include "IECHost.h" +#include "IECDevice.h" +#include "IECBusHandler.h" + #define BUS_DEVICEID_PRINTER 4 #define BUS_DEVICEID_DISK 8 #define BUS_DEVICEID_NETWORK 16 + /** * @brief The command frame */ @@ -82,409 +48,15 @@ union cmdFrame_t } __attribute__((packed)); }; -// Return values for service: -typedef enum -{ - BUS_OFFLINE = -4, // Bus is empty - BUS_RESET = -3, // The bus is in a reset state (RESET line). - BUS_ERROR = -2, // A problem occoured, reset communication - BUS_RELEASE = -1, // Clean Up - BUS_IDLE = 0, // Nothing recieved of our concern - BUS_ACTIVE = 1, // ATN is asserted and a command byte is expected - BUS_PROCESS = 2, // A command is ready to be processed -} bus_state_t; - -/** - * @enum bus command - */ -typedef enum -{ - IEC_GLOBAL = 0x00, // 0x00 + cmd (global command) - IEC_LISTEN = 0x20, // 0x20 + device_id (LISTEN) (0-30) - IEC_UNLISTEN = 0x3F, // 0x3F (UNLISTEN) - IEC_TALK = 0x40, // 0x40 + device_id (TALK) (0-30) - IEC_UNTALK = 0x5F, // 0x5F (UNTALK) - IEC_REOPEN = 0x60, // 0x60 + channel (OPEN CHANNEL) (0-15) - IEC_LOAD = 0x61, // 0x61 + channel (OPEN CHANNEL) (0-15) - JIFFYDOS/DOLPHINDOS LOAD PROTOCOL - IEC_CLOSE = 0xE0, // 0xE0 + channel (CLOSE NAMED CHANNEL) (0-15) - IEC_OPEN = 0xF0 // 0xF0 + channel (OPEN NAMED CHANNEL) (0-15) -} bus_command_t; - -typedef enum -{ - DEVICE_ERROR = -1, - DEVICE_IDLE = 0, // Ready and waiting - DEVICE_ACTIVE = 1, - DEVICE_LISTEN = 2, // A command is recieved and data is coming to us - DEVICE_TALK = 3, // A command is recieved and we must talk now - DEVICE_PAUSED = 4, // Execute device command -} device_state_t; - -typedef enum { - PROTOCOL_SERIAL, - PROTOCOL_FAST_SERIAL, - PROTOCOL_SAUCEDOS, - PROTOCOL_JIFFYDOS, - PROTOCOL_EPYXFASTLOAD, - PROTOCOL_WARPSPEED, - PROTOCOL_SPEEDDOS, - PROTOCOL_DOLPHINDOS, - PROTOCOL_WIC64, - PROTOCOL_IEEE488 -} bus_protocol_t; - -/** - * @class IECData - * @brief the IEC command data passed to devices - */ -class IECData -{ -public: - /** - * @brief the primary command byte - */ - uint8_t primary = 0; - /** - * @brief the secondary command byte - */ - uint8_t secondary = 0; - /** - * @brief the primary device number - */ - uint8_t device = 0; - /** - * @brief the secondary command channel - */ - uint8_t channel = 0; - /** - * @brief the device command - */ - std::string payload = ""; - /** - * @brief the raw bytes received for the command - */ - std::vector payload_raw; - /** - * @brief clear and initialize IEC command data - */ - void init(void) - { - primary = 0; - device = 0; - secondary = 0; - channel = 0; - payload.clear(); - payload_raw.clear(); - } - - int channelCommand(); - void debugPrint(); -}; - -/** - * @class Forward declaration of System Bus - */ -class systemBus; - -/** - * @class virtualDevice - * @brief All #FujiNet devices derive from this. - */ -class virtualDevice -{ -private: - /** - * @brief All IEC devices repeatedly call this routine to fan out to other methods for each command. - * This is typcially implemented as a switch() statement. - * @return new device state. - */ - virtual device_state_t process(); - -protected: - friend systemBus; /* Because we connect to it. */ - - /** - * @brief The device number (ID) - */ - int _devnum; - - /** - * @brief The passed in command frame, copied. - */ - cmdFrame_t cmdFrame; - - /** - * @brief The current device command - */ - std::string payload; - - /** - * @brief The current device command in raw PETSCII. Used when payload is converted to ASCII for Basic commands - */ - std::string payloadRaw; - - /** - * @brief pointer to the current command data - */ - IECData commanddata; - - /** - * @brief current device state. - */ - device_state_t state; - - /** - * @brief response queue (e.g. INPUT) - * @deprecated remove as soon as it's out of fuji. - */ - std::queue response_queue; - - /** - * @brief tokenized payload - */ - std::vector pt; - std::vector pti; - - /** - * @brief The status information to send back on cmd input - * @param error = the latest error status - * @param msg = most recent status message - * @param connected = is most recent channel connected? - * @param channel = channel of most recent status msg. - */ - struct _iecStatus - { - int8_t error; - uint8_t cmd; - std::string msg; - bool connected; - int channel; - } iecStatus; - - /** - * @brief Get device ready to handle next phase of command. - */ - device_state_t queue_command(const IECData &data) - { - commanddata = data; - - if (commanddata.primary == IEC_LISTEN) - state = DEVICE_LISTEN; - else if (commanddata.primary == IEC_UNLISTEN) - state = DEVICE_IDLE; - else if (commanddata.primary == IEC_TALK) - state = DEVICE_TALK; - else if (commanddata.primary == IEC_UNTALK) - state = DEVICE_IDLE; - - return state; - } - - virtual device_state_t openChannel(/*int chan, IECPayload &payload*/) = 0; - virtual device_state_t closeChannel(/*int chan*/) = 0; - virtual device_state_t readChannel(/*int chan*/) = 0; - virtual device_state_t writeChannel(/*int chan, IECPayload &payload*/) = 0; - - /** - * @brief poll whether interrupt should be wiggled - * @param c secondary channel (0-15) - */ - virtual void poll_interrupt(unsigned char c) {} - - /** - * @brief Dump the current IEC frame to terminal. - */ - void dumpData(); - - /** - * @brief If response queue is empty, Return 1 if ANY receive buffer has data in it, else 0 - */ - virtual void iec_talk_command_buffer_status(); - - // Optional shutdown/reboot cleanup routine - virtual void shutdown(){}; - -public: - /** - * @brief get the IEC device Number (1-31) - * @return The device number registered for this device - */ - int id() { return _devnum; }; - - /** - * @brief Is this virtualDevice holding the virtual disk drive used to boot CONFIG? - */ - bool is_config_device = false; - - /** - * @brief is device active (turned on?) - */ - bool device_active = true; - - /** - * The spinlock for the ESP32 hardware timers. Used for interrupt rate limiting. - */ - portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; - - /** - * @brief Get the systemBus object that this virtualDevice is attached to. - */ - systemBus get_bus(); - - void set_iec_status(int8_t error, uint8_t cmd, const std::string msg, bool connected, int channel) { - iecStatus.error = error; - iecStatus.cmd = cmd; - iecStatus.msg = msg; - iecStatus.connected = connected; - iecStatus.channel = channel; - } - - // TODO: does this need to translate the message to PETSCII? - std::vector iec_status_to_vector() { - std::vector data; - data.push_back(static_cast(iecStatus.error)); - data.push_back(iecStatus.cmd); - data.push_back(iecStatus.connected ? 1 : 0); - data.push_back(static_cast(iecStatus.channel & 0xFF)); // it's only an int because of atoi from some basic commands, but it's never really more than 1 byte - - // max of 41 chars in message including the null terminator. It will simply be truncated, so if we find any that are excessive, should trim them down in firmware - size_t actualLength = std::min(iecStatus.msg.length(), static_cast(40)); - for (size_t i = 0; i < actualLength; ++i) { - data.push_back(static_cast(iecStatus.msg[i])); - } - data.push_back(0); // null terminate the string - - return data; - } - - void reset_state() { - payload.clear(); - std::queue().swap(response_queue); - pt.clear(); - pt.shrink_to_fit(); - } -}; - -namespace Protocol { - class IECProtocol; - class CPBStandardSerial; - class JiffyDOS; -} -class oiecstream; /** * @class systemBus * @brief the system bus that all virtualDevices attach to. */ -class systemBus +class systemBus : public IECBusHandler { -friend Protocol::IECProtocol; -friend Protocol::CPBStandardSerial; -friend Protocol::JiffyDOS; -friend oiecstream; - -private: - /** - * @brief current bus state - */ - bus_state_t _state; - - /** - * @brief data about current bus transaction - */ - IECData *iec_curCommand; - QueueHandle_t iec_commandQueue; - - /** - * @brief The chain of devices on the bus. - */ - std::forward_list _daisyChain; - - /** - * @brief Number of devices on bus - */ - int _num_devices = 0; - - /** - * @brief the active device being process()'ed - */ - virtualDevice *_activeDev = nullptr; - - /** - * @brief is device shutting down? - */ - bool shuttingDown = false; - - /** - * @brief the detected bus protocol - */ - bus_protocol_t detected_protocol = PROTOCOL_SERIAL; // default is IEC Serial - - /** - * @brief the active bus protocol - */ - std::shared_ptr protocol = nullptr; - - /** - * @brief Switch to detected bus protocol - */ - std::shared_ptr selectProtocol(); - - /** - * @brief bus flags - */ - uint16_t flags = 0;//CLEAR; - - void newIO(int val); - void channelIO(int val); - void sendInput(); - - /** - * BUS TURNAROUND (switch from listener to talker) - */ - bool turnAround(); - - /** - * ESP timer handle for the Interrupt rate limiting timer - */ - esp_timer_handle_t rateTimerHandle = nullptr; - - /** - * Timer Rate for interrupt timer - */ - int timerRate = 100; - - /** - * @brief Start the Interrupt rate limiting timer - */ - void timer_start_srq(); - - /** - * @brief Stop the Interrupt rate limiting timer - */ - void timer_stop_srq(); - public: - /** - * @brief bus enabled - */ - bool enabled = true; - - /** - * @brief vic20 mode enables faster valid bit timing - */ - bool vic20_mode = false; - - /** - * Toggled by the rate limiting timer to indicate that the SRQ interrupt should - * be pulsed. - */ - bool interruptSRQ = false; - - /** - * @brief Enabled device bits - */ - uint32_t enabledDevices; + systemBus(); /** * @brief called in main.cpp to set up the bus. @@ -496,124 +68,23 @@ friend oiecstream; */ void service(); - /** - * @brief Called to pulse the PROCEED interrupt, rate limited by the interrupt timer. - */ - void assert_interrupt(); - - /** - * @brief Release the bus lines, we're done. - */ - void releaseLines(bool wait = false); - - /** - * @brief Set 2bit fast loader pair timing - * @param set Send 's', Receive 'r' - * @param p1 Pair 1 - * @param p2 Pair 2 - * @param p3 Pair 3 - * @param p4 Pair 4 - */ - void setBitTiming(std::string set, int p1 = 0, int p2 = 0, int p3 = 0, int p4 = 0); - - /** - * @brief send single byte - * @param c byte to send - * @param eoi Send EOI? - * @return true on success, false on error - */ - bool sendByte(const char c, bool eoi = false); - - /** - * @brief Send bytes to bus - * @param buf buffer to send - * @param len length of buffer - * @param eoi Send EOI? - * @return true on success, false on error - */ - size_t sendBytes(const char *buf, size_t len, bool eoi = true); - - /** - * @brief Send string to bus - * @param s std::string to send - * @param eoi Send EOI? - * @return true on success, false on error - */ - size_t sendBytes(std::string s, bool eoi = true); - - /** - * @brief called in response to RESET pin being asserted. - */ - void reset_all_our_devices(); - /** * @brief called from main shutdown to clean up the device. */ void shutdown(); - /** - * @brief Return number of devices on bus. - * @return # of devices on bus. - */ - int numDevices() { return _num_devices; }; - - /** - * @brief Add device to bus. - * @param pDevice Pointer to virtualDevice - * @param device_id The ID to assign to virtualDevice - */ - void addDevice(virtualDevice *pDevice, int device_id); - - /** - * @brief Remove device from bus - * @param pDevice pointer to virtualDevice - */ - void remDevice(virtualDevice *pDevice); - - /** - * @brief Check if device is enabled - * @param deviceNumber The device ID to check - */ - bool isDeviceEnabled ( const uint8_t device_id ); - - /** - * @brief Return pointer to device given ID - * @param device_id ID of device to return. - * @return pointer to virtualDevice - */ - virtualDevice *deviceById(int device_id); - - /** - * @brief Change ID of a particular virtualDevice - * @param pDevice pointer to virtualDevice - * @param device_id new device ID - */ - void changeDeviceId(virtualDevice *pDevice, int device_id); - /** * @brief Are we shutting down? * @return value of shuttingDown */ bool getShuttingDown() { return shuttingDown; } + private: /** - * @brief signal to bus that we timed out. + * @brief is device shutting down? */ - void senderTimeout(); - - // FIXME - these should be private - void cbm_on_atn_isr_handler(); - void cbm_on_clk_isr_handler(); - - void init_gpio(gpio_num_t _pin); -#if IEC_ASSERT_RELEASE_AS_FUNCTIONS - void assert ( uint8_t _pin ); - void release ( uint8_t _pin ); - bool status ( uint8_t _pin ); -#endif - bool status (); + bool shuttingDown = false; - void debugTiming(); }; /** * @brief Return diff --git a/lib/bus/iec/ieee-488.cpp b/lib/bus/iec/ieee-488.cpp deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/bus/iec/ieee-488.h b/lib/bus/iec/ieee-488.h deleted file mode 100644 index ed381079d..000000000 --- a/lib/bus/iec/ieee-488.h +++ /dev/null @@ -1,4 +0,0 @@ -// -// https://www.pagetable.com/?p=1023 -// https://ia601902.us.archive.org/9/items/PET_and_the_IEEE488_Bus_1980_McGraw-Hill/PET_and_the_IEEE488_Bus_1980_McGraw-Hill.pdf -// diff --git a/lib/bus/iec/parallel.cpp b/lib/bus/iec/parallel.cpp deleted file mode 100644 index 58ded55cc..000000000 --- a/lib/bus/iec/parallel.cpp +++ /dev/null @@ -1,430 +0,0 @@ - -#ifdef PARALLEL_BUS - -#include "parallel.h" - -#include -#include - -/* Dependencies */ -#include "gpiox.h" // Required for PCF8575/PCA9674/MCP23017 - -#include "iec.h" -#include "../../include/pinmap.h" -#include "../../include/debug.h" - -parallelBus PARALLEL; - -//I2C_t& myI2C = i2c0; // i2c0 and i2c1 are the default objects - -static QueueHandle_t ml_parallel_evt_queue = NULL; - -static void IRAM_ATTR ml_parallel_isr_handler(void* arg) -{ - // Generic default interrupt handler - uint32_t gpio_num = (uint32_t) arg; - xQueueSendFromISR(ml_parallel_evt_queue, &gpio_num, NULL); -} - -static void ml_parallel_intr_task(void* arg) -{ - uint32_t gpio_num; - - while ( true ) - { - if(xQueueReceive(ml_parallel_evt_queue, &gpio_num, portMAX_DELAY)) - { - PARALLEL.service(); - } - - //taskYIELD(); - } -} - -void parallelBus::service() -{ - PARALLEL.state = PBUS_IDLE; - - Debug_printv( "User Port Data Interrupt Received!" ); - - //Debug_printv("state[%d]", IEC.state); - if ( IEC.state > BUS_OFFLINE ) // Is C64 is powered on? - { - // Update flags and data - PARALLEL.readByte(); - Debug_printv("receive <<< " BYTE_TO_BINARY_PATTERN " (%0.2d) " BYTE_TO_BINARY_PATTERN " (%0.2d)", BYTE_TO_BINARY(PARALLEL.flags), PARALLEL.flags, BYTE_TO_BINARY(PARALLEL.data), PARALLEL.data); - - // If PC2 is set then parallel is active and a byte is ready to be read! - if ( PARALLEL.status( PC2 ) ) - { - PARALLEL.state = PBUS_PROCESS; - //Debug_printv("receive <<< " BYTE_TO_BINARY_PATTERN " (%0.2d) " BYTE_TO_BINARY_PATTERN " (%0.2d)", BYTE_TO_BINARY(PARALLEL.flags), PARALLEL.flags, BYTE_TO_BINARY(PARALLEL.data), PARALLEL.data); - } - - // // Set RECEIVE/SEND mode - // if ( PARALLEL.status( PA2 ) ) - // { - // PARALLEL.mode = MODE_RECEIVE; - // GPIOX.portMode( USERPORT_DATA, GPIOX_MODE_INPUT ); - // PARALLEL.readByte(); - - // Debug_printv("receive <<< " BYTE_TO_BINARY_PATTERN " (%0.2d) " BYTE_TO_BINARY_PATTERN " (%0.2d)", BYTE_TO_BINARY(PARALLEL.flags), PARALLEL.flags, BYTE_TO_BINARY(PARALLEL.data), PARALLEL.data); - - // // // DolphinDOS Detection - // // if ( PARALLEL.status( ATN ) ) - // // { - // // if ( IEC.data.secondary == IEC_OPEN || IEC.data.secondary == IEC_REOPEN ) - // // { - // // IEC.protocol->flags xor_eq PARALLEL_ACTIVE; - // // Debug_printv("dolphindos"); - // // } - // // } - - // // // WIC64 - // // if ( PARALLEL.status( PC2 ) ) - // // { - // // if ( PARALLEL.data == 0x57 ) // WiC64 commands start with 'W' - // // { - // // IEC.protocol->flags xor_eq WIC64_ACTIVE; - // // Debug_printv("wic64"); - // // } - // // } - // } - // else - // { - // PARALLEL.mode = MODE_SEND; - // GPIOX.portMode( USERPORT_DATA, GPIOX_MODE_OUTPUT ); - - // Debug_printv("send >>> " BYTE_TO_BINARY_PATTERN " (%0.2d) " BYTE_TO_BINARY_PATTERN " (%0.2d)", BYTE_TO_BINARY(PARALLEL.flags), PARALLEL.flags, BYTE_TO_BINARY(PARALLEL.data), PARALLEL.data); - // } - } - else - { - PARALLEL.reset(); - } -} - -void parallelBus::setup () -{ - // Setup i2c device - GPIOX.begin(); - reset(); - - // Create a queue to handle parallel event from ISR - ml_parallel_evt_queue = xQueueCreate(10, sizeof(uint32_t)); - - // Start task - //xTaskCreate(ml_parallel_intr_task, "ml_parallel_intr_task", 2048, NULL, 10, NULL); - xTaskCreatePinnedToCore(ml_parallel_intr_task, "ml_parallel_intr_task", 4096, NULL, 10, NULL, 0); - - // Setup interrupt for paralellel port - gpio_config_t io_conf = { - .pin_bit_mask = ( 1ULL << PIN_GPIOX_INT ), // bit mask of the pins that you want to set - .mode = GPIO_MODE_INPUT, // set as input mode - .pull_up_en = GPIO_PULLUP_DISABLE, // disable pull-up mode - .pull_down_en = GPIO_PULLDOWN_DISABLE, // disable pull-down mode - .intr_type = GPIO_INTR_NEGEDGE // interrupt of falling edge - }; - - //configure GPIO with the given settings - gpio_config(&io_conf); - gpio_isr_handler_add((gpio_num_t)PIN_GPIOX_INT, ml_parallel_isr_handler, NULL); -} - -void parallelBus::reset() -{ - // Reset default pin modes - // Debug_printv("clear"); - // GPIOX.clear(); - // Debug_printv("pa2"); - // GPIOX.pinMode( PA2, GPIOX_MODE_INPUT ); - // Debug_printv("pc2"); - // GPIOX.pinMode( PC2, GPIOX_MODE_INPUT ); - // Debug_printv("flag2"); - // GPIOX.pinMode( FLAG2, GPIOX_MODE_OUTPUT ); - - //Debug_printv("reset! state[%d]", IEC.state); - - //Debug_printv("userport flags"); - GPIOX.portMode( USERPORT_FLAGS, 0x05 ); // Set PA2 & PC2 to INPUT - GPIOX.digitalWrite( FLAG2, HIGH); - - //Debug_printv("userport data"); - setMode( MODE_RECEIVE ); -} - -void parallelBus::setMode(parallel_mode_t mode) -{ - if ( mode == MODE_RECEIVE ) - GPIOX.portMode( USERPORT_DATA, GPIOX_MODE_INPUT ); - else - GPIOX.portMode( USERPORT_DATA, GPIOX_MODE_OUTPUT ); -} - -void parallelBus::handShake() -{ - // Signal received or sent - - // LOW - GPIOX.digitalWrite( FLAG2, LOW ); - - // HIGH - GPIOX.digitalWrite( FLAG2, HIGH ); -} - -uint8_t parallelBus::readByte() -{ - - this->data = GPIOX.read( USERPORT_DATA ); - this->flags = GPIOX.PORT0; - - //Debug_printv("flags[%.2x] data[%.2x]", this->flags, this->data); - - // Acknowledge byte received - this->handShake(); - - return this->data; -} - -void parallelBus::writeByte( uint8_t byte ) -{ - this->data = byte; - - Debug_printv("flags[%.2x] data[%.2x] byte[%.2x]", this->flags, this->data, byte); - GPIOX.write( USERPORT_DATA, byte); - - // Tell receiver byte is ready to read - this->handShake(); -} - -bool parallelBus::status( user_port_pin_t pin ) -{ - if ( pin < 8 ) - return ( this->flags & ( 1 >> pin) ); - - return ( this->data & ( 1 >> ( pin - 8) ) ); -} - - - -void wic64_command() -{ - // if (lastinput.startsWith("W")) // Commando startet mit W = Richtig - // { - // if (lastinput.charAt(3) == 1) - // { - // ex = true; - // displaystuff("loading http"); - // loader(lastinput); - - // if (errorcode != "") - // { - // sendmessage(errorcode); - // } - // } - - // if (lastinput.charAt(3) == 2) - // { - // ex = true; - // displaystuff("config wifi"); - // httpstring = lastinput; - // sendmessage(setwlan()); - // delay(3000); - // displaystuff("config changed"); - // } - - // if (lastinput.charAt(3) == 3) - // { - // ex = true; // Normal SW update - no debug messages on serial - // displaystuff("FW update 1"); - // handleUpdate(); - // } - - // if (lastinput.charAt(3) == 4) - // { - // ex = true; // Developer SW update - debug output to serial - // displaystuff("FW update 2"); - // handleDeveloper(); - // } - - // if (lastinput.charAt(3) == 5) - // { - // ex = true; // Developer SW update - debug output to serial - // displaystuff("FW update 3"); - // handleDeveloper2(); - // } - - // if (lastinput.charAt(3) == 6) - // { - // ex = true; - // displaystuff("get ip"); - // String ipaddress = WiFi.localIP().toString(); - // sendmessage(ipaddress); - // } - - // if (lastinput.charAt(3) == 7) - // { - // ex = true; - // displaystuff("get stats"); - // String stats = __DATE__ " " __TIME__; - // sendmessage(stats); - // } - - // if (lastinput.charAt(3) == 8) - // { - // ex = true; - // displaystuff("set server"); - // lastinput.remove(0, 4); - // setserver = lastinput; - // preferences.putString("server", lastinput); - // } - - // if (lastinput.charAt(3) == 9) - // { - // ex = true; // REM Send messages to debug console. - // displaystuff("REM"); - // Serial.println(lastinput); - // } - - // if (lastinput.charAt(3) == 10) - // { - // ex = true; // Get UDP data and return them to c64 - // displaystuff("get upd"); - // sendmessage(getudpmsg()); - // } - - // if (lastinput.charAt(3) == 11) - // { - // ex = true; // Send UDP data to IP - // displaystuff("send udp"); - // sendudpmsg(lastinput); - // } - - // if (lastinput.charAt(3) == 12) - // { - // ex = true; // wlan scanner - // displaystuff("scanning wlan"); - // sendmessage(getWLAN()); - // } - - // if (lastinput.charAt(3) == 13) - // { - // ex = true; // wlan setup via scanlist - // displaystuff("config wifi id"); - // httpstring = lastinput; - // sendmessage(setWLAN_list()); - // displaystuff("config wifi set"); - // } - - // if (lastinput.charAt(3) == 14) - // { - // ex = true; - // displaystuff("change udp port"); - // httpstring = lastinput; - // startudpport(); - // } - - // if (lastinput.charAt(3) == 15) - // { - // ex = true; // Chatserver string decoding - // displaystuff("loading httpchat"); - // loader(lastinput); - - // if (errorcode != "") - // { - // sendmessage(errorcode); - // } - // } - - // if (lastinput.charAt(3) == 16) - // { - // ex = true; - // displaystuff("get ssid"); - // sendmessage(WiFi.SSID()); - // } - - // if (lastinput.charAt(3) == 17) - // { - // ex = true; - // displaystuff("get rssi"); - // sendmessage(String(WiFi.RSSI())); - // } - - // if (lastinput.charAt(3) == 18) - // { - // ex = true; - // displaystuff("get server"); - - // if (setserver != "") - // { - // sendmessage(setserver); - // } - // else - // { - // sendmessage("no server set"); - // } - // } - - // if (lastinput.charAt(3) == 19) - // { - // ex = true; // XXXX 4 bytes header for padding ! - // displaystuff("get external ip"); - // loader("XXXXhttp://sk.sx-64.de/wic64/ip.php"); - - // if (errorcode != "") - // { - // sendmessage(errorcode); - // } - // } - - // if (lastinput.charAt(3) == 20) - // { - // ex = true; - // displaystuff("get mac"); - // sendmessage(WiFi.macAddress()); - // } - - // if (lastinput.charAt(3) == 30) - // { - // ex = true; // Get TCP data and return them to c64 INCOMPLETE - // displaystuff("get tcp"); - // getudpmsg(); - - // if (errorcode != "") - // { - // sendmessage(errorcode); - // } - // } - - // if (lastinput.charAt(3) == 31) - // { - // ex = true; // Get TCP data and return them to c64 INCOMPLETE - // displaystuff("send tcp"); - // sendudpmsg(lastinput); - // sendmessage(""); - // log_i("tcp send %s", lastinput); - // } - - // if (lastinput.charAt(3) == 32) - // { - // ex = true; - // displaystuff("set tcp port"); - // httpstring = lastinput; - // settcpport(); - // } - - // if (lastinput.charAt(3) == 99) - // { - // ex = true; - // displaystuff("factory reset"); - // WiFi.begin("-", "-"); - // WiFi.disconnect(true); - // preferences.putString("server", defaultserver); - // display.clearDisplay(); - // delay(3000); - // ESP.restart(); - // } - // } -} - -#endif // PARALLEL_BUS \ No newline at end of file diff --git a/lib/bus/iec/parallel.h b/lib/bus/iec/parallel.h deleted file mode 100644 index 2b5e972fc..000000000 --- a/lib/bus/iec/parallel.h +++ /dev/null @@ -1,130 +0,0 @@ -// http://sta.c64.org/cbmpar.html -// https://ist.uwaterloo.ca/~schepers/c64par.html -// https://ist.uwaterloo.ca/~schepers/1541port.html -// - -#ifndef MEATLOAF_BUS_PARALLEL -#define MEATLOAF_BUS_PARALLEL -#ifdef PARALLEL_BUS - -#include "../../gpiox/gpiox.h" - -#include - -// C64, 128, VIC20 -// User Port to pin mapping -// #define FLAG2 P07 // B -// #define CNT1 P06 // 4 -// #define SP1 P05 // 5 -// #define CNT2 P04 // 6 -// #define SP2 P03 // 7 -// #define PC2 P02 // 8 -// #define ATN P01 // 9 -// #define PA2 P00 // M - -// #define PB0 P10 // C -// #define PB1 P11 // D -// #define PB2 P12 // E -// #define PB3 P13 // F -// #define PB4 P14 // H - G -// #define PB5 P15 // J - H -// #define PB6 P16 // K - I -// #define PB7 P17 // L - J - -#define USERPORT_FLAGS GPIOX_PORT0 -#define USERPORT_DATA GPIOX_PORT1 - -// C64, 128, VIC20 -typedef enum { - FLAG2 = P07, // B - CNT1 = P06, // 4 - SP1 = P05, // 5 - CNT2 = P04, // 6 - SP2 = P03, // 7 - PC2 = P02, // 8 - ATN = P01, // 9 - PA2 = P00, // M - K - - PB0 = P10, // C - PB1 = P11, // D - PB2 = P12, // E - PB3 = P13, // F - PB4 = P14, // H - G - PB5 = P15, // J - H - PB6 = P16, // K - I - PB7 = P17, // L - J -} user_port_pin_t; - - -// // Plus4 -// typedef enum { -// PB0 = P07, // B // P0 DATA 0 -// CNT1 = P06, // 4 // P2 DATA 2 / Cassette Sense -// SP1 = P05, // 5 // P3 DATA 3 -// CNT2 = P04, // 6 // P4 DATA 4 -// SP2 = P03, // 7 // P5 DATA 5 -// PC2 = P02, // 8 // -// ATN = P01, // 9 -// PA2 = P00, // M - K - -// PB0 = P10, // C -// PB1 = P11, // D -// PB2 = P12, // E -// PB3 = P13, // F -// PB4 = P14, // H - G -// PB5 = P15, // J - H -// PB6 = P16, // K - I -// PB7 = P17, // L - J -// } user_port_plus4_pin_t; - -// Return values for service: -typedef enum -{ - PBUS_OFFLINE = -3, // Bus is empty - PBUS_RESET = -2, // The bus is in a reset state (RESET line). - PBUS_ERROR = -1, // A problem occoured, reset communication - PBUS_IDLE = 0, // Nothing recieved of our concern - PBUS_ACTIVE = 1, // ATN is pulled and a command byte is expected - PBUS_PROCESS = 2, // A command is ready to be processed -} pbus_state_t; - -typedef enum { - MODE_SEND = 0, - MODE_RECEIVE = 1 -} parallel_mode_t; - -#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" -#define BYTE_TO_BINARY(byte) \ - (byte & 0x80 ? '1' : '0'), \ - (byte & 0x40 ? '1' : '0'), \ - (byte & 0x20 ? '1' : '0'), \ - (byte & 0x10 ? '1' : '0'), \ - (byte & 0x08 ? '1' : '0'), \ - (byte & 0x04 ? '1' : '0'), \ - (byte & 0x02 ? '1' : '0'), \ - (byte & 0x01 ? '1' : '0') - -class parallelBus -{ - public: - void setup(); - void reset(); - void service(); - - void setMode(parallel_mode_t mode); - void handShake(); - uint8_t readByte(); - void writeByte( uint8_t byte ); - bool status( user_port_pin_t pin ); - - uint8_t flags = 0x00; - uint8_t data = 0; - parallel_mode_t mode = MODE_RECEIVE; - pbus_state_t state; - bool enabled = true; -}; - -extern parallelBus PARALLEL; - -#endif // PARALLEL_BUS -#endif // MEATLOAF_BUS_PARALLEL \ No newline at end of file diff --git a/lib/bus/iec/protocol/_protocol.cpp b/lib/bus/iec/protocol/_protocol.cpp deleted file mode 100644 index 3e485739a..000000000 --- a/lib/bus/iec/protocol/_protocol.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifdef BUILD_IEC - -#include "_protocol.h" - -#include "bus.h" - -#include "../../../include/pinmap.h" -#include "../../../include/debug.h" - -using namespace Protocol; - -int IRAM_ATTR IECProtocol::waitForSignals(int pin1, int state1, - int pin2, int state2, - int timeout) -{ - uint64_t start, now, elapsed; - int abort = 0; - - start = esp_timer_get_time(); - for (;;) { - if (IEC_IS_ASSERTED(pin1) == state1) - break; - if (pin2 && IEC_IS_ASSERTED(pin2) == state2) - break; - - now = esp_timer_get_time(); - elapsed = now - start; - if (elapsed >= timeout) { - abort = 1; - break; - } - } - - return abort ? TIMED_OUT : 0; -} - -void IECProtocol::transferDelaySinceLast(size_t minimumDelay) -{ - uint64_t now, elapsed; - int64_t remaining; - - - now = esp_timer_get_time(); - elapsed = now - _transferEnded; - if (minimumDelay > 0) { - remaining = minimumDelay; - remaining -= elapsed; - if (remaining > 0) { - usleep(remaining); - now = esp_timer_get_time(); - } - } - - _transferEnded = now; - return; -} - -#endif /* BUILD_IEC */ diff --git a/lib/bus/iec/protocol/_protocol.h b/lib/bus/iec/protocol/_protocol.h deleted file mode 100644 index 8c7cedd2b..000000000 --- a/lib/bus/iec/protocol/_protocol.h +++ /dev/null @@ -1,87 +0,0 @@ -#ifndef _PROTOCOL_H -#define _PROTOCOL_H - -#include -#include -#include - -#include - -#include "../../include/cbm_defines.h" - -#define IEC_RELEASE(pin) ({ \ - uint32_t _pin = pin; \ - uint32_t _mask = 1 << (_pin % 32); \ - if (_pin >= 32) \ - GPIO.enable1_w1tc.val = _mask; \ - else \ - GPIO.enable_w1tc = _mask; \ - }) -#define IEC_ASSERT(pin) ({ \ - uint32_t _pin = pin; \ - uint32_t _mask = 1 << (_pin % 32); \ - if (_pin >= 32) \ - GPIO.enable1_w1ts.val = _mask; \ - else \ - GPIO.enable_w1ts = _mask; \ - }) - -#ifndef IEC_INVERTED_LINES -#define IEC_IS_ASSERTED(pin) ({ \ - uint32_t _pin = pin; \ - !((_pin >= 32 ? GPIO.in1.val : GPIO.in) & (1 << (_pin % 32))); \ - }) -#else -#define IEC_IS_ASSERTED(pin) ({ \ - uint32_t _pin = pin; \ - !!(_pin >= 32 ? GPIO.in1.val : GPIO.in) & (1 << (_pin % 32)); \ - }) -#endif /* !IEC_INVERTED_LINES */ - -static uint64_t timer_start_us; -#define timer_start() timer_start_us = esp_timer_get_time() -#define timer_elapsed() esp_timer_get_time() - timer_start_us -#define timer_wait(us) while( (esp_timer_get_time()-timer_start_us) < ((int) (us+0.5)) ) -#define timer_timeout(us) (esp_timer_get_time() - timer_start_us > us) - -namespace Protocol -{ - /** - * @brief The IEC bus protocol base class - */ - class IECProtocol - { - private: - uint64_t _transferEnded = 0; - - public: - - // Fast Loader Pair Timing - std::vector> bit_pair_timing = { - {0, 0, 0, 0}, // Receive - {0, 0, 0, 0} // Send - }; - - - /** - * @brief receive byte from bus - * @return The byte received from bus. - */ - virtual uint8_t receiveByte() = 0; - - /** - * @brief send byte to bus - * @param b Byte to send - * @param eoi Signal EOI (end of Information) - * @return true if send was successful. - */ - virtual bool sendByte(uint8_t b, bool signalEOI) = 0; - - int waitForSignals(int pin1, int state1, int pin2, int state2, int timeout); - void transferDelaySinceLast(size_t minimumDelay); - }; -}; - -#define transferEnd() transferDelaySinceLast(0) - -#endif /* IECPROTOCOLBASE_H */ diff --git a/lib/bus/iec/protocol/cpbstandardserial.cpp b/lib/bus/iec/protocol/cpbstandardserial.cpp deleted file mode 100644 index 621c5e512..000000000 --- a/lib/bus/iec/protocol/cpbstandardserial.cpp +++ /dev/null @@ -1,383 +0,0 @@ -// Meatloaf - A Commodore 64/128 multi-device emulator -// https://github.com/idolpx/meatloaf -// Copyright(C) 2020 James Johnston -// -// Meatloaf is free software : you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Meatloaf is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Meatloaf. If not, see . - -#ifdef BUILD_IEC - -#define DYNAMIC_DELAY - -#include "cpbstandardserial.h" - -#include "bus.h" -#include "_protocol.h" - -#include "../../../include/debug.h" -#include "../../../include/pinmap.h" - -#define IEC_SET_STATE(x) ({IEC._state = x;}) - -using namespace Protocol; - -// STEP 1: READY TO RECEIVE -// Sooner or later, the talker will want to talk, and send a -// character. When it's ready to go, it releases the Clock line. -// This signal change might be translated as "I'm ready to send a -// character." The listener must detect this and respond, but it -// doesn't have to do so immediately. The listener will respond to the -// talker's "ready to send" signal whenever it likes; it can wait a -// long time. If it's a printer chugging out a line of print, or a -// disk drive with a formatting job in progress, it might holdback for -// quite a while; there's no time limit. -uint8_t CPBStandardSerial::receiveByte() -{ - int abort; - int idx, data; - uint64_t start, now; - int elapsed = 0; - - - IEC.flags &= CLEAR_LOW; - if (IEC_IS_ASSERTED(PIN_IEC_ATN)) - IEC.flags |= ATN_ASSERTED; - - portDISABLE_INTERRUPTS(); - -#ifdef DYNAMIC_DELAY - transferDelaySinceLast(TIMING_Tf); -#endif - - // STEP 2: READY FOR DATA - // line. Suppose there is more than one listener. The Data line - // will be reelased only when all listeners have RELEASED it - in - // other words, when all listeners are ready to accept data. What - // happens next is variable. - - // Release Data and wait for all other devices to release the data line too - - IEC_RELEASE(PIN_IEC_DATA_OUT); - - // Either the talker will assert the Clock line back to asserted - // in less than 200 microseconds - usually within 60 microseconds - // - or it will do nothing. The listener should be watching, and - // if 200 microseconds pass without the Clock line being asserted, - // it has a special task to perform: note EOI. - start = esp_timer_get_time(); - for (abort = 0; !abort && !IEC_IS_ASSERTED(PIN_IEC_CLK_IN); ) { - now = esp_timer_get_time(); - elapsed = now - start; - - if (!(IEC.flags & EOI_RECVD) && elapsed >= TIMING_Tye) { - // INTERMISSION: EOI - // If the Ready for Data signal isn't acknowledged by the - // talker within 200 microseconds, the listener knows that the - // talker is trying to signal EOI. EOI, which formally stands - // for "End of Indicator," means "this character will be the - // last one." If it's a sequential disk file, don't ask for - // more: there will be no more. If it's a relative record, - // that's the end of the record. The character itself will - // still be coming, but the listener should note: here comes - // the last character. So if the listener sees the 200 - // microsecond time-out, it must signal "OK, I noticed the - // EOI" back to the talker, It does this by asserting the Data - // line for at least 60 microseconds, and then releasing it. - // The talker will then revert to transmitting the character - // in the usual way; within 60 microseconds it will assert the - // Clock line, and transmission will continue. At this point, - // the Clock line is asserted whether or not we have gone - // through the EOI sequence; we're back to a common - // transmission sequence. - IEC_ASSERT(PIN_IEC_DATA_OUT); - usleep(TIMING_Tei); - IEC_RELEASE(PIN_IEC_DATA_OUT); - IEC.flags |= EOI_RECVD; - } - - if (elapsed > 100000) { - abort = 1; - break; - } - } - - // STEP 3: RECEIVING THE BITS - // The talker has eight bits to send. They will go out without - // handshake; in other words, the listener had better be there to - // catch them, since the talker won't wait to hear from the listener. - // At this point, the talker controls both lines, Clock and Data. At - // the beginning of the sequence, it is holding the Clock asserted, - // while the Data line is RELEASED. the Data line will change soon, - // since we'll sendthe data over it. The eights bits will go out from - // the character one at a time, with the least significant bit going - // first. For example, if the character is the ASCII question mark, - // which is written in binary as 00011111, the ones will go out first, - // followed by the zeros. Now, for each bit, we set the Data line - // released (one) or asserted (zero) according to whether the bit is - // one or zero. As soon as that's set, the Clock line is RELEASED, - // signalling "data ready." The talker will typically have a bit in - // place and be signalling ready in 70 microseconds or less. Once the - // talker has signalled "data ready," it will hold the two lines - // steady for at least 20 microseconds timing needs to be increased to - // 60 microseconds if the Commodore 64 is listening, since the 64's - // video chip may interrupt the processor for 42 microseconds at a - // time, and without the extra wait the 64 might completely miss a - // bit. The listener plays a passive role here; it sends nothing, and - // just watches. As soon as it sees the Clock line released, it grabs - // the bit from the Data line and puts it away. It then waits for the - // clock line to be asserted, in order to prepare for the next - // bit. When the talker figures the data has been held for a - // sufficient length of time, it asserts the Clock line and releases - // the Data line. Then it starts to prepare the next bit. - -#ifndef JIFFYDOS - for (idx = data = 0; !abort && idx < 8; idx++) { -#else - for (idx = data = 0; !abort && idx < 7; idx++) { -#endif - if ((abort = waitForSignals(PIN_IEC_CLK_IN, IEC_RELEASED, 0, 0, TIMEOUT_DEFAULT))) - break; - - if (!IEC_IS_ASSERTED(PIN_IEC_DATA_IN)) - data |= 1 << idx; - - if (waitForSignals(PIN_IEC_CLK_IN, IEC_ASSERTED, 0, 0, TIMEOUT_DEFAULT)) { - if (idx < 7) - abort = 1; - } - } - -#ifdef JIFFYDOS - // If there is a 218us delay before bit 7, the controller uses JiffyDOS - if (waitForSignals(PIN_IEC_CLK_IN, IEC_RELEASED, 0, 0, - TIMING_PROTOCOL_DETECT) == TIMED_OUT) { - // acknowledge we support JiffyDOS - IEC_ASSERT(PIN_IEC_DATA_OUT); - usleep(TIMING_PROTOCOL_ACK); - IEC_RELEASE(PIN_IEC_DATA_OUT); - IEC.flags |= JIFFYDOS_ACTIVE; - - abort = waitForSignals(PIN_IEC_CLK_IN, IEC_RELEASED, 0, 0, TIMEOUT_DEFAULT); - } -#endif - - if (!abort) { - // JiffyDOS check complete, Get last bit - if (!IEC_IS_ASSERTED(PIN_IEC_DATA_IN)) - data |= 1 << idx; - - waitForSignals(PIN_IEC_CLK_IN, IEC_ASSERTED, 0, 0, TIMEOUT_DEFAULT); - } - - portENABLE_INTERRUPTS(); - - // STEP 4: FRAME HANDSHAKE - // After the eighth bit has been sent, it's the listener's turn to - // acknowledge. At this moment, the Clock line is asserted and the - // Data line is released. The listener must acknowledge receiving - // the byte OK by asserting the Data line. The talker is now - // watching the Data line. If the listener doesn't assert the Data - // line within one millisecond - one thousand microseconds - it - // will know that something's wrong and may alarm appropriately. - IEC_ASSERT(PIN_IEC_DATA_OUT); - -#ifdef DYNAMIC_DELAY - transferEnd(); -#else - //usleep(TIMING_Tf); -#endif - - // STEP 5: START OVER - // We're finished, and back where we started. The talker is - // asserting the Clock line, and the listener is asserting the - // Data line. We're ready for step 1; we may send another - // character - unless EOI has happened. If EOI was sent or - // received in this last transmission, both talker and listener - // "letgo." After a suitable pause, the Clock and Data lines are - // RELEASED and transmission stops. - - if (abort) - return -1; - - return data; -} - -// STEP 1: READY TO SEND -// Sooner or later, the talker will want to talk, and send a -// character. When it's ready to go, it releases the Clock line. This -// signal change might be translated as "I'm ready to send a -// character." The listener must detect this and respond, but it -// doesn't have to do so immediately. The listener will respond to the -// talker's "ready to send" signal whenever it likes; it can wait a -// long time. If it's a printer chugging out a line of print, or a -// disk drive with a formatting job in progress, it might holdback for -// quite a while; there's no time limit. -bool CPBStandardSerial::sendByte(uint8_t data, bool eoi) -{ - int len; - int abort = 0; - uint8_t Tv = TIMING_Tv64; - - - if (0) //(IEC.vic20_mode) - Tv = TIMING_Tv; - - if (IEC_IS_ASSERTED(PIN_IEC_ATN) || IEC._state > BUS_IDLE) { - Debug_printv("Abort"); - return 0; - } - - //IEC_ASSERT(PIN_IEC_SRQ);//Debug - gpio_intr_disable(PIN_IEC_CLK_IN); - portDISABLE_INTERRUPTS(); - -#ifdef DYNAMIC_DELAY - transferDelaySinceLast(TIMING_Tbb); -#endif - - IEC_RELEASE(PIN_IEC_CLK_OUT); - - // STEP 2: READY FOR DATA - // When the listener is ready to listen, it releases the Data - // line. Suppose there is more than one listener. The Data line - // will be released only when ALL listeners have RELEASED it - in - // other words, when all listeners are ready to accept data. - // IEC_ASSERT( PIN_IEC_SRQ ); - - // FIXME - Can't wait FOREVER because watchdog will get - // mad. Probably need to configure DATA GPIO with POSEDGE - // interrupt and not do portDISABLE_INTERRUPTS(). Without - // interrupts disabled though there is a big risk of false - // EOI being sent. Maybe the DATA ISR needs to handle EOI - // signaling too? - - if ((abort = waitForSignals(PIN_IEC_DATA_IN, IEC_RELEASED, PIN_IEC_ATN, IEC_ASSERTED, FOREVER))) { - Debug_printv("data released abort"); - } - - /* Because interrupts are disabled it's possible to miss the ATN pause signal */ - if (IEC_IS_ASSERTED(PIN_IEC_ATN)) { - abort = 1; - Debug_printv("ATN abort"); - } - - // What happens next is variable. Either the talker will assert - // the Clock line in less than 200 microseconds - usually within - // 60 microseconds - or it will do nothing. The listener should be - // watching, and if 200 microseconds pass without the Clock line - // being asserted, it has a special task to perform: note EOI. - if (!abort && eoi) { - // INTERMISSION: EOI - // If the Ready for Data signal isn't acknowledged by the - // talker within 200 microseconds, the listener knows that the - // talker is trying to signal EOI. EOI, which formally stands - // for "End of Indicator," means "this character will be the - // last one." If it's a sequential disk file, don't ask for - // more: there will be no more. If it's a relative record, - // that's the end of the record. The character itself will - // still be coming, but the listener should note: here comes - // the last character. So if the listener sees the 200 - // microsecond time-out, it must signal "OK, I noticed the - // EOI" back to the talker, It does this by asserting the Data - // line for at least 60 microseconds, and then releasing - // it. The talker will then revert to transmitting the - // character in the usual way; within 60 microseconds it will - // assert the Clock line, and transmission will continue. At - // this point, the Clock line is asserted whether or not we - // have gone through the EOI sequence; we're back to a common - // transmission sequence. - - // Wait for EOI ACK - // This will happen after appx 250us - if ((abort = waitForSignals(PIN_IEC_DATA_IN, IEC_ASSERTED, PIN_IEC_ATN, IEC_ASSERTED, TIMEOUT_Tf))) { - Debug_printv("EOI ack abort"); - } - - if (!abort && - (abort = waitForSignals(PIN_IEC_DATA_IN, IEC_RELEASED, PIN_IEC_ATN, IEC_ASSERTED, TIMEOUT_Tne))) { - Debug_printv("EOI ackack abort"); - } - } - - IEC_ASSERT(PIN_IEC_CLK_OUT); - usleep(TIMING_Tpr); - - // STEP 3: SENDING THE BITS - for (len = 0; !abort && len < 8; len++, data >>= 1) { - if (IEC_IS_ASSERTED(PIN_IEC_ATN)) { - Debug_printv("ATN 2 abort"); - abort = 1; - break; - } - - if (data & 1) - IEC_RELEASE(PIN_IEC_DATA_OUT); - else - IEC_ASSERT(PIN_IEC_DATA_OUT); - - usleep(TIMING_Ts); - IEC_RELEASE(PIN_IEC_CLK_OUT); - usleep(Tv); - IEC_RELEASE(PIN_IEC_DATA_OUT); - IEC_ASSERT(PIN_IEC_CLK_OUT); - } - - // STEP 4: FRAME HANDSHAKE - // After the eighth bit has been sent, it's the listener's turn to - // acknowledge. At this moment, the Clock line is asserted and the - // Data line is released. The listener must acknowledge receiving - // the byte OK by asserting the Data line. The talker is now - // watching the Data line. If the listener doesn't assert the Data - // line within one millisecond - one thousand microseconds - it - // will know that something's wrong and may alarm appropriately. - - // Wait for listener to accept data - if (!abort && - (abort = waitForSignals(PIN_IEC_DATA_IN, IEC_ASSERTED, PIN_IEC_ATN, IEC_ASSERTED, TIMEOUT_Tf))) { - // RECIEVER TIMEOUT - // If no receiver asserts DATA within 1000 µs at the end of - // the transmission of a byte (after step 28), a receiver - // timeout is raised. - if (!IEC_IS_ASSERTED(PIN_IEC_ATN)) { - abort = 0; - } - else { - IEC_SET_STATE(BUS_IDLE); - } - } - portENABLE_INTERRUPTS(); - gpio_intr_enable(PIN_IEC_CLK_IN); - //IEC_RELEASE(PIN_DEBUG);//Debug - -#ifdef DYNAMIC_DELAY - transferEnd(); -#else - //usleep(TIMING_Tbb); -#endif - - // STEP 5: START OVER - // We're finished, and back where we started. The talker is - // asserting the Clock line, and the listener is asserting the - // Data line. We're ready for step 1; we may send another - // character - unless EOI has happened. If EOI was sent or - // received in this last transmission, both talker and listener - // "letgo." After a suitable pause, the Clock and Data lines are - // RELEASED and transmission stops. - - if (abort && IEC_IS_ASSERTED(PIN_IEC_ATN)) - IEC_RELEASE(PIN_IEC_CLK_OUT); - - return !abort; -} - -#endif // BUILD_IEC diff --git a/lib/bus/iec/protocol/cpbstandardserial.h b/lib/bus/iec/protocol/cpbstandardserial.h deleted file mode 100644 index 2946628bc..000000000 --- a/lib/bus/iec/protocol/cpbstandardserial.h +++ /dev/null @@ -1,54 +0,0 @@ -// Meatloaf - A Commodore 64/128 multi-device emulator -// https://github.com/idolpx/meatloaf -// Copyright(C) 2020 James Johnston -// -// Meatloaf is free software : you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Meatloaf is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Meatloaf. If not, see . - -// https://www.pagetable.com/?p=1135 -// https://codebase64.org/doku.php?id=base:how_the_vic_64_serial_bus_works -// http://www.zimmers.net/anonftp/pub/cbm/programming/serial-bus.pdf -// https://github.com/0cjs/sedoc/blob/master/8bit/cbm/serial-bus.md -// https://github.com/mist64/cbmsrc/blob/5c5138ff128d289ccd98d260f700af52c4a75521/DOS_1541_05/seratn.src#L6 - - -#ifndef PROTOCOL_CPBSTANDARDSERIAL_H -#define PROTOCOL_CPBSTANDARDSERIAL_H - -// Commodore Peripheral Bus: Standard Serial - -#include "_protocol.h" - -namespace Protocol -{ - class CPBStandardSerial : public IECProtocol - { - public: - - /** - * @brief receive byte from bus - * @return The byte received from bus. - */ - virtual uint8_t receiveByte(); - - /** - * @brief send byte to bus - * @param b Byte to send - * @param eoi Signal EOI (end of Information) - * @return true if send was successful. - */ - virtual bool sendByte(uint8_t data, bool eoi); - }; -}; - -#endif // PROTOCOL_CPBSTANDARDSERIAL_H diff --git a/lib/bus/iec/protocol/jiffydos.cpp b/lib/bus/iec/protocol/jiffydos.cpp deleted file mode 100644 index 92e3af57d..000000000 --- a/lib/bus/iec/protocol/jiffydos.cpp +++ /dev/null @@ -1,222 +0,0 @@ -#ifdef BUILD_IEC -#ifdef JIFFYDOS -// Meatloaf - A Commodore 64/128 multi-device emulator -// https://github.com/idolpx/meatloaf -// Copyright(C) 2020 James Johnston -// -// Meatloaf is free software : you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Meatloaf is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Meatloaf. If not, see . - -#include "jiffydos.h" - -#include -// #include -// #include -// #include -// #include - -#include "bus.h" -#include "_protocol.h" - -#include "../../../include/debug.h" -#include "../../../include/pinmap.h" - -using namespace Protocol; - - -JiffyDOS::JiffyDOS() { - - // Fast Loader Pair Timing - bit_pair_timing.clear(); - bit_pair_timing = { - {14, 27, 38, 51}, // Receive - {17, 27, 39, 50} // Send - }; - -}; - - -uint8_t JiffyDOS::receiveByte () -{ - uint8_t data = 0; - - portDISABLE_INTERRUPTS(); - - IEC.flags &= CLEAR_LOW; - - // Release the data to signal we are ready - IEC_RELEASE(PIN_IEC_DATA_IN); - - // Wait for talker ready - while ( IEC_IS_ASSERTED( PIN_IEC_CLK_IN ) ) - { - if ( IEC_IS_ASSERTED( PIN_IEC_ATN) ) - { - IEC.flags |= ATN_ASSERTED; - goto done; - } - } - - // RECEIVING THE BITS - // As soon as the talker releases the Clock line we are expected to receive the bits - // Bits are inverted so use IEC_IS_ASSERTED() to get asserted/released status - - //IEC_ASSERT( PIN_IEC_SRQ ); - timer_start(); - - // get bits 4,5 - timer_wait ( bit_pair_timing[0][0] ); // Includes setup delay - if ( IEC_IS_ASSERTED( PIN_IEC_CLK_IN ) ) data |= 0b00010000; // 0 - if ( IEC_IS_ASSERTED( PIN_IEC_DATA_IN ) ) data |= 0b00100000; // 1 - //IEC_ASSERT( PIN_IEC_SRQ ); - - // get bits 6,7 - timer_wait ( bit_pair_timing[0][1] ); - if ( IEC_IS_ASSERTED( PIN_IEC_CLK_IN ) ) data |= 0b01000000; // 0 - if ( IEC_IS_ASSERTED( PIN_IEC_DATA_IN ) ) data |= 0b10000000; // 0 - //IEC_RELEASE( PIN_IEC_SRQ ); - - // get bits 3,1 - timer_wait ( bit_pair_timing[0][2] ); - if ( IEC_IS_ASSERTED( PIN_IEC_CLK_IN ) ) data |= 0b00001000; // 0 - if ( IEC_IS_ASSERTED( PIN_IEC_DATA_IN ) ) data |= 0b00000010; // 0 - //IEC_ASSERT( PIN_IEC_SRQ ); - - // get bits 2,0 - timer_wait ( bit_pair_timing[0][3] ); - if ( IEC_IS_ASSERTED( PIN_IEC_CLK_IN ) ) data |= 0b00000100; // 1 - if ( IEC_IS_ASSERTED( PIN_IEC_DATA_IN ) ) data |= 0b00000001; // 0 - //IEC_RELEASE( PIN_IEC_SRQ ); - - // Check CLK for EOI - timer_wait ( 64 ); - if ( IEC_IS_ASSERTED( PIN_IEC_CLK_IN ) ) - IEC.flags |= EOI_RECVD; - //IEC_ASSERT( PIN_IEC_SRQ ); - - // Acknowledge byte received - // If we want to indicate an error we can release DATA - IEC_ASSERT( PIN_IEC_DATA_OUT ); - - // Wait for sender to read acknowledgement - timer_wait ( 83 ); - - //IEC_RELEASE( PIN_IEC_SRQ ); - - //Debug_printv("data[%02X] eoi[%d]", data, IEC.flags); // $ = 0x24 - -done: - portENABLE_INTERRUPTS(); - - return data; -} // receiveByte - - -// STEP 1: READY TO SEND -// Sooner or later, the talker will want to talk, and send a character. -// When it's ready to go, it releases the Clock line to false. This signal change might be -// translated as "I'm ready to send a character." The listener must detect this and respond, -// but it doesn't have to do so immediately. The listener will respond to the talker's -// "ready to send" signal whenever it likes; it can wait a long time. If it's -// a printer chugging out a line of print, or a disk drive with a formatting job in progress, -// it might holdback for quite a while; there's no time limit. -bool JiffyDOS::sendByte ( uint8_t data, bool eoi ) -{ - portDISABLE_INTERRUPTS(); - - IEC.flags &= CLEAR_LOW; - - // Release the data to signal we are ready - IEC_RELEASE(PIN_IEC_CLK_IN); - - // Wait for listener ready - while ( IEC_IS_ASSERTED( PIN_IEC_DATA_IN ) ) - { - if ( IEC_IS_ASSERTED( PIN_IEC_ATN) ) - { - IEC.flags |= ATN_ASSERTED; - goto done; - } - } - - // STEP 2: SENDING THE BITS - // As soon as the listener releases the DATA line we are expected to send the bits - // Bits are inverted so use IEC_IS_ASSERTED() to get asserted/released status - - //IEC_ASSERT( PIN_IEC_SRQ ); - timer_start(); - - // set bits 0,1 - //IEC_ASSERT( PIN_IEC_SRQ ); - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_CLK_OUT ) : IEC_ASSERT( PIN_IEC_CLK_OUT ); - data >>= 1; // shift to next bit - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_DATA_OUT ) : IEC_ASSERT( PIN_IEC_DATA_OUT ); - timer_wait ( bit_pair_timing[1][1] ); - - // set bits 2,3 - data >>= 1; // shift to next bit - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_CLK_OUT ) : IEC_ASSERT( PIN_IEC_CLK_OUT ); - data >>= 1; // shift to next bit - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_DATA_OUT ) : IEC_ASSERT( PIN_IEC_DATA_OUT ); - timer_wait ( bit_pair_timing[1][2] ); - - // set bits 4,5 - data >>= 1; // shift to next bit - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_CLK_OUT ) : IEC_ASSERT( PIN_IEC_CLK_OUT ); - data >>= 1; // shift to next bit - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_DATA_OUT ) : IEC_ASSERT( PIN_IEC_DATA_OUT ); - timer_wait ( bit_pair_timing[1][3] ); - - // set bits 6,7 - data >>= 1; // shift to next bit - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_CLK_OUT ) : IEC_ASSERT( PIN_IEC_CLK_OUT ); - data >>= 1; // shift to next bit - ( data & 1 ) ? IEC_RELEASE( PIN_IEC_DATA_OUT ) : IEC_ASSERT( PIN_IEC_DATA_OUT ); - timer_wait ( bit_pair_timing[1][4] ); - - // Check CLK for EOI - if ( eoi ) - { - // This was the last byte - IEC_RELEASE( PIN_IEC_CLK_OUT ); - IEC_ASSERT( PIN_IEC_CLK_OUT ); - } - else - { - // More data to come - IEC_ASSERT( PIN_IEC_CLK_OUT ); - IEC_RELEASE( PIN_IEC_CLK_OUT ); - } - timer_wait ( 60 ); - //IEC_RELEASE( PIN_IEC_SRQ ); - - // Wait for listener to acknowledge of byte received - while ( !IEC_IS_ASSERTED( PIN_IEC_DATA_IN ) ) - { - if ( IEC_IS_ASSERTED( PIN_IEC_ATN) ) - { - IEC.flags |= ATN_ASSERTED; - goto done; - } - } - - Debug_printv("data[%02X] eoi[%d]", data, eoi); // $ = 0x24 - -done: - portENABLE_INTERRUPTS(); - - return true; -} // sendByte - -#endif // JIFFYDOS -#endif // BUILD_IEC diff --git a/lib/bus/iec/protocol/jiffydos.h b/lib/bus/iec/protocol/jiffydos.h deleted file mode 100644 index 19b52285e..000000000 --- a/lib/bus/iec/protocol/jiffydos.h +++ /dev/null @@ -1,93 +0,0 @@ -// Meatloaf - A Commodore 64/128 multi-device emulator -// https://github.com/idolpx/meatloaf -// Copyright(C) 2020 James Johnston -// -// Meatloaf is free software : you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Meatloaf is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Meatloaf. If not, see . -// -// https://github.com/MEGA65/open-roms/blob/master/doc/Protocol-JiffyDOS.md -// http://www.nlq.de/ -// http://www.baltissen.org/newhtm/sourcecodes.htm -// https://www.amigalove.com/viewtopic.php?t=1734 -// https://ar.c64.org/rrwiki/images/4/48/The_Transactor_Vol09_03_1989_Feb_JD_review.pdf -// https://web.archive.org/web/20090826145226/http://home.arcor.de/jochen.adler/ajnjil-t.htm -// https://web.archive.org/web/20220423162959/https://sites.google.com/site/h2obsession/CBM/C128/JiffySoft128 -// https://www.c64-wiki.com/wiki/SJLOAD -// https://github.com/mist64/cbmbus_doc/blob/cb021f3454b499c579c265859ce67ba99e85652b/7%20JiffyDOS.md -// https://ar.c64.org/rrwiki/images/4/48/The_Transactor_Vol09_03_1989_Feb_JD_review.pdf -// https://c65gs.blogspot.com/2023/10/hardware-accelerated-iec-serial.html?m=0 -// https://c65gs.blogspot.com/2023/12/hardware-accelerated-iec-serial.html?m=0 -// https://c65gs.blogspot.com/2024/01/hardware-accelerated-iec-serial-bus.html?m=0 -// https://c65gs.blogspot.com/2024/01/hardware-accelerated-iec-controller.html?m=0 -// - -// https://retrocomputing.stackexchange.com/questions/14071/what-are-my-options-for-fast-bidirectional-transfer-between-a-c64-and-a-1541?rq=1 -// -// ltransferbyte: -// nop ; timing critical section -// nop -// nop -// nop -// lda #$03 -// ldx #$23 -// stx $dd00 ; data=active,clock=inactive,ATN=inactive -// bit $dd00 -// bvc lloadinnerloop ; branch if 1541 sets clock active (needs to load next block) -// nop -// sta $dd00 ; set data inactive -// lda $dd00 ; read bits 1/0 -// nop -// lsr -// lsr -// eor $dd00 ; read bits 3/2 -// bit $00 ; burn cycles -// lsr -// lsr -// eor $dd00 ; read bits 5/4 -// bit $00 ; burn cycles -// lsr -// lsr -// eor $dd00 ; read bits 7/6 -// eor #$03 -// sta ($ae),y ; store byte -// inc $ae ; load address lo -// bne ltransferbyte -// inc $af ; load address hi -// jmp ltransferbyte -// - -#ifndef PROTOCOL_JIFFYDOS_H -#define PROTOCOL_JIFFYDOS_H - -#include "_protocol.h" - -#include - -#define TIMING_JIFFY_BITPAIR -#define TIMING_JIFFY_BYTE - -namespace Protocol { -class JiffyDOS : public IECProtocol { - public: - JiffyDOS(); - - protected: - uint8_t loadmode = 0; - uint8_t skipeoi = 0; - uint8_t receiveByte(void) override; - bool sendByte(uint8_t data, bool eoi) override; - bool sendByte(uint8_t data, bool eoi, uint8_t loadflags); -}; -}; // namespace Protocol - -#endif // PROTOCOL_JIFFYDOS_H diff --git a/lib/bus/iwm/iwm_ll.cpp b/lib/bus/iwm/iwm_ll.cpp index ff21b95cd..42d79576a 100644 --- a/lib/bus/iwm/iwm_ll.cpp +++ b/lib/bus/iwm/iwm_ll.cpp @@ -890,8 +890,10 @@ void IRAM_ATTR iwm_diskii_ll::diskii_write_handler() item.buffer = (decltype(item.buffer)) heap_caps_malloc(item.length, MALLOC_CAP_8BIT); if (!item.buffer) + { Debug_printf("\r\nDisk II unable to allocate buffer! %u %u %u", item.length, item.track_begin, item.track_end); + } else { size_t end1, end2; @@ -906,7 +908,9 @@ void IRAM_ATTR iwm_diskii_ll::diskii_write_handler() end1 -= d2w_position; memcpy(item.buffer, &d2w_buffer[d2w_position], end1); if (end2) + { memcpy(&item.buffer[end1], d2w_buffer, end2); + } xQueueSendFromISR(iwm_write_queue, &item, &woken); } d2w_writing = false; diff --git a/lib/bus/mac/mac.cpp b/lib/bus/mac/mac.cpp index 9a6e324a8..2848a804d 100644 --- a/lib/bus/mac/mac.cpp +++ b/lib/bus/mac/mac.cpp @@ -116,7 +116,7 @@ void macBus::service(void) // fnUartBUS.flush(); break; default: - Debug_printf("%03d"); + //Debug_printf("%03d"); fnUartBUS.write('X'); // fnUartBUS.flush(); break; diff --git a/lib/bus/mac/mac_ll.cpp b/lib/bus/mac/mac_ll.cpp index 58cec148f..02a48cb4e 100644 --- a/lib/bus/mac/mac_ll.cpp +++ b/lib/bus/mac/mac_ll.cpp @@ -616,8 +616,8 @@ void mac_ll::setup_gpio() if (!fnSystem.no3state()) { - fnSystem.set_pin_mode(SP_RD_BUFFER, gpio_mode_t::GPIO_MODE_OUTPUT); // tri-state buffer control - fnSystem.digital_write(SP_RD_BUFFER, DIGI_LOW); // Turn tristate buffer ON by default + fnSystem.set_pin_mode(SP_RDDATA, gpio_mode_t::GPIO_MODE_OUTPUT); // tri-state buffer control + fnSystem.digital_write(SP_RDDATA, DIGI_LOW); // Turn tristate buffer ON by default } #ifdef EXTRA diff --git a/lib/bus/sio/sio.cpp b/lib/bus/sio/sio.cpp index 28ccde12c..3dc2f431b 100755 --- a/lib/bus/sio/sio.cpp +++ b/lib/bus/sio/sio.cpp @@ -877,7 +877,7 @@ void systemBus::setUltraHigh(bool _enable, int _ultraHighBaud) _sioBaudUltraHigh = _ultraHighBaud; - Debug_printf("Enabling SIO clock, rate: %lu\n", _ultraHighBaud); + Debug_printf("Enabling SIO clock, rate: %u\n", _ultraHighBaud); // Enable PWM on CLOCK IN #ifdef ESP_PLATFORM diff --git a/lib/console/Commands/CoreCommands.cpp b/lib/console/Commands/CoreCommands.cpp new file mode 100644 index 000000000..d9f20a4b8 --- /dev/null +++ b/lib/console/Commands/CoreCommands.cpp @@ -0,0 +1,226 @@ +#include "./CoreCommands.h" +#include "linenoise/linenoise.h" +#include "soc/soc_caps.h" +//#include "argparse/argparse.hpp" + +#include + +#include "display.h" +#include "string_utils.h" + +static int clear(int argc, char **argv) +{ + // If we are on a dumb terminal clearing does not work + if (linenoiseProbe()) + { + printf("\r\nYour terminal does not support escape sequences. Clearing screen does not work!\r\n"); + return EXIT_FAILURE; + } + + linenoiseClearScreen(); + return EXIT_SUCCESS; +} + +static int echo(int argc, char **argv) +{ + for (int n = 1; n 1) + { + if (strcasecmp(argv[1], "-c")) + { // When -c option was detected clear history. + linenoiseHistorySetMaxLen(0); + printf("History cleared!\r\n"); + linenoiseHistorySetMaxLen(10); + return EXIT_SUCCESS; + } + else + { + printf("Invalid argument. Use -c to clear history.\r\n"); + + return EXIT_FAILURE; + } + } + else*/ + { // Without arguments we just output the history + // We use the ESP-IDF VFS to directly output the file to an UART. UART channel 0 has the path /dev/uart/0 and so on. + char path[12] = {0}; + snprintf(path, 12, "/dev/uart/%d", history_channel); + + // If we found the correct one, let linoise save (output) them. + linenoiseHistorySave(path); + return EXIT_SUCCESS; + } + + return EXIT_FAILURE; +} + +extern char **environ; + +static int env(int argc, char **argv) +{ + char **s = environ; + + for (; *s; s++) + { + printf("%s\r\n", *s); + } + return EXIT_SUCCESS; +} + +static int declare(int argc, char **argv) +{ + if (argc != 3) { + fprintf(stderr, "Syntax: declare VAR short OR declare VARIABLE \"Long Value\"\r\n"); + return EXIT_FAILURE; + } + + setenv(argv[1], argv[2], 1); + + return EXIT_SUCCESS; +} + +#ifdef ENABLE_DISPLAY +static int led(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "led {idle|send|receive|activity|progress {0-100}|status {1-255}|speed {0-255}}\r\n"); + return EXIT_FAILURE; + } + + if (mstr::startsWith(argv[1], "idle")) + { + DISPLAY.idle(); + } + else if (mstr::startsWith(argv[1], "send")) + { + DISPLAY.send(); + } + else if (mstr::startsWith(argv[1], "receive")) + { + DISPLAY.receive(); + } + else if (mstr::startsWith(argv[1], "activity")) + { + DISPLAY.activity = !DISPLAY.activity; + } + else if (mstr::startsWith(argv[1], "progress")) + { + if (argc == 3) + DISPLAY.progress = atoi(argv[2]); + else + DISPLAY.idle(); + } + else if (mstr::startsWith(argv[1], "status")) + { + if (argc == 3) + DISPLAY.status(atoi(argv[2])); + else + DISPLAY.idle(); + } + else if (mstr::startsWith(argv[1], "speed")) + { + if (argc == 3) + DISPLAY.speed = atoi(argv[2]); + else + DISPLAY.idle(); + } + else if (mstr::startsWith(argv[1], "pixel")) + { + // if (argc == 4) + // DISPLAY.set_pixel(atoi(argv[2]), (CRGB)atoi(argv[3])); + // else + if (argc == 6) + DISPLAY.set_pixel(atoi(argv[2]), atoi(argv[3]), atoi(argv[4]), atoi(argv[5])); + else + DISPLAY.idle(); + } + + return EXIT_SUCCESS; +} +#endif + +namespace ESP32Console::Commands +{ + const ConsoleCommand getClearCommand() + { + return ConsoleCommand("clear", &clear, "Clears the screen using ANSI codes"); + } + + const ConsoleCommand getEchoCommand() + { + return ConsoleCommand("echo", &echo, "Echos the text supplied as argument"); + } + + const ConsoleCommand getSetMultilineCommand() + { + return ConsoleCommand("multiline_mode", &set_multiline_mode, "Sets the multiline mode of the console"); + } + + const ConsoleCommand getHistoryCommand(int uart_channel) + { + history_channel = uart_channel; + return ConsoleCommand("history", &history, "Shows and clear command history (using -c parameter)"); + } + + const ConsoleCommand getEnvCommand() + { + return ConsoleCommand("env", &env, "List all environment variables."); + } + + const ConsoleCommand getDeclareCommand() + { + return ConsoleCommand("declare", &declare, "Change enviroment variables"); + } + +#ifdef ENABLE_DISPLAY + const ConsoleCommand getLEDCommand() + { + return ConsoleCommand("led", &led, "Change LED display settings"); + } +#endif +} \ No newline at end of file diff --git a/lib/console/Commands/CoreCommands.h b/lib/console/Commands/CoreCommands.h new file mode 100644 index 000000000..3b01f7e76 --- /dev/null +++ b/lib/console/Commands/CoreCommands.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../ConsoleCommand.h" + +namespace ESP32Console::Commands +{ + const ConsoleCommand getClearCommand(); + + const ConsoleCommand getEchoCommand(); + + const ConsoleCommand getSetMultilineCommand(); + + const ConsoleCommand getHistoryCommand(int uart_channel=0); + + const ConsoleCommand getEnvCommand(); + + const ConsoleCommand getDeclareCommand(); + +#ifdef ENABLE_DISPLAY + const ConsoleCommand getLEDCommand(); +#endif +} \ No newline at end of file diff --git a/lib/console/Commands/GPIOCommands.cpp b/lib/console/Commands/GPIOCommands.cpp new file mode 100644 index 000000000..c92e901b4 --- /dev/null +++ b/lib/console/Commands/GPIOCommands.cpp @@ -0,0 +1,219 @@ +#include "GPIOCommands.h" + +#include +#include +#include + +#include "string_utils.h" + +#define HIGH 1 +#define LOW 0 + +static int _pinmode(int argc, char **argv) +{ + // if (argc != 3) + // { + // printf("You have to pass a pin number and mode. Syntax: pinMode [GPIO] [MODE]\r\n"); + // return 1; + // } + + // char *pin_str = argv[1]; + // std::string mode_str = std::string(argv[2]); + + // unsigned long pin = 0; + + // try + // { + // pin = std::stoul(pin_str); + // } + // catch (std::invalid_argument ex) + // { + // fprintf(stderr, "Invalid argument for pin: %s\r\n", ex.what()); + // return 1; + // } + + // if (pin > 255 || !digitalPinIsValid(pin)) { + // fprintf(stderr, "%d is not a GPIO pin\r\n", pin); + // return 1; + // } + + // gpio_mode_t mode = GPIO_MODE_INPUT; + + // if (mstr::equals(mode_str, "INPUT", false)) + // { + // mode = GPIO_MODE_INPUT; + // } + // else if (mstr::equals(mode_str, "OUTPUT")) + // { + // mode = GPIO_MODE_OUTPUT; + // } + // else if (mstr::equals(mode_str, "INPUT_PULLUP")) + // { + // mode = GPIO_MODE_INPUT | GPIO_PULLUP_ENABLE; + // } + // else if (mstr::equals(mode_str, "INPUT_PULLDOWN")) + // { + // mode = GPIO_MODE_INPUT | GPIO_PULLDOWN_ENABLE; + // } + // else if (mstr::equals(mode_str, "OUTPUT_OPEN_DRAIN")) + // { + // mode = GPIO_MODE_OUTPUT_OD; + // } + // else + // { + // fprintf(stderr, "Invalid mode: Allowed modes are INPUT, OUTPUT, INPUT_PULLUP, INPUT_PULLDOWN, OUTPUT_OPEN_DRAIN\r\n"); + // } + + // pinMode(pin, mode); + // printf("Mode set successful.\r\n"); + + return 0; +} + +static int _digitalWrite(int argc, char** argv) +{ + // if (argc != 3) + // { + // printf("You have to pass an pin number and level. Syntax: digitalWrite [GPIO] [Level]\r\n"); + // return 1; + // } + + // char *pin_str = argv[1]; + // std::string mode_str = std::string(argv[2]); + + // unsigned long pin = 0; + + // try + // { + // pin = std::stoul(pin_str); + // } + // catch (std::invalid_argument ex) + // { + // fprintf(stderr, "Invalid argument for pin: %s\r\n", ex.what()); + // return 1; + // } + + // if (pin > 255 || !digitalPinCanOutput(pin)) { + // fprintf(stderr, "%d is not a GPIO pin\r\n", pin); + // return 1; + // } + + // int mode = LOW; + + // if (mstr::equals(mode_str, "HIGH") || mstr::equals(mode_str, "1")) + // { + // mode = HIGH; + // } + // else if (mstr::equals(mode_str, "LOW") || mstr::equals(mode_str, "0")) + // { + // mode = LOW; + // } else + // { + // fprintf(stderr, "Invalid mode: Allowed levels are HIGH, LOW, 0 and 1\r\n"); + // } + + // pinMode(pin, mode); + // printf("Output set successful.\r\n"); + + return 0; +} + +static int _digitalRead(int argc, char** argv) +{ + // if (argc != 2) + // { + // printf("You have to pass an pin number to read\r\n"); + // return 1; + // } + + // char *pin_str = argv[1]; + + // unsigned long pin = 0; + + // try + // { + // pin = std::stoul(pin_str); + // } + // catch (std::invalid_argument ex) + // { + // fprintf(stderr, "Invalid argument for pin: %s\r\n", ex.what()); + // return 1; + // } + + // if (pin > 255 || !digitalPinCanOutput(pin)) { + // fprintf(stderr, "%d is not a GPIO pin\r\n", pin); + // return 1; + // } + + // auto level = digitalRead(pin); + + // if(level == HIGH) { + // printf("HIGH\r\n"); + // } else if(level == LOW) { + // printf("LOW\r\n"); + // } else { + // fprintf(stderr, "Unknown state (%u) of pin %u!\r\n", level, pin); + // return 1; + // } + + return 0; +} + +static int _analogRead(int argc, char** argv) +{ + // if (argc != 2) + // { + // printf("You have to pass an pin number to read\r\n"); + // return 1; + // } + + // char *pin_str = argv[1]; + + // unsigned long pin = 0; + + // try + // { + // pin = std::stoul(pin_str); + // } + // catch (std::invalid_argument ex) + // { + // fprintf(stderr, "Invalid argument for pin: %s\r\n", ex.what()); + // return 1; + // } + + // if (pin > 255 || digitalPinToAnalogChannel(pin) == -1) { + // fprintf(stderr, "%d is not a ADC pin\r\n", pin); + // return 1; + // } + + // auto value = analogReadMilliVolts(pin); + + // printf("%u mV\r\n", value); + + return 0; +} + + + +namespace ESP32Console::Commands +{ + const ConsoleCommand getPinModeCommand() + { + return ConsoleCommand("pinMode", &_pinmode, "Changes the pinmode of an GPIO pin (similar to Arduino function)"); + } + + const ConsoleCommand getDigitalWriteCommand() + { + return ConsoleCommand("digitalWrite", &_digitalWrite, "Writes the state of an ouput pin (similar to Arduino function)"); + } + + const ConsoleCommand getDigitalReadCommand() + { + return ConsoleCommand("digitalRead", &_digitalRead, "Reads the state of an input pin (similar to Arduino function)"); + } + + const ConsoleCommand getAnalogReadCommand() + { + return ConsoleCommand("analogRead", &_analogRead, "Show the voltage at an analog pin in millivollts."); + } +} \ No newline at end of file diff --git a/lib/console/Commands/GPIOCommands.h b/lib/console/Commands/GPIOCommands.h new file mode 100644 index 000000000..a3e174121 --- /dev/null +++ b/lib/console/Commands/GPIOCommands.h @@ -0,0 +1,14 @@ +#pragma once + +#include "../ConsoleCommand.h" + +namespace ESP32Console::Commands +{ + const ConsoleCommand getPinModeCommand(); + + const ConsoleCommand getDigitalWriteCommand(); + + const ConsoleCommand getDigitalReadCommand(); + + const ConsoleCommand getAnalogReadCommand(); +} \ No newline at end of file diff --git a/lib/console/Commands/NetworkCommands.cpp b/lib/console/Commands/NetworkCommands.cpp new file mode 100644 index 000000000..ae9526c4a --- /dev/null +++ b/lib/console/Commands/NetworkCommands.cpp @@ -0,0 +1,523 @@ +#include "NetworkCommands.h" + +#include +#include +#include +#include "lwip/inet.h" +#include "lwip/netdb.h" +#include "lwip/sockets.h" +#include +#include +#include + +#include "../device/fuji.h" +#include "fnWiFi.h" + +#include "string_utils.h" +#include "../improv/improv.h" + +// static const char *wlstatus2string(wl_status_t status) +// { +// switch (status) +// { +// case WL_NO_SHIELD: +// return "Not initialized"; +// case WL_CONNECT_FAILED: +// return "Connection failed"; +// case WL_CONNECTED: +// return "Connected"; +// case WL_CONNECTION_LOST: +// return "Connection lost"; +// case WL_DISCONNECTED: +// return "Disconnected"; +// case WL_IDLE_STATUS: +// return "Idle status"; +// case WL_NO_SSID_AVAIL: +// return "No SSID available"; +// case WL_SCAN_COMPLETED: +// return "Scan completed"; +// default: +// return "Unknown"; +// } +// } + +const char* wlmode2string(wifi_mode_t mode) +{ + switch(mode) { + case WIFI_MODE_NULL: + return "Not initialized"; + case WIFI_MODE_AP: + return "Accesspoint"; + case WIFI_MODE_STA: + return "Station"; + case WIFI_MODE_APSTA: + return "Station + Accesspoint"; + default: + return "Unknown"; + } +} + + +// Ping Functions +static void on_ping_success(esp_ping_handle_t hdl, void *args) +{ + uint8_t ttl; + uint16_t seqno; + uint32_t elapsed_time, recv_len; + ip_addr_t target_addr; + esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); + esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl)); + esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); + esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len)); + esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time)); + printf("%lu bytes from %s icmp_seq=%d ttl=%d time=%lu ms\r\n", + recv_len, inet_ntoa(target_addr.u_addr.ip4), seqno, ttl, elapsed_time); +} + +static void on_ping_timeout(esp_ping_handle_t hdl, void *args) +{ + uint16_t seqno; + ip_addr_t target_addr; + esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); + esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); + printf("From %s icmp_seq=%u timeout\r\n", inet_ntoa(target_addr.u_addr.ip4), seqno); +} + +static void on_ping_end(esp_ping_handle_t hdl, void *args) +{ + ip_addr_t target_addr; + uint32_t transmitted; + uint32_t received; + uint32_t total_time_ms; + esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted)); + esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received)); + esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); + esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms)); + uint32_t loss = (uint32_t)((1 - ((float)received) / transmitted) * 100); + if (IP_IS_V4(&target_addr)) { + printf("\n--- %s ping statistics ---", inet_ntoa(*ip_2_ip4(&target_addr))); + } else { + printf("\n--- %s ping statistics ---", inet6_ntoa(*ip_2_ip6(&target_addr))); + } + printf("%"PRIu32" packets transmitted, %"PRIu32" received, %"PRIu32"%% packet loss, time %"PRIu32"ms", + transmitted, received, loss, total_time_ms); + // delete the ping sessions, so that we clean up all resources and can create a new ping session + // we don't have to call delete function in the callback, instead we can call delete function from other tasks + esp_ping_delete_session(hdl); +} + +static int ping(int argc, char **argv) +{ + //By default do 5 pings + int number_of_pings = 5; + + int opt; + while ((opt = getopt(argc, argv, "n:")) != -1) { + switch(opt) { + case 'n': + number_of_pings = atoi(optarg); + break; + case '?': + printf("Unknown option: %c\r\n", optopt); + break; + case ':': + printf("Missing arg for %c\r\n", optopt); + break; + + default: + fprintf(stderr, "Usage: ping -n 5 [HOSTNAME]\r\n"); + fprintf(stderr, "-n: The number of pings. 0 means infinite. Can be aborted with Ctrl+D or Ctrl+C."); + return 1; + } + } + + int argind = optind; + + //Get hostname + if (argind >= argc) { + fprintf(stderr, "You need to pass an hostname!\r\n"); + return EXIT_FAILURE; + } + + char* hostname = argv[argind]; + + /* convert hostname to IP address */ + ip_addr_t target_addr; + struct addrinfo hint; + struct addrinfo *res = NULL; + memset(&hint, 0, sizeof(hint)); + memset(&target_addr, 0, sizeof(target_addr)); + auto result = getaddrinfo(hostname, NULL, &hint, &res); + + if (result) { + fprintf(stderr, "Could not resolve hostname! (getaddrinfo returned %d)\r\n", result); + return 1; + } + + struct in_addr addr4 = ((struct sockaddr_in *) (res->ai_addr))->sin_addr; + inet_addr_to_ip4addr(ip_2_ip4(&target_addr), &addr4); + freeaddrinfo(res); + + //Configure ping session + esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG(); + ping_config.task_stack_size = 4096; + ping_config.target_addr = target_addr; // target IP address + ping_config.count = number_of_pings; // 0 means infinite ping + + /* set callback functions */ + esp_ping_callbacks_t cbs; + cbs.on_ping_success = on_ping_success; + cbs.on_ping_timeout = on_ping_timeout; + cbs.on_ping_end = on_ping_end; + //Pass a variable as pointer so the sub tasks can decrease it + //cbs.cb_args = &number_of_pings_remaining; + + esp_ping_handle_t ping; + esp_ping_new_session(&ping_config, &cbs, &ping); + + esp_ping_start(ping); + + char c = 0; + + uint16_t seqno; + esp_ping_get_profile(ping, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); + + //Make stdin input non blocking so we can query for input AND check ping seqno + int flags = fcntl(fileno(stdin), F_GETFL, 0); + fcntl(fileno(stdin), F_SETFL, flags | O_NONBLOCK); + + //Wait for Ctrl+D or Ctr+C or that our task finishes + //The async tasks decrease number_of_pings, so wait for it to get to 0 + while((number_of_pings == 0 || seqno <= number_of_pings) && c != 4 && c != 3) { + esp_ping_get_profile(ping, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); + c = getc(stdin); + sleep(1); + } + + //Reset flags, so we dont end up destroying our terminal env later, when linenoise takes over again + fcntl(fileno(stdin), F_SETFL, flags); + + esp_ping_stop(ping); + + //Print total statistics + uint32_t transmitted; + uint32_t received; + uint32_t total_time_ms; + esp_ping_get_profile(ping, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted)); + esp_ping_get_profile(ping, ESP_PING_PROF_REPLY, &received, sizeof(received)); + esp_ping_get_profile(ping, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms)); + printf("%lu packets transmitted, %lu received, time %lu ms\r\n", transmitted, received, total_time_ms); + + esp_ping_delete_session(ping); + + return EXIT_SUCCESS; +} + + +static void ipconfig_wlan() +{ + printf("==== WLAN ====\r\n"); + // auto status = fnWiFi.status(); + // printf("Mode: %s\r\n", wlmode2string(fnWiFi.getMode())); + // printf("Status: %s\r\n", wlstatus2string(status)); + + // if (status == WL_NO_SHIELD) { + // return; + // } + + // printf("\r\n"); + // printf("SSID: %s\r\n", fnWiFi.get_current_ssid().c_str()); + // printf("BSSID: %s\r\n", fnWiFi.get_current_bssid_str().c_str()); + // //printf("Channel: %d\r\n", fnWiFi.channel()); + + // printf("\r\n"); + // printf("IP: %s\r\n", fnWiFi.localIP().toString().c_str()); + // printf("Subnet Mask: %s (/%d)\r\n", WiFi.subnetMask().toString().c_str(), WiFi.subnetCIDR()); + // printf("Gateway: %s\r\n", WiFi.gatewayIP().toString().c_str()); + // printf("IPv6: %s\r\n", WiFi.localIPv6().toString().c_str()); + + // printf("\r\n"); + // printf("Hostname: %s\r\n", WiFi.getHostname()); + // printf("DNS1: %s\r\n", WiFi.dnsIP(0).toString().c_str()); + // printf("DNS2: %s\r\n", WiFi.dnsIP(0).toString().c_str()); +} + +static int ipconfig(int argc, char **argv) +{ + ipconfig_wlan(); + return EXIT_SUCCESS; +} + +static int scan(int argc, char **argv) +{ + fnWiFi.scan_networks(); + printf("Found following networks:\r\n"); + std::vector network_names = fnWiFi.get_network_names(); + for (std::string _network_name: network_names) + { + uint8_t c_crc8 = esp_crc8_le(0, (uint8_t *)_network_name.c_str(), _network_name.length()); + printf("[%03d] - %s\r\n", c_crc8, _network_name.c_str()); + } + return EXIT_SUCCESS; +} + +static int connect(int argc, char **argv) +{ + if (argc == 3) + { + std::string network = argv[1]; + if (mstr::isNumeric(network)) { + // Find SSID by CRC8 Number + network = fnWiFi.get_network_name_by_crc8(std::stoi(argv[1])); + } + int e = ( fnWiFi.connect(network.c_str(), argv[2]) ); + + if ( e == ESP_OK) + fnWiFi.store_wifi(network, argv[2]); + + return e; + } + + return fnWiFi.connect(); +} + + + +// *** Improv + +std::vector getLocalUrl() { + return { + // URL where user can finish onboarding or use device + // Recommended to use website hosted by device + std::string("http://meatloaf.local") + }; +} + +void serial_write(std::vector &data) { + // print buffer bytes + for (int i = 0; i < data.size(); i++) { + fprintf(stdout, "%c", data[i]); + } +} + + +void set_state(improv::State state) { + + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = improv::IMPROV_SERIAL_VERSION; + data[7] = improv::TYPE_CURRENT_STATE; + data[8] = 1; + data[9] = state; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + + serial_write(data); +} + + +void send_response(std::vector &response) { + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(9); + data[6] = improv::IMPROV_SERIAL_VERSION; + data[7] = improv::TYPE_RPC_RESPONSE; + data[8] = response.size(); + data.insert(data.end(), response.begin(), response.end()); + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data.push_back(checksum); + + serial_write(data); +} + +void set_error(improv::Error error) { + std::vector data = {'I', 'M', 'P', 'R', 'O', 'V'}; + data.resize(11); + data[6] = improv::IMPROV_SERIAL_VERSION; + data[7] = improv::TYPE_ERROR_STATE; + data[8] = 1; + data[9] = error; + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data[10] = checksum; + + serial_write(data); +} + +void getAvailableWifiNetworks() { +// int networkNum = WiFi.scanNetworks(); + +// for (int id = 0; id < networkNum; ++id) { +// std::vector data = improv::build_rpc_response( +// improv::GET_WIFI_NETWORKS, {WiFi.SSID(id), String(WiFi.RSSI(id)), (WiFi.encryptionType(id) == WIFI_AUTH_OPEN ? "NO" : "YES")}, false); +// send_response(data); +// sleep(1); +// } +// //final response +// std::vector data = +// improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector{}, false); +// send_response(data); +} + +bool connectWifi(std::string ssid, std::string password) { + uint8_t count = 0; + +// WiFi.begin(ssid.c_str(), password.c_str()); + +// while (WiFi.status() != WL_CONNECTED) { +// blink_led(500, 1); + +// if (count > MAX_ATTEMPTS_WIFI_CONNECTION) { +// WiFi.disconnect(); +// return false; +// } +// count++; +// } + + return true; +} + +static int improv_c(int argc, char **argv) +{ + + if (argc != 2) + { + fprintf(stderr, "Usage: improv {data}\n"); + return 1; + } + + uint8_t version = argv[1][0]; // 6 + uint8_t type = argv[1][1]; // 7 + uint8_t data_len = argv[1][2]; // 8 + uint8_t data[data_len] = { 0 }; + memcpy(data, &argv[1][2], data_len); + + uint8_t checksum = 0xDD; // IMPROV + checksum += version; + checksum += type; + checksum += data_len; + + for (size_t i = 0; i < (data_len + 1); i++) + checksum += data[i]; + + if (checksum != data[data_len + 1]) { + fprintf(stderr, "bad checksum [%02x][%02x]\r\n", (data[data_len + 1]), checksum); + return false; + } + + if (type != improv::TYPE_RPC) { + return false; + } + + auto cmd = improv::parse_improv_data(data, data_len, false); + // fprintf(stdout, "alldata[%s]", mstr::toHex(argv[1], strlen(argv[1])).c_str()); + // fprintf(stdout, " data[%s]", mstr::toHex(argv[1][2], data_len).c_str()); + + switch (cmd.command) { + case improv::Command::GET_CURRENT_STATE: + { + //if ((WiFi.status() == WL_CONNECTED)) { + if (0) { + set_state(improv::State::STATE_PROVISIONED); + std::vector data = improv::build_rpc_response(improv::GET_CURRENT_STATE, getLocalUrl(), false); + send_response(data); + + } else { + set_state(improv::State::STATE_AUTHORIZED); + } + + break; + } + + case improv::Command::WIFI_SETTINGS: + { + if (cmd.ssid.length() == 0) { + set_error(improv::Error::ERROR_INVALID_RPC); + break; + } + + set_state(improv::STATE_PROVISIONING); + + if (connectWifi(cmd.ssid, cmd.password)) { + + //blink_led(100, 3); + + //TODO: Persist credentials here + + set_state(improv::STATE_PROVISIONED); + std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, getLocalUrl(), false); + send_response(data); + } else { + set_state(improv::STATE_STOPPED); + set_error(improv::Error::ERROR_UNABLE_TO_CONNECT); + } + + break; + } + + case improv::Command::GET_DEVICE_INFO: + { + std::vector infos = { + // Firmware name + "meatloaf", + // Firmware version + "20250106.04", + // Hardware chip/variant + "ESP32", + // Device name + "Meatloaf!" + }; + std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); + send_response(data); + break; + } + + case improv::Command::GET_WIFI_NETWORKS: + { + getAvailableWifiNetworks(); + break; + } + + default: { + set_error(improv::ERROR_UNKNOWN_RPC); + return false; + } + } + + return EXIT_SUCCESS; +} + +namespace ESP32Console::Commands +{ + const ConsoleCommand getPingCommand() + { + return ConsoleCommand("ping", &ping, "Ping host"); + } + + const ConsoleCommand getIpconfigCommand() + { + return ConsoleCommand("ipconfig", &ipconfig, "Show IP and connection informations"); + } + + const ConsoleCommand getScanCommand() + { + return ConsoleCommand("scan", &scan, "Scan for wifi networks"); + } + + const ConsoleCommand getConnectCommand() + { + return ConsoleCommand("connect", &connect, "Connect to wifi"); + } + + const ConsoleCommand getIMPROVCommand() + { + return ConsoleCommand("improv", &improv_c, "Wifi config via IMPROV protocol"); + } +} \ No newline at end of file diff --git a/lib/console/Commands/NetworkCommands.h b/lib/console/Commands/NetworkCommands.h new file mode 100644 index 000000000..9dd269149 --- /dev/null +++ b/lib/console/Commands/NetworkCommands.h @@ -0,0 +1,16 @@ +#pragma once + +#include "../ConsoleCommand.h" + +namespace ESP32Console::Commands +{ + const ConsoleCommand getPingCommand(); + + const ConsoleCommand getIpconfigCommand(); + + const ConsoleCommand getScanCommand(); + + const ConsoleCommand getConnectCommand(); + + const ConsoleCommand getIMPROVCommand(); +} \ No newline at end of file diff --git a/lib/console/Commands/SystemCommands.cpp b/lib/console/Commands/SystemCommands.cpp new file mode 100644 index 000000000..4f04084e1 --- /dev/null +++ b/lib/console/Commands/SystemCommands.cpp @@ -0,0 +1,285 @@ +#include "SystemCommands.h" + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../ESP32Console.h" + +#include "../../../include/version.h" + +#include "Esp.h" + +EspClass ESP; + +static std::string mac2String(uint64_t mac) +{ + uint8_t *ar = (uint8_t *)&mac; + std::string s; + for (uint8_t i = 0; i < 6; ++i) + { + char buf[3]; + sprintf(buf, "%02X", ar[i]); // J-M-L: slight modification, added the 0 in the format for padding + s += buf; + if (i < 5) + s += ':'; + } + return s; +} + +static const char *getFlashModeStr() +{ + auto mode = ESP.getFlashChipMode(); + + switch(mode) + { + case FM_QIO: return "QIO"; + case FM_QOUT: return "QOUT"; + case FM_DIO: return "DIO"; + case FM_DOUT: return "DOUT"; + case FM_FAST_READ: return "FAST READ"; + case FM_SLOW_READ: return "SLOW READ"; + default: return "DOUT"; + } +} + +static const char *getResetReasonStr() +{ + switch (esp_reset_reason()) + { + case ESP_RST_BROWNOUT: + return "Brownout reset (software or hardware)"; + case ESP_RST_DEEPSLEEP: + return "Reset after exiting deep sleep mode"; + case ESP_RST_EXT: + return "Reset by external pin (not applicable for ESP32)"; + case ESP_RST_INT_WDT: + return "Reset (software or hardware) due to interrupt watchdog"; + case ESP_RST_PANIC: + return "Software reset due to exception/panic"; + case ESP_RST_POWERON: + return "Reset due to power-on event"; + case ESP_RST_SDIO: + return "Reset over SDIO"; + case ESP_RST_SW: + return "Software reset via esp_restart"; + case ESP_RST_TASK_WDT: + return "Reset due to task watchdog"; + case ESP_RST_WDT: + return "ESP_RST_WDT"; + + case ESP_RST_UNKNOWN: + default: + return "Unknown"; + } +} + +static int sysInfo(int argc, char **argv) +{ + esp_chip_info_t info; + esp_chip_info(&info); + + printf("FujiNet %s\r\n", FN_VERSION_FULL); +// printf("ESP32Console version: %s\r\n", ESP32CONSOLE_VERSION); +// printf("Arduino Core version: %s (%x)\r\n", XTSTR(ARDUINO_ESP32_GIT_DESC), ARDUINO_ESP32_GIT_VER); + printf("ESP-IDF v%s\r\n", ESP.getSdkVersion()); + + printf("\r\n"); + printf("Chip info:\r\n"); + printf("\tModel: %s\r\n", ESP.getChipModel()); + printf("\tRevison number: %d\r\n", ESP.getChipRevision()); + printf("\tCores: %d\r\n", ESP.getChipCores()); + printf("\tClock: %lu MHz\r\n", ESP.getCpuFreqMHz()); + printf("\tFeatures:%s%s%s%s%s\r\r\n", + info.features & CHIP_FEATURE_WIFI_BGN ? " 802.11bgn " : "", + info.features & CHIP_FEATURE_BLE ? " BLE " : "", + info.features & CHIP_FEATURE_BT ? " BT " : "", + info.features & CHIP_FEATURE_EMB_FLASH ? " Embedded-Flash " : " External-Flash ", + info.features & CHIP_FEATURE_EMB_PSRAM ? " Embedded-PSRAM" : ""); + + printf("EFuse MAC: %s\r\n", mac2String(ESP.getEfuseMac()).c_str()); + + printf("Flash size: %ld MB (mode: %s, speed: %ld MHz)\r\n", ESP.getFlashChipSize() / (1024 * 1024), getFlashModeStr(), ESP.getFlashChipSpeed() / (1024 * 1024)); + printf("PSRAM size: %ld MB\r\n", ESP.getPsramSize() / (1024 * 1024)); + +#ifndef CONFIG_APP_REPRODUCIBLE_BUILD + printf("Compilation datetime: " __DATE__ " " __TIME__ "\r\n"); +#endif + + //printf("\nReset reason: %s\r\n", getResetReasonStr()); + + //printf("\r\n"); + //printf("CPU temperature: %.01f °C\r\n", ESP.temperatureRead()); + + return EXIT_SUCCESS; +} + +static int restart(int argc, char **argv) +{ + printf("Restarting..."); + ESP.restart(); + return EXIT_SUCCESS; +} + +static int meminfo(int argc, char **argv) +{ + uint32_t free = ESP.getFreeHeap() / 1024; + uint32_t total = ESP.getHeapSize() / 1024; + uint32_t used = total - free; + uint32_t min = ESP.getMinFreeHeap() / 1024; + uint32_t total_free = esp_get_free_heap_size() / 1024; + + printf("Internal Heap: %lu KB free, %lu KB used, (%lu KB total)\r\n", free, used, total); + printf("Minimum free heap size during uptime was: %lu KB\r\n", min); + printf("Overall Free Memory: %lu KB\r\n\r\n", total_free); + + total = ESP.getPsramSize() / 1024; + free = ESP.getFreePsram() / 1024; + used = total - free; + printf("PSRAM: %lu KB free, %lu KB used, (%lu KB total)\r\n", free, used, total); + return EXIT_SUCCESS; +} + +static int taskinfo(int argc, char **argv) +{ + printf( "Task Name\tStatus\tPrio\tHWM\tTask\tAffinity\r\r\n"); + char stats_buffer[1024]; + vTaskList(stats_buffer); + printf("%s\r\r\n", stats_buffer); + return EXIT_SUCCESS; +} + +static int date(int argc, char **argv) +{ + bool set_time = false; + char *target = nullptr; + + int c; + opterr = 0; + + // Set timezone from env variable + tzset(); + + while ((c = getopt(argc, argv, "s")) != -1) + switch (c) + { + case 's': + set_time = true; + break; + case '?': + printf("Unknown option: %c\r\n", optopt); + return 1; + case ':': + printf("Missing arg for %c\r\n", optopt); + return 1; + } + + if (optind < argc) + { + target = argv[optind]; + } + + if (set_time) + { + if (!target) + { + fprintf(stderr, "Set option requires an datetime as argument in format '%%Y-%%m-%%d %%H:%%M:%%S' (e.g. 'date -s \"2022-07-13 22:47:00\"'\r\n"); + return 1; + } + + tm t; + + if (!strptime(target, "%Y-%m-%d %H:%M:%S", &t)) + { + fprintf(stderr, "Set option requires an datetime as argument in format '%%Y-%%m-%%d %%H:%%M:%%S' (e.g. 'date -s \"2022-07-13 22:47:00\"'\r\n"); + return 1; + } + + timeval tv = { + .tv_sec = mktime(&t), + .tv_usec = 0}; + + if (settimeofday(&tv, nullptr)) + { + fprintf(stderr, "Could not set system time: %s", strerror(errno)); + return 1; + } + + time_t tmp = time(nullptr); + + constexpr int buffer_size = 100; + char buffer[buffer_size]; + strftime(buffer, buffer_size, "%a %b %e %H:%M:%S %Z %Y", localtime(&tmp)); + printf("Time set: %s\r\n", buffer); + + return 0; + } + + // If no target was supplied put a default one (similar to coreutils date) + if (!target) + { + target = (char*) "+%a %b %e %H:%M:%S %Z %Y"; + } + + // Ensure the format string is correct + if (target[0] != '+') + { + fprintf(stderr, "Format string must start with an +!\r\n"); + return 1; + } + + // Ignore + by moving pointer one step forward + target++; + + constexpr int buffer_size = 100; + char buffer[buffer_size]; + time_t t = time(nullptr); + strftime(buffer, buffer_size, target, localtime(&t)); + printf("%s\r\n", buffer); + return 0; + + return EXIT_SUCCESS; +} + +namespace ESP32Console::Commands +{ + const ConsoleCommand getRestartCommand() + { + return ConsoleCommand("restart", &restart, "Restart / Reboot the system"); + } + + const ConsoleCommand getSysInfoCommand() + { + return ConsoleCommand("sysinfo", &sysInfo, "Shows informations about the system like chip model and ESP-IDF version"); + } + + const ConsoleCommand getMemInfoCommand() + { + return ConsoleCommand("meminfo", &meminfo, "Shows information about heap usage"); + } + + const ConsoleCommand getTaskInfoCommand() + { + return ConsoleCommand("ps", &taskinfo, "Shows information about running tasks"); + } + + const ConsoleCommand getDateCommand() + { + return ConsoleCommand("date", &date, "Shows and modify the system time"); + } +} \ No newline at end of file diff --git a/lib/console/Commands/SystemCommands.h b/lib/console/Commands/SystemCommands.h new file mode 100644 index 000000000..e005ab7a8 --- /dev/null +++ b/lib/console/Commands/SystemCommands.h @@ -0,0 +1,16 @@ +#pragma once + +#include "../ConsoleCommand.h" + +namespace ESP32Console::Commands +{ + const ConsoleCommand getSysInfoCommand(); + + const ConsoleCommand getRestartCommand(); + + const ConsoleCommand getMemInfoCommand(); + + const ConsoleCommand getTaskInfoCommand(); + + const ConsoleCommand getDateCommand(); +}; \ No newline at end of file diff --git a/lib/console/Commands/VFSCommands.cpp b/lib/console/Commands/VFSCommands.cpp new file mode 100644 index 000000000..04c712ab7 --- /dev/null +++ b/lib/console/Commands/VFSCommands.cpp @@ -0,0 +1,490 @@ +#include "VFSCommands.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../Console.h" +#include "../Helpers/PWDHelpers.h" +#include "../ute/ute.h" + +#include "../device/fuji.h" +#include "display.h" +#include "meatloaf.h" +#include "string_utils.h" + +char *canonicalize_file_name(const char *path); + +int cat(int argc, char **argv) +{ + if (argc == 1) + { + fprintf(stderr, "You have to pass at least one file path!\r\n"); + return EXIT_SUCCESS; + } + + for (int n = 1; n < argc; n++) + { + char filename[PATH_MAX]; + // We have manually do resolving of . and .., as VFS does not do it + ESP32Console::console_realpath(argv[n], filename); + + FILE *file = fopen(filename, "r"); + if (file == nullptr) + { + fprintf(stderr, "%s %s : %s\r\n", argv[0], filename, + strerror(errno)); + return errno; + } + + int chr; + while ((chr = getc(file)) != EOF) + fprintf(stdout, "%c", chr); + fclose(file); + } + + return EXIT_SUCCESS; +} + +int pwd(int argc, char **argv) +{ + printf("%s\r\n", ESP32Console::console_getpwd()); + return EXIT_SUCCESS; +} + +int cd(int argc, char **argv) +{ + const char *path; + + if (argc != 2) + { + path = getenv("HOME"); + if (!path) + { + fprintf(stderr, "No HOME env variable set!\r\n"); + return EXIT_FAILURE; + } + } + else + { + path = argv[1]; + } + + // Check if target path is a file + char resolved[PATH_MAX]; + ESP32Console::console_realpath(path, resolved); + + // Get file stats + struct stat st; + stat(resolved, &st); + //Debug_printv("path[%s] resolved[%s]", path, resolved); + + // If we can open it, then we can not chdir into it. + //FILE *file = fopen(resolved, "r"); + //if (file) + if(!S_ISDIR(st.st_mode)) + { + //fclose(file); + fprintf(stderr, "cd: not a directory: %s\r\n", path); + return 1; + } + + + // Check if the new PWD exists, and show a warning if not + //const char *pwd = ESP32Console::console_getpwd(); + DIR *dir = opendir(resolved); + if (dir) + { + closedir(dir); + + if (ESP32Console::console_chdir(path)) + { + fprintf(stderr, "Error: %s\r\n", strerror(errno)); + return 1; + } + } + // else if (ENOENT == errno) + // { + // fprintf(stderr, "cd: no such file or directory: %s\r\n", path); + // } + + return EXIT_SUCCESS; +} + +int ls(int argc, char **argv) +{ + char *inpath; + if (argc == 1) + { + inpath = (char *)"."; + } + else if (argc == 2) + { + inpath = argv[1]; + } + else + { + printf("You can pass only one filename!\r\n"); + return 1; + } + + char path[PATH_MAX]; + ESP32Console::console_realpath(inpath, path); + + DIR *dir = opendir(path); + if (!dir) + { + fprintf(stderr, "Could not open filepath: %s\r\n", strerror(errno)); + return 1; + } + + struct dirent *d; + struct stat st; + + // Add "sd" if we are at the root + if ( mstr::equals(path, (char *)"/", false) ) + { + printf("d %8u sd\r\n", 0); + } + + while ((d = readdir(dir)) != NULL) + { + std::string filename = path; + filename += "/"; + filename += d->d_name; + stat(filename.c_str(), &st); + printf("%c %8lu %s\r\n", (S_ISDIR(st.st_mode)) ? 'd':'-', st.st_size, d->d_name); + } + + closedir(dir); + return EXIT_SUCCESS; +} + +int mv(int argc, char **argv) +{ + if (argc != 3) + { + fprintf(stderr, "Syntax is mv [ORIGIN] [TARGET]\r\n"); + return 1; + } + + char old_name[PATH_MAX], new_name[PATH_MAX]; + + // Resolve arguments to full path + ESP32Console::console_realpath(argv[1], old_name); + ESP32Console::console_realpath(argv[2], new_name); + + // Do rename + if (rename(old_name, new_name)) + { + printf("Error moving: %s\r\n", strerror(errno)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +int cp(int argc, char **argv) +{ + //TODO: Shows weird error message + if (argc != 3) + { + fprintf(stderr, "Syntax is cp [ORIGIN] [TARGET]\r\n"); + return 1; + } + + char old_name[PATH_MAX], new_name[PATH_MAX]; + + // Resolve arguments to full path + ESP32Console::console_realpath(argv[1], old_name); + ESP32Console::console_realpath(argv[2], new_name); + + // Do copy + FILE *origin = fopen(old_name, "r"); + if (!origin) + { + fprintf(stderr, "Error opening origin file: %s\r\n", strerror(errno)); + return 1; + } + + FILE *target = fopen(new_name, "w"); + if (!target) + { + fclose(origin); + fprintf(stderr, "Error opening target file: %s\r\n", strerror(errno)); + return 1; + } + + int buffer; + + // Clear existing errors + auto error = errno; + + while ((buffer = getc(origin)) != EOF) + { + if(fputc(buffer, target) == EOF) { + fprintf(stderr, "Error writing: %s\r\n", strerror(errno)); + fclose(origin); fclose(target); + return 1; + } + } + + error = errno; + if (error && !feof(origin)) + { + fprintf(stderr, "Error copying: %s\r\n", strerror(error)); + fclose(origin); + fclose(target); + return 1; + } + + fclose(origin); + fclose(target); + + return EXIT_SUCCESS; +} + +int rm(int argc, char **argv) +{ + if (argc != 2) + { + fprintf(stderr, "You have to pass exactly one file. Syntax rm [FILE]\r\n"); + return EXIT_SUCCESS; + } + + char filename[PATH_MAX]; + ESP32Console::console_realpath(argv[1], filename); + //Debug_printv("filename[%s]", filename); + + if ( strlen(filename) > 1 && filename[strlen(filename) - 1] == '*' ) + { + char path[PATH_MAX]; + ESP32Console::console_realpath(".", path); + + DIR *dir = opendir(path); + struct dirent *d; + while ((d = readdir(dir)) != NULL) + { + std::string pattern = filename; + std::string match_file = path; + if (strlen(path) > 1) + match_file += "/"; + match_file += d->d_name; + //Debug_printv("pattern[%s] match_file[%s]", pattern.c_str(), match_file.c_str()); + if ( mstr::compare(pattern, match_file, false) ) + { + if (remove(match_file.c_str())) + { + fprintf(stderr, "Error removing %s: %s\r\n", filename, strerror(errno)); + closedir(dir); + return EXIT_FAILURE; + } + printf("%s removed\r\n", d->d_name); + } + } + closedir(dir); + } + else + { + if(remove(filename)) { + fprintf(stderr, "Error removing %s: %s\r\n", filename, strerror(errno)); + return EXIT_FAILURE; + } + printf("%s removed\r\n", filename); + } + + return EXIT_SUCCESS; +} + +int rmdir(int argc, char **argv) +{ + if (argc != 2) + { + fprintf(stderr, "You have to pass exactly one file. Syntax rmdir [DIRECTORY]\r\n"); + return EXIT_SUCCESS; + } + + char filename[PATH_MAX]; + ESP32Console::console_realpath(argv[1], filename); + + if(rmdir(filename)) { + fprintf(stderr, "Error deleting %s: %s\r\n", filename, strerror(errno)); + } + + return EXIT_SUCCESS; +} + +int mkdir(int argc, char **argv) +{ + if (argc != 2) + { + fprintf(stderr, "You have to pass exactly one file. Syntax mkdir [DIRECTORY]\r\n"); + return EXIT_SUCCESS; + } + + char directory[PATH_MAX]; + ESP32Console::console_realpath(argv[1], directory); + + if(mkdir(directory, 0755)) { + fprintf(stderr, "Error creating %s: %s\r\n", directory, strerror(errno)); + } + + return EXIT_SUCCESS; +} + +int mount(int argc, char **argv) +{ + if (argc != 3) + { + fprintf(stderr, "mount {device id} {url}\r\n"); + return EXIT_SUCCESS; + } + + if (!mstr::isNumeric(argv[1])) + { + fprintf(stderr, "device id is not numeric\r\n"); + return EXIT_SUCCESS; + } + + int did = atoi(argv[1]); + fprintf(stdout, "Mounted %d -> %s\r\n", did, argv[2]); + + theFuji.mount_all(); + + return EXIT_SUCCESS; +} + +int wget(int argc, char **argv) +{ + if (argc != 2) + { + fprintf(stderr, "wget {url}\r\n"); + return EXIT_SUCCESS; + } + + std::string pwd = std::string(ESP32Console::console_getpwd()); + + auto f = MFSOwner::File(argv[1]); + if (f != nullptr) + { + auto s = f->getSourceStream(); + + std::string outfile = pwd; + outfile += f->name; + + Debug_printv("size[%lu] name[%s] url[%s] outfile[%s]", f->size, f->name.c_str(), s->url.c_str(), outfile.c_str()); + + + FILE *file = fopen(outfile.c_str(), "w"); + if (file == nullptr) + { + fprintf(stdout, "2 Error: Can't open file!\r\n"); + return 2; + } + + // Receive File + int count = 0; + uint8_t bytes[256]; + while (true) + { + int bytes_read = s->read(bytes, 256); + if (bytes_read < 1) + { + if (s->available()) + fprintf(stdout, "\nError reading '%s'\r", f->name.c_str()); + break; + } + + int bytes_written = fwrite(bytes, 1, bytes_read, file); + if (bytes_written != bytes_read) + { + fprintf(stdout, "\nError writing '%s'\r", f->name.c_str()); + break; + } + + // Show percentage complete in stdout + uint8_t percent = (s->position() * 100) / s->size(); +#ifdef ENABLE_DISPLAY + DISPLAY.progress = percent; +#endif + fprintf(stdout, "Downloading '%s' %d%% [%lu]\r", f->name.c_str(), percent, s->position()); + count++; + } + fclose(file); + fprintf(stdout, "\n"); + } + +#ifdef ENABLE_DISPLAY + DISPLAY.idle(); +#endif + + return EXIT_SUCCESS; +} + +namespace ESP32Console::Commands +{ + const ConsoleCommand getCatCommand() + { + return ConsoleCommand("cat", &cat, "Show the content of one or more files."); + } + + const ConsoleCommand getPWDCommand() + { + return ConsoleCommand("pwd", &pwd, "Show the current working dir"); + } + + const ConsoleCommand getCDCommand() + { + return ConsoleCommand("cd", &cd, "Change the working directory"); + } + + const ConsoleCommand getLsCommand() + { + return ConsoleCommand("ls", &ls, "List the contents of the given path"); + } + + const ConsoleCommand getMvCommand() + { + return ConsoleCommand("mv", &mv, "Move the given file to another place or name"); + } + + const ConsoleCommand getCPCommand() + { + return ConsoleCommand("cp", &cp, "Copy the given file to another place or name"); + } + + const ConsoleCommand getRMCommand() + { + return ConsoleCommand("rm", &rm, "Permanenty deletes the given file."); + } + + const ConsoleCommand getRMDirCommand() + { + return ConsoleCommand("rmdir", &rmdir, "Permanenty deletes the given folder. Folder must be empty!"); + } + + const ConsoleCommand getMKDirCommand() + { + return ConsoleCommand("mkdir", &mkdir, "Create the DIRECTORY(ies), if they do not already exist."); + } + + const ConsoleCommand getEditCommand() + { + return ConsoleCommand("edit", &ute, "Edit files"); + } + + const ConsoleCommand getMountCommand() + { + return ConsoleCommand("mount", &mount, "Mount url on device id"); + } + + const ConsoleCommand getWgetCommand() + { + return ConsoleCommand("wget", &wget, "Download url to file"); + } +} \ No newline at end of file diff --git a/lib/console/Commands/VFSCommands.h b/lib/console/Commands/VFSCommands.h new file mode 100644 index 000000000..3d79272c0 --- /dev/null +++ b/lib/console/Commands/VFSCommands.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../ConsoleCommand.h" + +namespace ESP32Console::Commands +{ + const ConsoleCommand getCatCommand(); + + const ConsoleCommand getPWDCommand(); + + const ConsoleCommand getCDCommand(); + + const ConsoleCommand getLsCommand(); + + const ConsoleCommand getMvCommand(); + + const ConsoleCommand getCPCommand(); + + const ConsoleCommand getRMCommand(); + + const ConsoleCommand getRMDirCommand(); + + const ConsoleCommand getMKDirCommand(); + + const ConsoleCommand getEditCommand(); + + const ConsoleCommand getStatusCommand(); + + const ConsoleCommand getMountCommand(); + + const ConsoleCommand getCRC32Command(); + + const ConsoleCommand getWgetCommand(); +} \ No newline at end of file diff --git a/lib/console/Commands/XFERCommands.cpp b/lib/console/Commands/XFERCommands.cpp new file mode 100644 index 000000000..61df20cdc --- /dev/null +++ b/lib/console/Commands/XFERCommands.cpp @@ -0,0 +1,186 @@ +#include "XFERCommands.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils.h" + +#include "../Console.h" +#include "../Helpers/PWDHelpers.h" + +char *canonicalize_file_name(const char *path); + +std::string read_until(char delimiter) +{ + uint8_t byte = 0; + std::string response; + while (byte != delimiter) + { + size_t size = 0; + uart_get_buffered_data_len(CONSOLE_UART, &size); + if (size > 0) + { + int result = uart_read_bytes(CONSOLE_UART, &byte, 1, MAX_READ_WAIT_TICKS); + if (result < 1) + { + fprintf(stdout, "3 Error: Response Timeout\r\n"); + return ""; + } + + if (byte != delimiter) + response.push_back(byte); + } + } + return response; +} + +int rx(int argc, char **argv) +{ + // rx {filename} + if (argc != 2) + { + fprintf(stderr, "rx {filename}\r\n"); + return EXIT_SUCCESS; + } + + char filename[PATH_MAX]; + ESP32Console::console_realpath(argv[1], filename); + + // get file size and checksum + std::string s = read_until(' '); + int size = atoi(s.c_str()); + std::string src_checksum = read_until('\n'); + + FILE *file = fopen(filename, "w"); + if (file == nullptr) + { + fprintf(stdout, "2 Error: Can't open file!\r\n"); + return 2; + } + + // Receive File + int count = 0; + uint8_t byte = 0; + int dest_checksum = 0; + while (count < size) + { + size_t size = 0; + uart_get_buffered_data_len(CONSOLE_UART, &size); + if (size > 0) + { + int result = uart_read_bytes(CONSOLE_UART, &byte, 1, MAX_READ_WAIT_TICKS); + if (result < 1) + break; + + fprintf(file, "%c", byte); + + // Calculate checksum + dest_checksum = esp_rom_crc32_le(dest_checksum, &byte, 1); + count++; + } + } + fclose(file); + + // Check checksum + std::ostringstream ss; + ss << std::hex << dest_checksum; + std::string dest_checksum_str = ss.str(); + if ( !mstr::compare(dest_checksum_str, src_checksum) ) + { + fprintf(stdout, "2 Error: Checksum mismatch!\r\n"); + return 2; + } + + fprintf(stdout, "0 OK\r\n"); + return EXIT_SUCCESS; +} + +int tx(int argc, char **argv) +{ + // tx {filename} + if (argc != 2) + { + fprintf(stderr, "tx {filename}\r\n"); + return EXIT_SUCCESS; + } + + // Get file size + char filename[PATH_MAX]; + ESP32Console::console_realpath(argv[1], filename); + struct stat file_stat; + stat(filename, &file_stat); + int size = file_stat.st_size; + + // Receive File + uint8_t buffer[256]; + int bytesRead = 0; + + // Calculate checksum + int src_checksum = 0; + FILE *file = fopen(filename, "r"); + if (file == nullptr) + { + fprintf(stdout, "2 Error: Can't open file!\r\n"); + return 2; + } + else + { + // Read file 256 bytes at a time and calculate checksum + while ((bytesRead = fread(buffer, 1, 256, file)) > 0) + { + src_checksum = esp_rom_crc32_le(src_checksum, buffer, bytesRead); + } + fseek(file, 0, SEEK_SET); + } + + // Send size and checksum + fprintf(stdout, "%d %8x\r\n", size, src_checksum); + + // Send file 256 bytes at a time + while ((bytesRead = fread(buffer, 1, 256, file)) > 0) + { + // print buffer bytes + for (int i = 0; i < bytesRead; i++) { + fprintf(stdout, "%c", buffer[i]); + } + } + fclose(file); + + // End file data with CRLF + fprintf(stdout, "\r\n"); + + // Read response + std::string response = read_until('\n'); + + if (!mstr::startsWith(response, "0 OK")) + { + fprintf(stdout, "2 Error: %s\r\n", response.c_str()); + return 2; + } + + return EXIT_SUCCESS; +} + + +namespace ESP32Console::Commands +{ + const ConsoleCommand getRXCommand() + { + return ConsoleCommand("rx", &rx, "Receive file"); + } + + const ConsoleCommand getTXCommand() + { + return ConsoleCommand("tx", &tx, "Transmit file"); + } +} \ No newline at end of file diff --git a/lib/console/Commands/XFERCommands.h b/lib/console/Commands/XFERCommands.h new file mode 100644 index 000000000..3e0ce1659 --- /dev/null +++ b/lib/console/Commands/XFERCommands.h @@ -0,0 +1,35 @@ +#pragma once + +// Meatloaf Serial Transfer Protocol +// +// rx "filename" - receive file. "filename" can include a path to where the file is to be stored. +// tx "filename" [offset] [length] - transmit file with option to request start from offset and byte range. +// status [rx|tx] - status of last recieve/transmit +// mount "filename" /dev/{device id} - mount the file on specified device +// crc32 "filename" - get crc32 of file to detect change +// ​ +// ​The data block would look like the following. Similar to HTTP Chunked Transfer encoding. +// ```sender +// {size} {checksum}\r\n +// {data}\r\n +// ```receiver +// {status code} {status description}] +// ``` +// Receiver would acknowledge with: +// '0 OK' +// '1 RESEND' +// '2+ ERROR' +// +// Transfer stops on ERROR. (any status code greater than 1) +// Checksum will be CRC32 of data. +// Last block size and checksum will be '0'. +// + +#include "../ConsoleCommand.h" + +namespace ESP32Console::Commands +{ + const ConsoleCommand getRXCommand(); + + const ConsoleCommand getTXCommand(); +} \ No newline at end of file diff --git a/lib/console/Console.cpp b/lib/console/Console.cpp new file mode 100644 index 000000000..6747f598e --- /dev/null +++ b/lib/console/Console.cpp @@ -0,0 +1,311 @@ +#include "Console.h" + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "soc/soc_caps.h" +#include "esp_err.h" +#include "esp_log.h" + +#include "Commands/CoreCommands.h" +#include "Commands/SystemCommands.h" +#include "Commands/NetworkCommands.h" +#include "Commands/VFSCommands.h" +#include "Commands/GPIOCommands.h" +#include "Commands/XFERCommands.h" +#include "driver/uart.h" +#include "esp_vfs_dev.h" +#include "linenoise/linenoise.h" +#include "Helpers/PWDHelpers.h" +#include "Helpers/InputParser.h" + +#include "../../include/debug.h" +#include "string_utils.h" + +using namespace ESP32Console::Commands; + +namespace ESP32Console +{ + void Console::registerCoreCommands() + { + registerCommand(getClearCommand()); + registerCommand(getHistoryCommand()); + registerCommand(getEchoCommand()); + registerCommand(getSetMultilineCommand()); + registerCommand(getEnvCommand()); + registerCommand(getDeclareCommand()); +#ifdef ENABLE_DISPLAY + registerCommand(getLEDCommand()); +#endif + } + + void Console::registerSystemCommands() + { + registerCommand(getSysInfoCommand()); + registerCommand(getRestartCommand()); + registerCommand(getMemInfoCommand()); + registerCommand(getTaskInfoCommand()); + registerCommand(getDateCommand()); + } + + void ESP32Console::Console::registerNetworkCommands() + { + registerCommand(getPingCommand()); + registerCommand(getIpconfigCommand()); + registerCommand(getScanCommand()); + registerCommand(getConnectCommand()); + registerCommand(getIMPROVCommand()); + } + + void Console::registerVFSCommands() + { + registerCommand(getCatCommand()); + registerCommand(getCDCommand()); + registerCommand(getPWDCommand()); + registerCommand(getLsCommand()); + registerCommand(getMvCommand()); + registerCommand(getCPCommand()); + registerCommand(getRMCommand()); + registerCommand(getRMDirCommand()); + registerCommand(getMKDirCommand()); + registerCommand(getEditCommand()); + registerCommand(getMountCommand()); + registerCommand(getWgetCommand()); + } + + void Console::registerGPIOCommands() + { + registerCommand(getPinModeCommand()); + registerCommand(getDigitalReadCommand()); + registerCommand(getDigitalWriteCommand()); + registerCommand(getAnalogReadCommand()); + } + + void Console::registerXFERCommands() + { + registerCommand(getRXCommand()); + registerCommand(getTXCommand()); + } + + + void Console::beginCommon() + { + /* Tell linenoise where to get command completions and hints */ + linenoiseSetCompletionCallback(&esp_console_get_completion); + linenoiseSetHintsCallback((linenoiseHintsCallback *)&esp_console_get_hint); + + /* Set command history size */ + linenoiseHistorySetMaxLen(max_history_len_); + + /* Set command maximum length */ + linenoiseSetMaxLineLen(max_cmdline_len_); + + // Load history if defined + if (history_save_path_) + { + linenoiseHistoryLoad(history_save_path_); + } + + // Register core commands like echo + esp_console_register_help_command(); + registerCoreCommands(); + } + + void Console::begin(int baud, int rxPin, int txPin, uint8_t channel) + { + Debug_printv("Initialize console"); + + if (channel >= SOC_UART_NUM) + { + Debug_printv("Serial number is invalid, please use numers from 0 to %u", SOC_UART_NUM - 1); + return; + } + + this->uart_channel_ = channel; + + //Reinit the UART driver if the channel was already in use + if (uart_is_driver_installed(channel)) { + uart_driver_delete(channel); + } + + /* Drain stdout before reconfiguring it */ + fflush(stdout); + fsync(fileno(stdout)); + + /* Disable buffering on stdin */ + setvbuf(stdin, NULL, _IONBF, 0); + + /* Minicom, screen, idf_monitor send CR when ENTER key is pressed */ + esp_vfs_dev_uart_port_set_rx_line_endings(channel, ESP_LINE_ENDINGS_CR); + /* Move the caret to the beginning of the next line on '\n' */ + esp_vfs_dev_uart_port_set_tx_line_endings(channel, ESP_LINE_ENDINGS_CRLF); + + /* Enable non-blocking mode on stdin and stdout */ + fcntl(fileno(stdout), F_SETFL, 0); + fcntl(fileno(stdin), F_SETFL, 0); + + + /* Configure UART. Note that REF_TICK is used so that the baud rate remains + * correct while APB frequency is changing in light sleep mode. + */ + const uart_config_t uart_config = { + .baud_rate = baud, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .source_clk = UART_SCLK_DEFAULT, + }; + + + ESP_ERROR_CHECK(uart_param_config(channel, &uart_config)); + + // Set the correct pins for the UART of needed + if (rxPin > 0 || txPin > 0) { + if (rxPin < 0 || txPin < 0) { + Debug_printv("Both rxPin and txPin has to be passed!"); + } + uart_set_pin(channel, txPin, rxPin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + } + + /* Install UART driver for interrupt-driven reads and writes */ + ESP_ERROR_CHECK(uart_driver_install(channel, 256, 0, 0, NULL, 0)); + + /* Tell VFS to use UART driver */ + esp_vfs_dev_uart_use_driver(channel); + + esp_console_config_t console_config = { + .max_cmdline_length = max_cmdline_len_, + .max_cmdline_args = max_cmdline_args_, + .hint_color = 333333 + }; + + ESP_ERROR_CHECK(esp_console_init(&console_config)); + + beginCommon(); + + // Start REPL task + if (xTaskCreatePinnedToCore(&Console::repl_task, "console_repl", task_stack_size_, this, task_priority_, &task_, 0) != pdTRUE) + { + Debug_printv("Could not start REPL task!"); + } + } + + static void resetAfterCommands() + { + //Reset all global states a command could change + + //Reset getopt parameters + optind = 0; + } + + void Console::repl_task(void *args) + { + Console const &console = *(static_cast(args)); + + /* Change standard input and output of the task if the requested UART is + * NOT the default one. This block will replace stdin, stdout and stderr. + * We have to do this in the repl task (not in the begin, as these settings are only valid for the current task) + */ + // if (console.uart_channel_ != CONFIG_ESP_CONSOLE_UART_NUM) + // { + // char path[13] = {0}; + // snprintf(path, 13, "/dev/uart/%1d", console.uart_channel_); + + // stdin = fopen(path, "r"); + // stdout = fopen(path, "w"); + // stderr = stdout; + // } + + //setvbuf(stdin, NULL, _IONBF, 0); + + /* This message shall be printed here and not earlier as the stdout + * has just been set above. */ + // printf("\r\n" + // "Type 'help' to get the list of commands.\r\n" + // "Use UP/DOWN arrows to navigate through command history.\r\n" + // "Press TAB when typing command name to auto-complete.\r\n"); + + // Probe terminal status + int probe_status = linenoiseProbe(); + if (probe_status) + { + linenoiseSetDumbMode(1); + } + + // if (linenoiseIsDumbMode()) + // { + // printf("\r\n" + // "Your terminal application does not support escape sequences.\n\n" + // "Line editing and history features are disabled.\n\n" + // "On Windows, try using Putty instead.\r\n"); + // } + + linenoiseSetMaxLineLen(console.max_cmdline_len_); + while (true) + { + std::string prompt = console.prompt_; + + // Insert current PWD into prompt if needed + mstr::replaceAll(prompt, "%pwd%", console_getpwd()); + + char *line = linenoise(prompt.c_str()); + if (line == NULL) + { + Debug_printv("empty line"); + /* Ignore empty lines */ + continue; + } + + //Debug_printv("Line received from linenoise: [%s]\n", line); + + // /* Add the command to the history */ + // linenoiseHistoryAdd(line); + + // /* Save command history to filesystem */ + // if (console.history_save_path_) + // { + // linenoiseHistorySave(console.history_save_path_); + // } + + //Interpolate the input line + std::string interpolated_line = interpolateLine(line); + //Debug_printv("Interpolated line: [%s]\n", interpolated_line.c_str()); + + // Flush trailing CR + uart_flush(CONSOLE_UART); + + /* Try to run the command */ + int ret; + esp_err_t err = esp_console_run(interpolated_line.c_str(), &ret); + + //Reset global state + resetAfterCommands(); + + if (err == ESP_ERR_NOT_FOUND) + { + printf("Unrecognized command\n"); + } + else if (err == ESP_ERR_INVALID_ARG) + { + // command was empty + } + else if (err == ESP_OK && ret != ESP_OK) + { + // printf("Command returned non-zero error code: 0x%x (%s)\n", ret, esp_err_to_name(ret)); + } + else if (err != ESP_OK) + { + printf("Internal error: %s\n", esp_err_to_name(err)); + } + /* linenoise allocates line buffer on the heap, so need to free it */ + linenoiseFree(line); + } + //Debug_printv("REPL task ended"); + vTaskDelete(NULL); + esp_console_deinit(); + } + + void Console::end() + { + } +}; \ No newline at end of file diff --git a/lib/console/Console.h b/lib/console/Console.h new file mode 100644 index 000000000..b5632ab4c --- /dev/null +++ b/lib/console/Console.h @@ -0,0 +1,156 @@ +#pragma once + +#include "esp_console.h" + +#include "ConsoleCommandBase.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "linenoise/linenoise.h" + +#include "../../include/debug.h" + +#define CONSOLE_UART 0 +#define MAX_READ_WAIT_TICKS 200 + +namespace ESP32Console +{ + class Console + { + private: + const char *prompt_ = "ESP32> "; + const uint32_t task_priority_; + const BaseType_t task_stack_size_; + + uint16_t max_history_len_ = 40; + const char* history_save_path_ = nullptr; + + const size_t max_cmdline_len_; + const size_t max_cmdline_args_; + + uint8_t uart_channel_; + + TaskHandle_t task_; + + static void repl_task(void *args); + + void beginCommon(); + + public: + /** + * @brief Create a new ESP32Console with the default parameters + */ + Console(const uint32_t task_stack_size = 8192, const BaseType_t task_priority = 4, int max_cmdline_len = 256, int max_cmdline_args = 8) : task_priority_(task_priority), task_stack_size_(task_stack_size), max_cmdline_len_(max_cmdline_len), max_cmdline_args_(max_cmdline_args){}; + + ~Console() + { + vTaskDelete(task_); + end(); + } + + /** + * @brief Register the given command, using the raw ESP-IDF structure. + * + * @param cmd The command that should be registered + * @return Return true, if the registration was successfull, false if not. + */ + bool registerCommand(const esp_console_cmd_t *cmd) + { + //Debug_printv("Registering new command %s", cmd->command); + + auto code = esp_console_cmd_register(cmd); + if (code != ESP_OK) + { + Debug_printv("Error registering command (Reason %s)", esp_err_to_name(code)); + return false; + } + + return true; + } + + /** + * @brief Register the given command + * + * @param cmd The command that should be registered + * @return true If the command was registered successful. + * @return false If the command was not registered because of an error. + */ + bool registerCommand(const ConsoleCommandBase &cmd) + { + auto c = cmd.toCommandStruct(); + return registerCommand(&c); + } + + /** + * @brief Registers the given command + * + * @param command The name under which the command can be called (e.g. "ls"). Must not contain spaces. + * @param func A pointer to the function which should be run, when this command is called + * @param help A text shown in output of "help" command describing this command. When empty it is not shown in help. + * @param hint A text describing the usage of the command in help output + * @return true If the command was registered successful. + * @return false If the command was not registered because of an error. + */ + bool registerCommand(const char *command, esp_console_cmd_func_t func, const char *help, const char *hint = "") + { + const esp_console_cmd_t cmd = { + .command = command, + .help = help, + .hint = hint, + .func = func, + .argtable = nullptr + }; + + return registerCommand(&cmd); + }; + + void registerCoreCommands(); + + void registerSystemCommands(); + + void registerNetworkCommands(); + + void registerVFSCommands(); + + void registerGPIOCommands(); + + void registerXFERCommands(); + + /** + * @brief Set the command prompt. Default is "ESP32>". + * + * @param prompt + */ + void setPrompt(const char *prompt) { prompt_ = prompt; }; + + /** + * @brief Set the History Max Length object + * + * @param max_length + */ + void setHistoryMaxLength(uint16_t max_length) + { + max_history_len_ = max_length; + linenoiseHistorySetMaxLen(max_length); + } + + /** + * @brief Enable saving of command history, which makes history persistent over resets. SPIFF need to be enabled, or you need to pass the filename to use. + * + * @param history_save_path The file which will be used to save command history. Set to nullptr to disable persistent saving + */ + void enablePersistentHistory(const char *history_save_path = "/spiffs/.history.txt") { history_save_path_ = history_save_path; }; + + /** + * @brief Starts the console. Similar to the Serial.begin() function + * + * @param baud The baud rate with which the console should work. Recommended: 115200 + * @param rxPin The pin to use for RX + * @param txPin The pin to use for TX + * @param channel The number of the UART to use + */ + void begin(int baud, int rxPin = -1, int txPin = -1, uint8_t channel = 0); + + void end(); + }; +}; \ No newline at end of file diff --git a/lib/console/ConsoleCommand.h b/lib/console/ConsoleCommand.h new file mode 100644 index 000000000..5e3da9c5c --- /dev/null +++ b/lib/console/ConsoleCommand.h @@ -0,0 +1,41 @@ +#pragma once + +#include "./ConsoleCommandBase.h" + +namespace ESP32Console +{ + + /** + * @brief A class for registering custom console commands via a passed function pointer. + * + */ + class ConsoleCommand : public ConsoleCommandBase + { + protected: + const char *hint_; + + public: + /** + * @brief Creates a new ConsoleCommand object with the given parameters. + * + * @param command The name under which the command will be called (e.g. "ls"). Must not contain spaces. + * @param func The pointer to the function which is run if this function is called. Takes two paramaters argc and argv, similar to a classical C program. + * @param help The text shown in "help" output for this command. If set to empty string, then the command is not shown in help. + * @param hint A hint explaining the parameters in help output + */ + ConsoleCommand(const char *command, esp_console_cmd_func_t func, const char* help, const char* hint = "") { command_ = command; func_ = func; help_= help; hint_ = hint;}; + + const esp_console_cmd_t toCommandStruct() const override + { + const esp_console_cmd_t cmd = { + .command = command_, + .help = help_, + .hint = hint_, + .func = func_, + .argtable = nullptr + }; + + return cmd; + } + }; +}; \ No newline at end of file diff --git a/lib/console/ConsoleCommandBase.h b/lib/console/ConsoleCommandBase.h new file mode 100644 index 000000000..e1fcde721 --- /dev/null +++ b/lib/console/ConsoleCommandBase.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esp_console.h" + +namespace ESP32Console { + class ConsoleCommandBase + { + protected: + const char *command_; + const char *help_; + esp_console_cmd_func_t func_; + + public: + /** + * @brief Get the command name + * + * @return const char* + */ + const char* getCommand() { return command_; }; + + const char* getHelp() { return help_; }; + + virtual const esp_console_cmd_t toCommandStruct() const = 0; + }; +}; \ No newline at end of file diff --git a/lib/console/ConsoleCommandD.cpp b/lib/console/ConsoleCommandD.cpp new file mode 100644 index 000000000..fde2cbc5e --- /dev/null +++ b/lib/console/ConsoleCommandD.cpp @@ -0,0 +1,34 @@ +#include "./ConsoleCommandD.h" + +#include "../../include/debug.h" + +namespace ESP32Console +{ + std::unordered_map ConsoleCommandD::registry_ = std::unordered_map(); + + int ConsoleCommandD::delegateResolver(int argc, char **argv) + { + // Retrieve ConsoleCommandD from registry + auto it = registry_.find(argv[0]); + if (it == registry_.end()) + { + Debug_printv("Could not resolve the delegated function call!"); + return 1; + } + + delegateFunc command = it->second; + + int code = 0; + + // try + // { + return command(argc, argv); + // } + // catch (const std::exception &err) + // { + // printf("%s", err.what()); + // printf("\r\n"); + // return 1; + // } + } +} \ No newline at end of file diff --git a/lib/console/ConsoleCommandD.h b/lib/console/ConsoleCommandD.h new file mode 100644 index 000000000..7c4234d6e --- /dev/null +++ b/lib/console/ConsoleCommandD.h @@ -0,0 +1,51 @@ +#pragma once + +#include "./ConsoleCommand.h" +#include "esp_console.h" +#include +#include +#include + +#include + +namespace ESP32Console +{ + using delegateFunc = std::function; + + /** + * @brief A class for registering custom console commands via delegate function element. The difference to ConsoleCommand is that you can pass a std::function object instead of a function pointer directly. + * This allows for use of lambda functions. The disadvantage is that we need more heap, as we have to save the delegate function objects in a map. + * + */ + class ConsoleCommandD : public ConsoleCommand + { + protected: + delegateFunc delegateFn_; + + static int delegateResolver(int argc, char **argv); + + public: + static std::unordered_map registry_; + + ConsoleCommandD(const char *command, delegateFunc func, const char* help, const char* hint = ""): ConsoleCommand(command, &delegateResolver, help, hint), delegateFn_(func) {}; + + const esp_console_cmd_t toCommandStruct() const override + { + const esp_console_cmd_t cmd = { + .command = command_, + .help = help_, + .hint = hint_, + .func = func_, + .argtable = nullptr + }; + + // When the command gets registered add it to our map, so we can access it later to resolve the delegated function call + registry_.insert({std::string(command_), std::move(delegateFn_)}); + + return cmd; + } + + delegateFunc &getDelegateFunction() { return delegateFn_; } + }; + +} \ No newline at end of file diff --git a/lib/console/ESP32Console.h b/lib/console/ESP32Console.h new file mode 100644 index 000000000..b539c8972 --- /dev/null +++ b/lib/console/ESP32Console.h @@ -0,0 +1,11 @@ +#pragma once + +// https://github.com/jbtronics/ESP32Console + + +#define ESP32CONSOLE_VERSION "1.2.2" + +#include "Console.h" +#include "ConsoleCommand.h" +#include "ConsoleCommandD.h" +#include "OptionsConsoleCommand.h" \ No newline at end of file diff --git a/lib/console/Helpers/InputParser.cpp b/lib/console/Helpers/InputParser.cpp new file mode 100644 index 000000000..cb6b2de47 --- /dev/null +++ b/lib/console/Helpers/InputParser.cpp @@ -0,0 +1,58 @@ +#include "InputParser.h" + +#include +#include "string_utils.h" + +namespace ESP32Console +{ + std::string interpolateLine(const char *in_) + { + std::string in(in_); + std::string out = in; + + // Add a space so this can be processed as a command + mstr::replaceAll(out, "IMPROV", "improv "); + + // Add a space at end of line, this does not change anything for our consoleLine and makes parsing easier + in = in + " "; + + // Interpolate each $ with the env variable if existing. If $ is the first character in a line it is not interpolated + int var_index = 1; + while ((var_index = in.find_first_of("$", var_index + 1)) > 0) + { + /** + * Extract the possible env variable + */ + int variable_start = var_index + 1; + // If the char after $ is a { we look for an closing }. Otherwise we just look for an space + char delimiter = ' '; + if (in[variable_start] == '{') + { + // Our variable starts then at the character after ${ + variable_start++; + } + + int variable_end = in.find_first_of(delimiter, variable_start + 1); + // If delimiter not found look for next possible env variable + if (variable_end == -1) + { + continue; + } + + variable_end -= variable_start; + std::string env_var = in.substr(variable_start, variable_end); + mstr::trim(env_var); + // Depending on whether this is an variable string, we have to include the next character + std::string replace_target = in.substr(var_index, delimiter == '}' ? variable_end + 1 : variable_end); + + // Check if we have an env with this name, then replace it + const char *value = getenv(env_var.c_str()); + if (value) + { + mstr::replaceAll(out, replace_target, value); + } + } + + return out.c_str(); + } +} \ No newline at end of file diff --git a/lib/console/Helpers/InputParser.h b/lib/console/Helpers/InputParser.h new file mode 100644 index 000000000..e2d1b8197 --- /dev/null +++ b/lib/console/Helpers/InputParser.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace ESP32Console +{ + /** + * @brief Interpolate the given line using environment variables. $VAR and ${ENV} are replaced by the representive values of the environment variables. + * + * @param in + * @return String + */ + std::string interpolateLine(const char *in); +} \ No newline at end of file diff --git a/lib/console/Helpers/PWDHelpers.cpp b/lib/console/Helpers/PWDHelpers.cpp new file mode 100644 index 000000000..cc9b32dd2 --- /dev/null +++ b/lib/console/Helpers/PWDHelpers.cpp @@ -0,0 +1,64 @@ +#include "PWDHelpers.h" + +#include +#include +#include +#include "string_utils.h" + +#include "../Console.h" + +namespace ESP32Console +{ + constexpr char *PWD_DEFAULT = (char*) "/"; + + const char *console_getpwd() + { + char *pwd = getenv("PWD"); + if (pwd) + { // If we have defined a PWD value, return it + return pwd; + } + + // Otherwise set a default one + setenv("PWD", PWD_DEFAULT, 1); + return PWD_DEFAULT; + } + + const char *console_realpath(const char *path, char *resolvedPath) + { + std::string in = std::string(path); + std::string pwd = std::string(console_getpwd()); + std::string result; + // If path is not absolute we prepend our pwd + if (!mstr::startsWith(in, "/")) + { + result = pwd + "/" + in; + } + else + { + result = in; + } + + realpath(result.c_str(), resolvedPath); + return resolvedPath; + } + + int console_chdir(const char *path) + { + char buffer[PATH_MAX + 2]; + console_realpath(path, buffer); + + size_t buffer_len = strlen(buffer); + //If path does not end with slash, add it. + if(buffer[buffer_len - 1] != '/') + { + buffer[buffer_len] = '/'; + buffer[buffer_len + 1] = '\0'; + } + + setenv("PWD", buffer, 1); + + return 0; + } + +} \ No newline at end of file diff --git a/lib/console/Helpers/PWDHelpers.h b/lib/console/Helpers/PWDHelpers.h new file mode 100644 index 000000000..5f3c491ed --- /dev/null +++ b/lib/console/Helpers/PWDHelpers.h @@ -0,0 +1,22 @@ +#pragma once + +namespace ESP32Console +{ + + /** + * @brief Returns the current console process working dir + * + * @return const char* + */ + const char *console_getpwd(); + + /** + * @brief Resolves the given path using the console process working dir + * + * @return const char* + */ + const char *console_realpath(const char *path, char *resolvedPath); + + int console_chdir(const char *path); + +} \ No newline at end of file diff --git a/lib/console/OptionsConsoleCommand.cpp b/lib/console/OptionsConsoleCommand.cpp new file mode 100644 index 000000000..f6a718e30 --- /dev/null +++ b/lib/console/OptionsConsoleCommand.cpp @@ -0,0 +1,49 @@ +#include "./OptionsConsoleCommand.h" + +#include "../../include/debug.h" + +namespace ESP32Console +{ + std::unordered_map OptionsConsoleCommand::registry_ = std::unordered_map(); + + int OptionsConsoleCommand::delegateResolver(int argc, char **argv) + { + // Retrieve ArgParserCommand from registry + auto it = registry_.find(argv[0]); + if (it == registry_.end()) + { + Debug_printv("Could not resolve the delegated function call!"); + return 1; + } + + auto command = it->second; + + int code = 0; + +// try + { + auto options = command.options; + auto result = options.parse(argc, argv); + + //Print help on --help argument + if (result.count("help")) { + printf(options.help().c_str()); + printf("\r\n"); + return EXIT_SUCCESS; + } + + if (command.getVersion() && result.count("version")) { + printf("Version: %s\r\n", command.getVersion()); + return EXIT_SUCCESS; + } + + return command.delegateFn_(argc, argv, result, options); + } + // catch (const std::exception &err) + // { + // printf(err.what()); + // printf("\r\n"); + // return EXIT_FAILURE; + // } + } +} diff --git a/lib/console/OptionsConsoleCommand.h b/lib/console/OptionsConsoleCommand.h new file mode 100644 index 000000000..e670b8d3a --- /dev/null +++ b/lib/console/OptionsConsoleCommand.h @@ -0,0 +1,87 @@ +#pragma once + +#include "./ConsoleCommandBase.h" + +#include "esp_console.h" + +//This define is important, otherwise we get very high memory usage from regex +#define CXXOPTS_NO_REGEX 1 +#define CXXOPTS_NO_RTTI 1 +#include "cxxopts/cxxopts.hpp" +#include +#include +#include + +namespace ESP32Console +{ + using cxxopts::Options; + using cxxopts::ParseResult; + using argParseFunc = std::function; + + class OptionsConsoleCommand : public ConsoleCommandBase + { + protected: + argParseFunc delegateFn_; + const char *hint_; + const char *version_; + + static int delegateResolver(int argc, char **argv); + + public: + Options options; + static std::unordered_map registry_; + + OptionsConsoleCommand(const char *command, argParseFunc func, const char *help, const char* version = nullptr, const char *hint = nullptr): options(command, help) + { + command_ = command; + help_ = help; + version_ = version; + + if (hint) + { + hint_ = hint; + } + else + { + hint_ = "Use --help option of command for more info"; + } + + //Add an option + options.add_options() + ("help", "Show help", cxxopts::value()->default_value("false")) + ; + + if (version_) + { + options.add_options() + ("version", "Show version number of this command", cxxopts::value()->default_value("false")) + ; + } + + + + delegateFn_ = func; + func_ = &delegateResolver; + } + + const esp_console_cmd_t toCommandStruct() const override + { + const esp_console_cmd_t cmd = { + .command = command_, + .help = help_, + .hint = hint_, + .func = func_, + .argtable = nullptr + }; + + // When the command gets registered add it to our map, so we can access it later to resolve the delegated function call + registry_.insert({std::string(command_), std::move(*this)}); + + return cmd; + } + + argParseFunc &getDelegateFunction() { return delegateFn_; } + + const char* getVersion() const {return version_;}; + }; +} \ No newline at end of file diff --git a/lib/console/cxxopts/cxxopts.hpp b/lib/console/cxxopts/cxxopts.hpp new file mode 100644 index 000000000..de1d72bfe --- /dev/null +++ b/lib/console/cxxopts/cxxopts.hpp @@ -0,0 +1,2713 @@ +/* + +Copyright (c) 2014, 2015, 2016, 2017 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +#ifndef CXXOPTS_HPP_INCLUDED +#define CXXOPTS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__GNUC__) && !defined(__clang__) +# if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 +# define CXXOPTS_NO_REGEX true +# endif +#endif + +#ifndef CXXOPTS_NO_REGEX +# include +#endif // CXXOPTS_NO_REGEX + +// Nonstandard before C++17, which is coincidentally what we also need for +#ifdef __has_include +# if __has_include() +# include +# ifdef __cpp_lib_optional +# define CXXOPTS_HAS_OPTIONAL +# endif +# endif +#endif + +#if __cplusplus >= 201603L +#define CXXOPTS_NODISCARD [[nodiscard]] +#else +#define CXXOPTS_NODISCARD +#endif + +#ifndef CXXOPTS_VECTOR_DELIMITER +#define CXXOPTS_VECTOR_DELIMITER ',' +#endif + +#define CXXOPTS__VERSION_MAJOR 3 +#define CXXOPTS__VERSION_MINOR 0 +#define CXXOPTS__VERSION_PATCH 0 + +#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 + #define CXXOPTS_NULL_DEREF_IGNORE +#endif + +namespace cxxopts +{ + static constexpr struct { + uint8_t major, minor, patch; + } version = { + CXXOPTS__VERSION_MAJOR, + CXXOPTS__VERSION_MINOR, + CXXOPTS__VERSION_PATCH + }; +} // namespace cxxopts + +//when we ask cxxopts to use Unicode, help strings are processed using ICU, +//which results in the correct lengths being computed for strings when they +//are formatted for the help output +//it is necessary to make sure that can be found by the +//compiler, and that icu-uc is linked in to the binary. + +#ifdef CXXOPTS_USE_UNICODE +#include + +namespace cxxopts +{ + using String = icu::UnicodeString; + + inline + String + toLocalString(std::string s) + { + return icu::UnicodeString::fromUTF8(std::move(s)); + } + +#if defined(__GNUC__) +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: +// warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" +#pragma GCC diagnostic ignored "-Weffc++" +// This will be ignored under other compilers like LLVM clang. +#endif + class UnicodeStringIterator : public + std::iterator + { + public: + + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) + { + } + + value_type + operator*() const + { + return s->char32At(i); + } + + bool + operator==(const UnicodeStringIterator& rhs) const + { + return s == rhs.s && i == rhs.i; + } + + bool + operator!=(const UnicodeStringIterator& rhs) const + { + return !(*this == rhs); + } + + UnicodeStringIterator& + operator++() + { + ++i; + return *this; + } + + UnicodeStringIterator + operator+(int32_t v) + { + return UnicodeStringIterator(s, i + v); + } + + private: + const icu::UnicodeString* s; + int32_t i; + }; +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + + inline + String& + stringAppend(String&s, String a) + { + return s.append(std::move(a)); + } + + inline + String& + stringAppend(String& s, size_t n, UChar32 c) + { + for (size_t i = 0; i != n; ++i) + { + s.append(c); + } + + return s; + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + while (begin != end) + { + s.append(*begin); + ++begin; + } + + return s; + } + + inline + size_t + stringLength(const String& s) + { + return s.length(); + } + + inline + std::string + toUTF8String(const String& s) + { + std::string result; + s.toUTF8String(result); + + return result; + } + + inline + bool + empty(const String& s) + { + return s.isEmpty(); + } +} + +namespace std +{ + inline + cxxopts::UnicodeStringIterator + begin(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, 0); + } + + inline + cxxopts::UnicodeStringIterator + end(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, s.length()); + } +} + +//ifdef CXXOPTS_USE_UNICODE +#else + +namespace cxxopts +{ + using String = std::string; + + template + T + toLocalString(T&& t) + { + return std::forward(t); + } + + inline + size_t + stringLength(const String& s) + { + return s.length(); + } + + inline + String& + stringAppend(String&s, const String& a) + { + return s.append(a); + } + + inline + String& + stringAppend(String& s, size_t n, char c) + { + return s.append(n, c); + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + return s.append(begin, end); + } + + template + std::string + toUTF8String(T&& t) + { + return std::forward(t); + } + + inline + bool + empty(const std::string& s) + { + return s.empty(); + } +} // namespace cxxopts + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts +{ + namespace + { +#ifdef _WIN32 + const std::string LQUOTE("\'"); + const std::string RQUOTE("\'"); +#else + const std::string LQUOTE("‘"); + const std::string RQUOTE("’"); +#endif + } // namespace + +#if defined(__GNUC__) +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: +// warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" +#pragma GCC diagnostic ignored "-Weffc++" +// This will be ignored under other compilers like LLVM clang. +#endif + class Value : public std::enable_shared_from_this + { + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual std::shared_ptr + no_implicit_value() = 0; + + virtual bool + is_boolean() const = 0; + }; +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + class OptionException : public std::exception + { + public: + explicit OptionException(std::string message) + : m_message(std::move(message)) + { + } + + CXXOPTS_NODISCARD + const char* + what() const noexcept override + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; + + class OptionSpecException : public OptionException + { + public: + + explicit OptionSpecException(const std::string& message) + : OptionException(message) + { + } + }; + + class OptionParseException : public OptionException + { + public: + explicit OptionParseException(const std::string& message) + : OptionException(message) + { + } + }; + + class option_exists_error : public OptionSpecException + { + public: + explicit option_exists_error(const std::string& option) + : OptionSpecException("Option " + LQUOTE + option + RQUOTE + " already exists") + { + } + }; + + class invalid_option_format_error : public OptionSpecException + { + public: + explicit invalid_option_format_error(const std::string& format) + : OptionSpecException("Invalid option format " + LQUOTE + format + RQUOTE) + { + } + }; + + class option_syntax_exception : public OptionParseException { + public: + explicit option_syntax_exception(const std::string& text) + : OptionParseException("Argument " + LQUOTE + text + RQUOTE + + " starts with a - but has incorrect syntax") + { + } + }; + + class option_not_exists_exception : public OptionParseException + { + public: + explicit option_not_exists_exception(const std::string& option) + : OptionParseException("Option " + LQUOTE + option + RQUOTE + " does not exist") + { + } + }; + + class missing_argument_exception : public OptionParseException + { + public: + explicit missing_argument_exception(const std::string& option) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + " is missing an argument" + ) + { + } + }; + + class option_requires_argument_exception : public OptionParseException + { + public: + explicit option_requires_argument_exception(const std::string& option) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + " requires an argument" + ) + { + } + }; + + class option_not_has_argument_exception : public OptionParseException + { + public: + option_not_has_argument_exception + ( + const std::string& option, + const std::string& arg + ) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } + }; + + class option_not_present_exception : public OptionParseException + { + public: + explicit option_not_present_exception(const std::string& option) + : OptionParseException("Option " + LQUOTE + option + RQUOTE + " not present") + { + } + }; + + class option_has_no_value_exception : public OptionException + { + public: + explicit option_has_no_value_exception(const std::string& option) + : OptionException( + !option.empty() ? + ("Option " + LQUOTE + option + RQUOTE + " has no value") : + "Option has no value") + { + } + }; + + class argument_incorrect_type : public OptionParseException + { + public: + explicit argument_incorrect_type + ( + const std::string& arg + ) + : OptionParseException( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" + ) + { + } + }; + + class option_required_exception : public OptionParseException + { + public: + explicit option_required_exception(const std::string& option) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + " is required but not present" + ) + { + } + }; + + template + void throw_or_mimic(const std::string& text) + { + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); + +// #ifndef CXXOPTS_NO_EXCEPTIONS +// // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw +// throw T{text}; +// #else + // Otherwise manually instantiate the exception, print what() to stderr, + // and exit + T exception{text}; + std::cerr << exception.what() << std::endl; + std::exit(EXIT_FAILURE); +//#endif + } + + namespace values + { + namespace parser_tool + { + struct IntegerDesc + { + std::string negative = ""; + std::string base = ""; + std::string value = ""; + }; + struct ArguDesc { + std::string arg_name = ""; + bool grouping = false; + bool set_value = false; + std::string value = ""; + }; +#ifdef CXXOPTS_NO_REGEX + inline IntegerDesc SplitInteger(const std::string &text) + { + if (text.empty()) + { + throw_or_mimic(text); + } + IntegerDesc desc; + const char *pdata = text.c_str(); + if (*pdata == '-') + { + pdata += 1; + desc.negative = "-"; + } + if (strncmp(pdata, "0x", 2) == 0) + { + pdata += 2; + desc.base = "0x"; + } + if (*pdata != '\0') + { + desc.value = std::string(pdata); + } + else + { + throw_or_mimic(text); + } + return desc; + } + + inline bool IsTrueText(const std::string &text) + { + const char *pdata = text.c_str(); + if (*pdata == 't' || *pdata == 'T') + { + pdata += 1; + if (strncmp(pdata, "rue\0", 4) == 0) + { + return true; + } + } + else if (strncmp(pdata, "1\0", 2) == 0) + { + return true; + } + return false; + } + + inline bool IsFalseText(const std::string &text) + { + const char *pdata = text.c_str(); + if (*pdata == 'f' || *pdata == 'F') + { + pdata += 1; + if (strncmp(pdata, "alse\0", 5) == 0) + { + return true; + } + } + else if (strncmp(pdata, "0\0", 2) == 0) + { + return true; + } + return false; + } + + inline std::pair SplitSwitchDef(const std::string &text) + { + std::string short_sw, long_sw; + const char *pdata = text.c_str(); + if (isalnum(*pdata) && *(pdata + 1) == ',') { + short_sw = std::string(1, *pdata); + pdata += 2; + } + while (*pdata == ' ') { pdata += 1; } + if (isalnum(*pdata)) { + const char *store = pdata; + pdata += 1; + while (isalnum(*pdata) || *pdata == '-' || *pdata == '_') { + pdata += 1; + } + if (*pdata == '\0') { + long_sw = std::string(store, pdata - store); + } else { + throw_or_mimic(text); + } + } + return std::pair(short_sw, long_sw); + } + + inline ArguDesc ParseArgument(const char *arg, bool &matched) + { + ArguDesc argu_desc; + const char *pdata = arg; + matched = false; + if (strncmp(pdata, "--", 2) == 0) + { + pdata += 2; + if (isalnum(*pdata)) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + while (isalnum(*pdata) || *pdata == '-' || *pdata == '_') + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + if (argu_desc.arg_name.length() > 1) + { + if (*pdata == '=') + { + argu_desc.set_value = true; + pdata += 1; + if (*pdata != '\0') + { + argu_desc.value = std::string(pdata); + } + matched = true; + } + else if (*pdata == '\0') + { + matched = true; + } + } + } + } + else if (strncmp(pdata, "-", 1) == 0) + { + pdata += 1; + argu_desc.grouping = true; + while (isalnum(*pdata)) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + matched = !argu_desc.arg_name.empty() && *pdata == '\0'; + } + return argu_desc; + } + +#else // CXXOPTS_NO_REGEX + + namespace + { + + std::basic_regex integer_pattern + ("(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"); + std::basic_regex truthy_pattern + ("(t|T)(rue)?|1"); + std::basic_regex falsy_pattern + ("(f|F)(alse)?|0"); + + std::basic_regex option_matcher + ("--([[:alnum:]][-_[:alnum:]]+)(=(.*))?|-([[:alnum:]]+)"); + std::basic_regex option_specifier + ("(([[:alnum:]]),)?[ ]*([[:alnum:]][-_[:alnum:]]*)?"); + + } // namespace + + inline IntegerDesc SplitInteger(const std::string &text) + { + std::smatch match; + std::regex_match(text, match, integer_pattern); + + if (match.length() == 0) + { + throw_or_mimic(text); + } + + IntegerDesc desc; + desc.negative = match[1]; + desc.base = match[2]; + desc.value = match[3]; + + if (match.length(4) > 0) + { + desc.base = match[5]; + desc.value = "0"; + return desc; + } + + return desc; + } + + inline bool IsTrueText(const std::string &text) + { + std::smatch result; + std::regex_match(text, result, truthy_pattern); + return !result.empty(); + } + + inline bool IsFalseText(const std::string &text) + { + std::smatch result; + std::regex_match(text, result, falsy_pattern); + return !result.empty(); + } + + inline std::pair SplitSwitchDef(const std::string &text) + { + std::match_results result; + std::regex_match(text.c_str(), result, option_specifier); + if (result.empty()) + { + throw_or_mimic(text); + } + + const std::string& short_sw = result[2]; + const std::string& long_sw = result[3]; + + return std::pair(short_sw, long_sw); + } + + inline ArguDesc ParseArgument(const char *arg, bool &matched) + { + std::match_results result; + std::regex_match(arg, result, option_matcher); + matched = !result.empty(); + + ArguDesc argu_desc; + if (matched) { + argu_desc.arg_name = result[1].str(); + argu_desc.set_value = result[2].length() > 0; + argu_desc.value = result[3].str(); + if (result[4].length() > 0) + { + argu_desc.grouping = true; + argu_desc.arg_name = result[4].str(); + } + } + + return argu_desc; + } + +#endif // CXXOPTS_NO_REGEX +#undef CXXOPTS_NO_REGEX + } + + namespace detail + { + template + struct SignedCheck; + + template + struct SignedCheck + { + template + void + operator()(bool negative, U u, const std::string& text) + { + if (negative) + { + if (u > static_cast((std::numeric_limits::min)())) + { + throw_or_mimic(text); + } + } + else + { + if (u > static_cast((std::numeric_limits::max)())) + { + throw_or_mimic(text); + } + } + } + }; + + template + struct SignedCheck + { + template + void + operator()(bool, U, const std::string&) const {} + }; + + template + void + check_signed_range(bool negative, U value, const std::string& text) + { + SignedCheck::is_signed>()(negative, value, text); + } + } // namespace detail + + template + void + checked_negate(R& r, T&& t, const std::string&, std::true_type) + { + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + r = static_cast(-static_cast(t-1)-1); + } + + template + void + checked_negate(R&, T&&, const std::string& text, std::false_type) + { + throw_or_mimic(text); + } + + template + void + integer_parser(const std::string& text, T& value) + { + parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); + + using US = typename std::make_unsigned::type; + constexpr bool is_signed = std::numeric_limits::is_signed; + + const bool negative = int_desc.negative.length() > 0; + const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; + const std::string & value_match = int_desc.value; + + US result = 0; + + for (char ch : value_match) + { + US digit = 0; + + if (ch >= '0' && ch <= '9') + { + digit = static_cast(ch - '0'); + } + else if (base == 16 && ch >= 'a' && ch <= 'f') + { + digit = static_cast(ch - 'a' + 10); + } + else if (base == 16 && ch >= 'A' && ch <= 'F') + { + digit = static_cast(ch - 'A' + 10); + } + else + { + throw_or_mimic(text); + } + + const US next = static_cast(result * base + digit); + if (result > next) + { + throw_or_mimic(text); + } + + result = next; + } + + detail::check_signed_range(negative, result, text); + + if (negative) + { + checked_negate(value, result, text, std::integral_constant()); + } + else + { + value = static_cast(result); + } + } + + template + void stringstream_parser(const std::string& text, T& value) + { + std::stringstream in(text); + in >> value; + if (!in) { + throw_or_mimic(text); + } + } + + template ::value>::type* = nullptr + > + void parse_value(const std::string& text, T& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, bool& value) + { + if (parser_tool::IsTrueText(text)) + { + value = true; + return; + } + + if (parser_tool::IsFalseText(text)) + { + value = false; + return; + } + + throw_or_mimic(text); + } + + inline + void + parse_value(const std::string& text, std::string& value) + { + value = text; + } + + // The fallback parser. It uses the stringstream parser to parse all types + // that have not been overloaded explicitly. It has to be placed in the + // source code before all other more specialized templates. + template ::value>::type* = nullptr + > + void + parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); + } + + template + void + parse_value(const std::string& text, std::vector& value) + { + if (text.empty()) { + T v; + parse_value(text, v); + value.emplace_back(std::move(v)); + return; + } + std::stringstream in(text); + std::string token; + while(!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + T v; + parse_value(token, v); + value.emplace_back(std::move(v)); + } + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + void + parse_value(const std::string& text, std::optional& value) + { + T result; + parse_value(text, result); + value = std::move(result); + } +#endif + + inline + void parse_value(const std::string& text, char& c) + { + if (text.length() != 1) + { + throw_or_mimic(text); + } + + c = text[0]; + } + + template + struct type_is_container + { + static constexpr bool value = false; + }; + + template + struct type_is_container> + { + static constexpr bool value = true; + }; + + template + class abstract_value : public Value + { + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + explicit abstract_value(T* t) + : m_store(t) + { + } + + ~abstract_value() override = default; + + abstract_value& operator=(const abstract_value&) = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) + { + m_result = std::make_shared(); + m_store = m_result.get(); + } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + parse(const std::string& text) const override + { + parse_value(text, *m_store); + } + + bool + is_container() const override + { + return type_is_container::value; + } + + void + parse() const override + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const override + { + return m_default; + } + + bool + has_implicit() const override + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) override + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) override + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::shared_ptr + no_implicit_value() override + { + m_implicit = false; + return shared_from_this(); + } + + std::string + get_default_value() const override + { + return m_default_value; + } + + std::string + get_implicit_value() const override + { + return m_implicit_value; + } + + bool + is_boolean() const override + { + return std::is_same::value; + } + + const T& + get() const + { + if (m_store == nullptr) + { + return *m_result; + } + return *m_store; + } + + protected: + std::shared_ptr m_result{}; + T* m_store{}; + + bool m_default = false; + bool m_implicit = false; + + std::string m_default_value{}; + std::string m_implicit_value{}; + }; + + template + class standard_value : public abstract_value + { + public: + using abstract_value::abstract_value; + + CXXOPTS_NODISCARD + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } + }; + + template <> + class standard_value : public abstract_value + { + public: + ~standard_value() override = default; + + standard_value() + { + set_default_and_implicit(); + } + + explicit standard_value(bool* b) + : abstract_value(b) + { + set_default_and_implicit(); + } + + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } + + private: + + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } + }; + } // namespace values + + template + std::shared_ptr + value() + { + return std::make_shared>(); + } + + template + std::shared_ptr + value(T& t) + { + return std::make_shared>(&t); + } + + class OptionAdder; + + class OptionDetails + { + public: + OptionDetails + ( + std::string short_, + std::string long_, + String desc, + std::shared_ptr val + ) + : m_short(std::move(short_)) + , m_long(std::move(long_)) + , m_desc(std::move(desc)) + , m_value(std::move(val)) + , m_count(0) + { + m_hash = std::hash{}(m_long + m_short); + } + + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_value(rhs.m_value->clone()) + , m_count(rhs.m_count) + { + } + + OptionDetails(OptionDetails&& rhs) = default; + + CXXOPTS_NODISCARD + const String& + description() const + { + return m_desc; + } + + CXXOPTS_NODISCARD + const Value& + value() const { + return *m_value; + } + + CXXOPTS_NODISCARD + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } + + CXXOPTS_NODISCARD + const std::string& + short_name() const + { + return m_short; + } + + CXXOPTS_NODISCARD + const std::string& + long_name() const + { + return m_long; + } + + CXXOPTS_NODISCARD + const std::string& + essential_name() const + { + return m_long.empty() ? m_short : m_long; + } + + size_t + hash() const + { + return m_hash; + } + + private: + std::string m_short{}; + std::string m_long{}; + String m_desc{}; + std::shared_ptr m_value{}; + int m_count; + + size_t m_hash{}; + }; + + struct HelpOptionDetails + { + std::string s; + std::string l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; + }; + + struct HelpGroupDetails + { + std::string name{}; + std::string description{}; + std::vector options{}; + }; + + class OptionValue + { + public: + void + parse + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + m_long_name = &details->long_name(); + } + + void + parse_default(const std::shared_ptr& details) + { + ensure_value(details); + m_default = true; + m_long_name = &details->long_name(); + m_value->parse(); + } + + void + parse_no_value(const std::shared_ptr& details) + { + m_long_name = &details->long_name(); + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnull-dereference" +#endif + + CXXOPTS_NODISCARD + size_t + count() const noexcept + { + return m_count; + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +#pragma GCC diagnostic pop +#endif + + // TODO: maybe default options should count towards the number of arguments + CXXOPTS_NODISCARD + bool + has_default() const noexcept + { + return m_default; + } + + template + const T& + as() const + { + if (m_value == nullptr) { + throw_or_mimic( + m_long_name == nullptr ? "" : *m_long_name); + } + +#ifdef CXXOPTS_NO_RTTI + return static_cast&>(*m_value).get(); +#else + return dynamic_cast&>(*m_value).get(); +#endif + } + + private: + void + ensure_value(const std::shared_ptr& details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } + + + const std::string* m_long_name = nullptr; + // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, + // where the key has the string we point to. + std::shared_ptr m_value{}; + size_t m_count = 0; + bool m_default = false; + }; + + class KeyValue + { + public: + KeyValue(std::string key_, std::string value_) + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } + + CXXOPTS_NODISCARD + const std::string& + key() const + { + return m_key; + } + + CXXOPTS_NODISCARD + const std::string& + value() const + { + return m_value; + } + + template + T + as() const + { + T result; + values::parse_value(m_value, result); + return result; + } + + private: + std::string m_key; + std::string m_value; + }; + + using ParsedHashMap = std::unordered_map; + using NameHashMap = std::unordered_map; + + class ParseResult + { + public: + class Iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = KeyValue; + using difference_type = void; + using pointer = const KeyValue*; + using reference = const KeyValue&; + + Iterator() = default; + Iterator(const Iterator&) = default; + + Iterator(const ParseResult *pr, bool end=false) + : m_pr(pr) + , m_iter(end? pr->m_defaults.end(): pr->m_sequential.begin()) + { + } + + Iterator& operator++() + { + ++m_iter; + if(m_iter == m_pr->m_sequential.end()) + { + m_iter = m_pr->m_defaults.begin(); + return *this; + } + return *this; + } + + Iterator operator++(int) + { + Iterator retval = *this; + ++(*this); + return retval; + } + + bool operator==(const Iterator& other) const + { + return m_iter == other.m_iter; + } + + bool operator!=(const Iterator& other) const + { + return !(*this == other); + } + + const KeyValue& operator*() + { + return *m_iter; + } + + const KeyValue* operator->() + { + return m_iter.operator->(); + } + + private: + const ParseResult* m_pr; + std::vector::const_iterator m_iter; + }; + + ParseResult() = default; + ParseResult(const ParseResult&) = default; + + ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector sequential, + std::vector default_opts, std::vector&& unmatched_args) + : m_keys(std::move(keys)) + , m_values(std::move(values)) + , m_sequential(std::move(sequential)) + , m_defaults(std::move(default_opts)) + , m_unmatched(std::move(unmatched_args)) + { + } + + ParseResult& operator=(ParseResult&&) = default; + ParseResult& operator=(const ParseResult&) = default; + + Iterator + begin() const + { + return Iterator(this); + } + + Iterator + end() const + { + return Iterator(this, true); + } + + size_t + count(const std::string& o) const + { + auto iter = m_keys.find(o); + if (iter == m_keys.end()) + { + return 0; + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + return 0; + } + + return viter->second.count(); + } + + const OptionValue& + operator[](const std::string& option) const + { + auto iter = m_keys.find(option); + + if (iter == m_keys.end()) + { + throw_or_mimic(option); + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + throw_or_mimic(option); + } + + return viter->second; + } + + const std::vector& + arguments() const + { + return m_sequential; + } + + const std::vector& + unmatched() const + { + return m_unmatched; + } + + const std::vector& + defaults() const + { + return m_defaults; + } + + const std::string + arguments_string() const + { + std::string result; + for(const auto& kv: m_sequential) + { + result += kv.key() + " = " + kv.value() + "\r\n"; + } + for(const auto& kv: m_defaults) + { + result += kv.key() + " = " + kv.value() + " " + "(default)" + "\r\n"; + } + return result; + } + + private: + NameHashMap m_keys{}; + ParsedHashMap m_values{}; + std::vector m_sequential{}; + std::vector m_defaults{}; + std::vector m_unmatched{}; + }; + + struct Option + { + Option + ( + std::string opts, + std::string desc, + std::shared_ptr value = ::cxxopts::value(), + std::string arg_help = "" + ) + : opts_(std::move(opts)) + , desc_(std::move(desc)) + , value_(std::move(value)) + , arg_help_(std::move(arg_help)) + { + } + + std::string opts_; + std::string desc_; + std::shared_ptr value_; + std::string arg_help_; + }; + + using OptionMap = std::unordered_map>; + using PositionalList = std::vector; + using PositionalListIterator = PositionalList::const_iterator; + + class OptionParser + { + public: + OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) + : m_options(options) + , m_positional(positional) + , m_allow_unrecognised(allow_unrecognised) + { + } + + ParseResult + parse(int argc, const char* const* argv); + + bool + consume_positional(const std::string& a, PositionalListIterator& next); + + void + checked_parse_arg + ( + int argc, + const char* const* argv, + int& current, + const std::shared_ptr& value, + const std::string& name + ); + + void + add_to_option(OptionMap::const_iterator iter, const std::string& option, const std::string& arg); + + void + parse_option + ( + const std::shared_ptr& value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(const std::shared_ptr& details); + + void + parse_no_value(const std::shared_ptr& details); + + private: + + void finalise_aliases(); + + const OptionMap& m_options; + const PositionalList& m_positional; + + std::vector m_sequential{}; + std::vector m_defaults{}; + bool m_allow_unrecognised; + + ParsedHashMap m_parsed{}; + NameHashMap m_keys{}; + }; + + class Options + { + public: + + explicit Options(std::string program, std::string help_string = "") + : m_program(std::move(program)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_allow_unrecognised(false) + , m_width(76) + , m_tab_expansion(false) + , m_options(std::make_shared()) + { + } + + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } + + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } + + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } + + Options& + allow_unrecognised_options() + { + m_allow_unrecognised = true; + return *this; + } + + Options& + set_width(size_t width) + { + m_width = width; + return *this; + } + + Options& + set_tab_expansion(bool expansion=true) + { + m_tab_expansion = expansion; + return *this; + } + + ParseResult + parse(int argc, const char* const* argv); + + OptionAdder + add_options(std::string group = ""); + + void + add_options + ( + const std::string& group, + std::initializer_list