/* * HBG3 (HomeBrew Gen3) Universal Accessory for the Celestron AUX bus. * * Copyright(c) 2020-2024 by Mark Lord . * This code is free for personal use/modification/whatever. * * This is a versatile add-on for the Celestron AUX bus. * Basic functionality includes WiFi, Bluetooth, and USB connectivity. * Plus Nunchuck, GPS, Smart DewControl, Focus Motor, wireless Relay, emulators, standalone mode.. * ..and so much more. See https://rtr.ca/hbg3/ */ #define VERSION "v8.51" /* MUST begin with 'v' */ #define VERSION_DATE "2024-07-13" /* * Arduino IDE configuration for this project: * -- Install pre-configured Arduino IDE with all needed libraries from https://rtr.ca/hbg3/ * -- Or do it the hard way: * -- Install Arduino Legacy IDE v1.8.xx from https://www.arduino.cc/en/software (scroll down!) * -- Install ESP32 support into Arduino IDE: https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html * -- First, select Board="ESP32 Dev Module". * -- Now choose Partition Scheme="Minimal SPIFFS.." * -- and also CPU Frequency = 160Mhz (WiFi/BT). * -- Use Tools-->Manage-Libraries to install EspSoftwareSerial library. * -- For EMULATE_FOCUS=true, use Tools-->Manage-Libraries to install AccelStepper library. * -- For EMULATE_FOCUS=true, use Tools-->Manage-Libraries to install FastAccelStepper library. * -- For OLED_ENABLED=true, install SSD1306AsciiWire library from https://github.com/greiman/SSD1306Ascii/ * * Tools-->Board-->Boards_Manager: ensure the esp32 support is 2.1.x or higher, but not 3.x.x. * * For Mount-USB and Arduino IDE: * >>> MS-Windows Driver installation is required for the onboard CP210x USB-Serial chip: * Go here: https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers?tab=downloads * Download/save the CP210x Universal Windows Driver .zip file. * Extract the .zip file. * Right click on the extracted silabser.inf file, and click INSTALL. * * Recent ChangeLog is below. Complete Version History is at https://rtr.ca/hbg3/history.html * * Version 8.51 * -- Minor fixes around DEW_RECALIBRATE_ENV_SENSOR. * -- Make suppress.lowbattery also hide such messages from verbose/trace. * -- Don't do Network Time Protocol (NTP) unless a real/fake GPS is configured. * -- Enable tracing 1 or 2 devices at once with the trace command. Eg. trace dev1 dev2 * -- Measure Dew Heater voltage on ADS1115 channel-3: use external 5:1 voltage divider on the input. * -- Read thermistor every 3 seconds instead of every 4 seconds. * -- Show p3000 responses when verbose=1. * Version 8.50 2024-06-18 * -- Add support "get wlan.mac" for CPWI 2.5.6-beta. * Version 8.49 2024-06-17 * -- Show GPS coordinates as DDD° MM'SS.ss" C, similar to Celestron hand-controller displays. * -- Fix build when EMULATE_SSAA is false. * -- Set default ETHERNET_ENABLED back to true, for benefit of hbg3-aio builds. * Version 8.48 2024-05-31 * -- Some mounts (eg. CPC) return an extra byte in response to certain cordwrap commands. Deal with it! * Version 8.47 2024-05-29 * -- Work around CPWI CORDWRAP bug: CPWI uses DEV_ALT instead of DEV_AZM for some (but not all) CORDWRAP commands. * -- Revert to 4-sec p2000_timeout for most stuff; use a longer timeout during SSAA capture activity. * Version 8.46 2024-05-11 * -- Use P2000_ACTIVITY_TIMEOUT for MUSB as well as WiFi/BT connections. * -- Increase P2000_ACTIVITY_TIMEOUT to 8 seconds (was 4). SSAA has been observed taking 5-seconds to respond. * -- Dump SSSWI challenge/response pairs together when verbose=1 * -- ADS1115: Use I2C clock of 800KHz for lower overhead. * -- ADS1115: use 475 SPS, so that a sample can be read in 2.1msec. * -- Increase default oled.timeout.secs to 120 (was 60). * Version 8.45 2024-05-02 * -- Add ADS1115 ADC support for thermistors. Wire ADC A0 to 3.3V, A1 to Thm0, A2 to Thm1. ADC A3 is not-used. * -- Incorporate a default ADC_LUT[] table. * Version 8.44 2024-05-02 * -- Add SSSWI_HACKING: intercept Challenge/Response and feed our own data to SSHC. * Version 8.43 2024-04-03 * -- New boolean var to reverse ALT direction of Nunchuck: nchuck.reverse.alt * Version 8.42 2024-04-02 * -- Never turn off GPS; instead let it run and update it's almanac etc.. * * Random note: * For Camera shutter release triggering via RTS: * The RTS pin of the CP2102 USB-Serial chip (on the ESP32 module) is wired through * a resistor and transistor, eventually connecting to * GPIO0 on the WROOM-32 module. */ #define WIFIRELAY_ENABLED true // When false, code for wifi_relay_mode is omitted. #define BLOCK_CFM_CONNECTIONS true // Set to false to allow CFM to attempt to interogate/update firmware (VERY RISKY!!). DO NOT DO THIS!! #define NCHUCK_ENABLED true // Enables use of a Nunchuck game controller on I2C for slew/focus. #define OLED_ENABLED true // When true, OLED support is included. Requires library: https://github.com/greiman/SSD1306Ascii/ #define OTA_ENABLED true // When true, support is included for Over-The-Air (OTA) Firmware Updates #define NTP_ENABLED true // When true, use Network Time Protocol until GPS Date/Time are available. #define EMULATE_GPS true // When true and GPS is attached, pretend to be a Celestron SkySync GPS receiver. #define EMULATE_SSAA true // When true, include code for StarSense AutoAlign simulator. Can be turned on/off via NVRAM variable. #define EMULATE_SSAG true // When true, include code for StarSense AutoGuider simulator. Can be turned on/off via NVRAM variable. #define EMULATE_DEW true // When true and SHT3x is attached, pretend to be a Celestron 2X Smart Dew Heater Controller. #define EMULATE_FOCUS true // When true and stepper motor is wired, pretend to be a Celestron Focus Motor. PIN conflict with Ethernet. #define ETHERNET_ENABLED true // When true and eth.h is present, include support for Ethernet. PIN conflict with Stepper/Focus. #define ORIGINAL_WIFI_PROJECT false // When true, build firmware for the original WIFI+BT+GPS project, instead of for the HBG3. #define COOLICK_PCB_PROJECT false // When true, build firmware for the Coolick PCB version of the original WIFI+BT+GPS project. #define SSSWI_HACKING false #define NVRAM_VALS_MAX_LEN 31 #if OTA_ENABLED static char ota_update_server[NVRAM_VALS_MAX_LEN + 1] = "rtr.ca"; static char ota_update_path [NVRAM_VALS_MAX_LEN + 1] = "/hbg3/firmware.bin"; static char ota_version_path [NVRAM_VALS_MAX_LEN + 1] = "/cgi-bin/hbg3_version"; static bool ota_partition_size_okay = false; static bool ota_ignore_version = false; #endif /* OTA_ENABLED */ // PIN definitions for this project: #define AUXBUS_RX_PIN 16 // (RX2) from AUX bus RX pin #define AUXBUS_TX_PIN 17 // (TX2) to AUX bus TX pin #define AUXBUS_BUSYIN_PIN 35 // was 19; from AUX bus BUSY pin (could be same pin as BUSYOUT) #define AUXBUS_BUSYOUT_PIN 32 // was 19; to AUX bus BUSY pin (could be same pin as BUSYIN) #define GPS_TX_PIN 33 // was 22; TX-to-GPS pin. GPS RX Pin (green) connects here. #define GPS_RX_PIN 25 // was 23; RX-from-GPS pin. GPS TX pin (white) connects here. #define ESP32_WIFI_MODE_PIN 13 // was 5; Ground for "Access Point" mode; float high for "Direct Connect" #define MUSB_SELECT_PIN 15 // Ground to use USB port as Mount-USB #define READ_WIFI_MODE_PIN() (digitalRead(ESP32_WIFI_MODE_PIN)) #define READ_MUSB_SELECT_PIN() (digitalRead(MUSB_SELECT_PIN)) #define OLED_BUTTON_PIN 4 // For push-button switch to cycle between OLED data screens and OFF. #define BLUE_LED_PIN 2 // ESP32 DevKitV1 built-in (blue) LED #define USB_RX_PIN 3 // Internal pad; no associated pin #define USB_TX_PIN 1 // Internal pad; no associated pin #define LED_ON HIGH // Some boards want LOW here; 30-pin DevKit-v1 wants HIGH. #define LED_OFF (((LED_ON) == HIGH) ? LOW : HIGH) // Do NOT edit tihs line #define AUXRELAY_RX_PIN 34 // from AUX relay bus RX pin #define AUXRELAY_TX_PIN 14 // to AUX relay bus TX pin #define AUXRELAY_BUSYIN_PIN 26 // from AUX relay bus BUSY pin (could be same pin as BUSYOUT on older hardware) #define AUXRELAY_BUSYOUT_PIN 27 // to AUX relay bus BUSY pin (could be same pin as BUSYIN on older hardware) #if (ORIGINAL_WIFI_PROJECT || COOLICK_PCB_PROJECT) // PIN assignments for earlier versions of the WiFi+BT+GPS project #undef AUXRELAY_RX_PIN #define AUXRELAY_RX_PIN -1 #undef ESP32_WIFI_MODE_PIN #define ESP32_WIFI_MODE_PIN 5 #undef GPS_TX_PIN #undef GPS_RX_PIN #if COOLICK_PCB_PROJECT #define GPS_TX_PIN 23 #define GPS_RX_PIN 19 #else // ORIGINAL_WIFI_PROJECT #define GPS_TX_PIN 22 #define GPS_RX_PIN 23 #endif #undef EMULATE_DEW #define EMULATE_DEW false #undef EMULATE_FOCUS #define EMULATE_FOCUS false #undef OTA_ENABLED #define OTA_ENABLED false #define HBG3_NAME "HBG1" #endif // The first include of this file defines PINs. #if __has_include("aio.h") #include "aio.h" #endif #include #include #include #include #include #include #include #include #include "AsyncUDP.h" #include "FS.h" #include "SPIFFS.h" #include #include "BluetoothSerial.h" #define P2000_NORMAL_TIMEOUT ( 4 * 1000) // Close port 2000 connection when idle this long. #define P2000_SSAA_CAPTURE_TIMEOUT (20 * 1000) // Longer timeout needed during SSAA capture operations. static long p2000_timeout = P2000_NORMAL_TIMEOUT; // Gets increased to 15-secs if SSAA is used. static bool simple_mode = false; // In "simple_mode", no auxtest, no WiFi, no BT. typedef unsigned long long u64; typedef unsigned long ulong; typedef unsigned int uint; BluetoothSerial bt; enum {BT_IDLE, BT_BOOTLOADER, BT_WAITBAUD, BT_SETBAUD, BT_HSDONE}; static byte bt_auxstate = BT_IDLE; // state machine for CPWI initial handshake. static bool bt_debug = false; // For debugging CPWI initial handshake. static bool bt_connected = 0; // BT Session established. static bool bt_ready = false; // BT Session is ready for AUX protocol packets static long bt_handshake = 0; // timer for parts of CPWI initial handshake. static long bt_active = 0; // BT Session has recent activity: an app is connected. static long esp32_reset_pending = 0; // Timer for resets of the HBG3 /* * There is no serial "debug" port if (musb_selected && auxrelay_detected). * Too complex to attempt debug over SoftwareSerial for now. * But one can instead use port 3000 and the "debug" command there instead. */ HardwareSerial *hwserial0 = &Serial; HardwareSerial hwserial1(1); // MUSB, auxrelay, GPS, or debug (in that order). HardwareSerial auxbus_uart(2); // auxbus #define auxrelay_uart hwserial1 HardwareSerial *SerialDebug = NULL; // Initialized in setup(): one of hwseria0, hwserial1, or NULL. HardwareSerial *musb_uart; // Initialized in setup(): one of hwseria0, hwserial1, or NULL. typedef unsigned long long u64; typedef unsigned long ulong; typedef unsigned int uint; static char hbg3_version[] = VERSION; static bool fm_detected = false; // Focus Motor detected, either on the HBG3 or on a bus somewhere. static bool mount_reversed_alt = false; // Use "set mount.reversed.alt Yes" for when OTA is mounted backwards on an ALT/AZM mount. static bool auxbus_passive_mode = false; static byte auxtest_remaining = 3; static bool verbose = false; static bool aux_only_tracing = true; // By default, don't show packets coming/in out on w2000, bt, or musb. static bool aux_ascii_passthru = false; static bool auxbus_raw_tracing = false; static byte auxbus_raw_count = 0; static long auxbus_raw_timeout = 0; static byte auxbus_stopbits = 2; // Default was always 8N2, but 8N1 should work fine and is 10% faster. static bool nchuck_debug = false; // For extra verbose output to help diagnose Nunchuck issues: enable with 'n' on Serial port. static bool gps_detected = false; // Don't respond as DEV_GPS unless a GPS is detected static bool gps_disabled = false; // GPS can be prevented from responding via NVRAM setting static bool oled_detected = false; static byte cordwrap_override = 0; static bool auxrelay_ssforward = false; // When true, AUXRELAY will forward StarSense Camera packets across the relay. static bool suppress_lowbattery = false; // For low-battery warnings spewed from the Evolution mount. static bool p3000_debug = false; // When true, serial debug commands/output will be redirected to the port 3000 connection. #define NVRAM_SAVE_DELAY ((long)1000) static long nvram_delayed_save = 0; static long nvram_save_delay = NVRAM_SAVE_DELAY; #if ETHERNET_ENABLED && !defined(AIO_H) #if ! __has_include("eth.h") #undef ETHERNET_ENABLED #define ETHERNET_ENABLED false #endif #endif /* ETHERNET_ENABLED && !defined(AIO_H) */ #if ETHERNET_ENABLED /* do NOT combine this with the #if above!! */ #ifndef AIO_H #define ETHERNET_SS_PIN 5 // AIO has a different SS_PIN for Ethernet. static inline bool read_ethernet_client_mode_switch (void) { return (READ_WIFI_MODE_PIN() == LOW); } #endif #include "eth.h" static bool p3000_debug_ethernet = false; // Selects between ethernet and wifi for p3000_debug destination. #else #define ETH_TYPE "NONE" #define p3000_debug_ethernet false #define e3000 w3000 // Keep compiler happy #endif /* ETHERNET_ENABLED */ #define BAD_FLOAT_VAL 999999.0 static void nvram_save_float (const char *name, double val); static double nvram_get_float (const char *name); static const char *nvram_get_val (const char *name); static int nvram_get_int (const char *name); static void nvram_save_int (const char *name, int val); static void nvram_erase_val (const char *name); #if EMULATE_SSAA static bool emulate_ssaa = false; static byte *ssaa_image_pkt = NULL; // allocated on each use #define EMULATING_SSAA emulate_ssaa #else #define ssaa_image_pkt NULL #define EMULATING_SSAA false #endif #if EMULATE_SSAG static bool emulate_ssag = false; #define EMULATING_SSAG emulate_ssag #else #define EMULATING_SSAG false #endif #ifndef HBG3_NAME #define HBG3_NAME "HBG3" #endif static char hbg3_ssid[NVRAM_VALS_MAX_LEN + 16] = {0,}; #define DEFAULT_SSID "HomeBrew-" #define ADAPTER_VERSION DEFAULT_SSID "AMW007-9.0.0.0, " VERSION_DATE "T12:00:00Z, " HBG3_NAME "-" // Celestron reports: "ZENTRI-AMW007-1.2.0.10, 2017-07-28T07:43:27Z, ZentriOS-WL-1.2.0.10, Board:N/A"); #define AUXBUS_TXQ_SIZE 24 // Was 32, hit 15/16 during SSAG auto-guiding. #define AUXBUS_PKT_MAX 16 // Max length of a regular 0x3b style auxbus packet. #define AUXBUS_SSAA_PKT_MAX 848 // Theoretically up to 2470 bytes. In practice, nothing is ever larger than about 832 bytes. static long auxtest_timer = 0; static WiFiClient w2000; // The wifi application port 2000, for controlling the mount static WiFiClient w3000; // The wifi management port 3000, for configuring the adapter static byte wifi_mac_addr[6]; // Filled in by setup(), used to create unique SSIDs static char current_ssid[32]; static char current_ipaddr[32]; #define SERVER_IPADDRESS 1,2,3,4 // For both WiFi and Ethernet static const IPAddress server_static_ip(SERVER_IPADDRESS); static const IPAddress server_static_netmask(255,255,255,0); static bool bt_enabled = false; static enum {bt_protocol_aux, bt_protocol_hc} bt_protocol = bt_protocol_aux; // "bt_mode" conflicts with v3.0.x libraries static inline void SERIAL_WRITE (const char *s) { if (SerialDebug) SerialDebug->write(s); } static inline int SERIAL_AVAILABLE (void) { return SerialDebug ? SerialDebug->available() : 0; } static inline char SERIAL_READ (void) { return SerialDebug ? SerialDebug->read() : 0; } static inline void SERIAL_FLUSH (bool txonly) { if (SerialDebug) SerialDebug->flush(txonly); } // Debug messages use these macros, normally sending to debug uart, but possibly instead to the TCP/IP Port-3000 connection: #define E3000_ENDLINE() do { e3000.println(); e3000.flush(); } while (0) #define W3000_ENDLINE() do { w3000.println(); w3000.flush(); } while (0) #define SERIAL_ENDLINE() do { \ if (p3000_debug) {if (p3000_debug_ethernet) E3000_ENDLINE(); else W3000_ENDLINE();} else \ if (SerialDebug) {SerialDebug->println(); SERIAL_FLUSH(true);} \ } while (0) #define SERIAL_PRINTLN(x) do { \ if (p3000_debug) {if (p3000_debug_ethernet) e3000.println(x); else w3000.println(x);} else \ if (SerialDebug) {SerialDebug->println(x); SERIAL_FLUSH(true);} \ } while (0) #define SERIAL_PRINTF(...) do { \ if (p3000_debug) {if (p3000_debug_ethernet) e3000.printf(__VA_ARGS__); else w3000.printf(__VA_ARGS__);} else \ if (SerialDebug) {SerialDebug->printf(__VA_ARGS__); SERIAL_FLUSH(true);} \ } while (0) #define SERIAL_PRINT(x) do { \ if (p3000_debug) {if (p3000_debug_ethernet) e3000.write(x); else w3000.write(x);} else \ if (SerialDebug) {SerialDebug->print(x);} \ } while (0) #if ESP_IDF_VERSION_MAJOR >= 5 #undef Serial #define pwm_enable(channel) ledcAttach(dew_pwm_pins[channel],PWM_FREQUENCY,PWM_RESOLUTION) #define pwm_set_duty_cycle(channel,duty) ledcWrite(dew_pwm_pins[channel],duty) #else #define pwm_enable(channel) do {ledcSetup(channel,PWM_FREQUENCY,PWM_RESOLUTION); ledcAttachPin(dew_pwm_pins[channel],channel);} while (0) #define pwm_set_duty_cycle(channel,duty) ledcWrite(channel,duty) #endif #define Serial Serial_UNDEFINED // Prevent using "Serial" by accident static long blue_led_timer = 0; static inline void set_blue_led (int on_off) { blue_led_timer = 0; digitalWrite(BLUE_LED_PIN, on_off); #ifdef AIO_H aio_set_leds(on_off); #endif /* AIO_H */ } static void set_wifi_info (const char *ssid, IPAddress ip) { strcpy(current_ssid, ssid); sprintf(current_ipaddr, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); } // Known devices on AUX bus (6 chars or less for nice formatting): #define DEV_UART 0x00 #define DEV_MOUNT 0x01 #define DEV_HC4 0x04 #define DEV_HC5 0x0d #define DEV_SSHC 0x0e #define DEV_AZM 0x10 // Aka. DEV_RA #define DEV_ALT 0x11 // Aka. DEV_DEC #define DEV_FOCUS 0x12 #define DEV_DEW 0x17 #define DEV_SW 0x20 #define DEV_SCANR 0x2f // Python3 AUX Bus Scanner version 0.9.35+ #define DEV_CFM 0x21 #define DEV_FUTIL 0x22 // Celestron Focuser Utility, or older versions of the Python3 AUX Bus Scanner software. #define DEV_RASW 0x30 #define DEV_DECSW 0x31 #define DEV_DECAGP 0x32 #define DEV_GPS 0xb0 #define DEV_RTC 0xb2 #define DEV_SPWIFI 0xb3 #define DEV_SSAA 0xb4 #define DEV_EVWIFI 0xb5 #define DEV_BATT 0xb6 #define DEV_PWRCHG 0xb7 #define DEV_SSSWI 0xb8 #define DEV_SSAG 0xb9 #define DEV_DEWBB 0xbb #define DEV_LEDS 0xbf // 0x3b // No device should use this ID; fix packet_decoder() if something ever does though. // 0x3c // No device should use this ID; fix packet_decoder() if something ever does though. #define DEV_SSSWI2 0xc8 #define DEV_HBG3 0xe2 #define DEV_DUMMY 0xe3 #define DEV_RELAY 0xe4 #define DEV_FF 0xff #define EVOLUTION_MODEL 0x1687 static uint16_t mount_model = 0x0000; static byte DEV_ESP32; // Either DEV_HBG3 or DEV_RELAY, from setup() #define AUXDEV(name,desc) {DEV_##name, #name, desc} static const struct devnames_s { byte dev; const char *name; const char *desc; } devnames[] = { AUXDEV(UART, "Mount UART"), // CFM talks to this to set baud rate of 115200 or 19200 AUXDEV(MOUNT, "Mount Mainboard"), AUXDEV(HC4, "Nexstar* v4 Hand-Controller"), AUXDEV(HC5, "Nexstar+ v5 Hand-Controller"), AUXDEV(SSHC, "StarSense Hand-Controller"), AUXDEV(AZM, "Azimuth Motor"), AUXDEV(ALT, "Altitude Motor"), AUXDEV(FOCUS, "Focus Motor"), AUXDEV(DEW, "Smart DewController"), AUXDEV(SW, "CPWI, SkyPortal, SkySafari etc."), AUXDEV(CFM, "Celestron Firmware Manager"), AUXDEV(FUTIL, "Focuser Utility"), AUXDEV(SCANR, "AUX Bus Scanner"), AUXDEV(RASW, "CGX RA Switch"), AUXDEV(DECSW, "CGX DEC Switch"), AUXDEV(DECAGP, "CGX DEC AutoGuide Pt"), AUXDEV(GPS, "GPS Receiver"), AUXDEV(RTC, "Real-Time Clock"), AUXDEV(SPWIFI, "SkyPortal WiFi Adapter"), AUXDEV(SSAA, "StarSense AutoAlign Camera"), AUXDEV(EVWIFI, "Evolution built-in WiFi"), AUXDEV(BATT, "Evolution Battery Controller"), AUXDEV(PWRCHG, "Evolution Power/Charge Port"), AUXDEV(SSSWI, "StarSense for SkyWatcher Interface Box"), AUXDEV(SSAG, "StarSense AutoGuider"), AUXDEV(DEWBB, "Dew Controller Detection"), AUXDEV(LEDS, "Evolution Mount/Tray Lighting"), AUXDEV(SSSWI2, "SSSWI2"), AUXDEV(HBG3, HBG3_NAME), AUXDEV(DUMMY, "Dummy device"), AUXDEV(RELAY, HBG3_NAME "-Relay"), AUXDEV(FF, "Nothing"), }; #define N_DEVNAMES (sizeof(devnames) / sizeof(struct devnames_s)) static const char *dev_to_name (byte dev, bool full_name) { static const struct devnames_s *prev = devnames; if (dev == prev->dev) goto done; for (byte i = 0; i < N_DEVNAMES; ++i) { prev = &devnames[i]; if (dev == prev->dev) goto done; } return "???"; done: return full_name ? prev->desc : prev->name; } #define AUX_DEV_OP(name) {name, #name} struct auxdev_op_s { byte op; const char *name; }; #define OP(op,len) ((uint)((((uint)op) << 8) | ((uint)len))) // Motor Controller ops: #define MC_GET_POS 0x01 #define MC_GOTO_FAST 0x02 #define MC_SET_POS 0x04 #define MC_SET_POS_GUIDERATE 0x06 #define MC_SET_NEG_GUIDERATE 0x07 #define MC_REMOTE_SWITCH_ALIVE 0x08 #define MC_SWITCH_STATE_CHANGE 0x09 #define MC_AG_STATE_CHANGE 0x0a #define MC_LEVEL_START 0x0b #define MC_PEC_RECORD_START 0x0c #define MC_PEC_PEC_PLAYBACK 0x0d #define MC_GET_PEC 0x0e #define MC_SET_POS_BACKLASH 0x10 #define MC_SET_NEG_BACKLASH 0x11 #define MC_LEVEL_DONE 0x12 #define MC_GOTO_DONE 0x13 // returns 00 if still slewing, ff when done. #define MC_PEC_STATE_CHANGE 0x14 #define MC_PECTRAIN_DONE 0x15 #define MC_CANCEL_PECTRAIN 0x16 #define MC_GOTO_SLOW 0x17 #define MC_IS_INDEX_FOUND 0x18 #define MC_FIND_INDEX 0x19 #define MC_SET_USER_LIMIT_MIN 0x1a #define MC_SET_USER_LIMIT_MAX 0x1b #define MC_GET_USER_LIMIT_MIN 0x1c #define MC_GET_USER_LIMIT_MAX 0x1d #define MC_IS_USER_LIMIT_ENA 0x1e #define MC_SET_USER_LIMIT_ENA 0x1f #define MC_SET_CUSTOM_RATE9 0x20 #define MC_GET_CUSTOM_RATE9 0x21 #define MC_SET_CUSTOM_RATE9_ENA 0x22 #define MC_GET_CUSTOM_RATE9_ENA 0x23 #define MC_MOVE_POS 0x24 #define MC_MOVE_NEG 0x25 #define MC_AUX_GUIDE 0x26 #define MC_IS_AUX_GUIDE_ACTIVE 0x27 #define MC_CALIBRATION_ENABLE 0x2a #define MC_IS_CALIBRATED 0x2b #define MC_GET_LIMITS 0x2c #define MC_EEPROM_READ 0x30 #define MC_EEPROM_WRITE 0x31 #define MC_PROGRAM_READ 0x32 #define MC_ENABLE_CORDWRAP 0x38 #define MC_DISABLE_CORDWRAP 0x39 #define MC_SET_CORDWRAP_POS 0x3a #define MC_POLL_CORDWRAP 0x3b // Ugh. Dumb. Should never have an opcode matching the 0x3b sync byte!! #define MC_GET_CORDWRAP_POS 0x3c #define MC_SET_SHUTTER 0x3d #define MC_GET_POS_BACKLASH 0x40 #define MC_GET_NEG_BACKLASH 0x41 #define MC_SET_AUTOGUIDE_RATE 0x46 #define MC_GET_AUTOGUIDE_RATE 0x47 #define MC_SET_SWITCH_CALIBRATION 0x48 #define MC_GET_SWITCH_CALIBRATION 0x49 #define MC_SET_PRN_VALUE 0x4a #define MC_GET_PRN_VALUE 0x4b #define MC_SEND_WARNING 0x50 #define MC_SEND_ERROR 0x51 #define MC_SET_PID_KP 0x5b #define MC_SET_PID_KI 0x5c #define MC_SET_PID_KD 0x5d #define MC_ENABLE_PID_ANALYSIS 0x5f #define MC_GET_HARDSWITCH_ENA 0xee #define MC_SET_HARDSWITCH_ENA 0xef #define MC_GET_CHIPVER 0xfa #define MC_GET_BOOTVER 0xfb #define MC_GET_APPROACH 0xfc #define MC_SET_APPROACH 0xfd #define SSSWI_GET_CHALLENGE 0x31 // SSHC --> SSSWI: generate 32-bit "Challenge" #define SSSWI_RESPONSE 0x32 // SSHC --> SSSWI: send 32-bit "Response" static const struct auxdev_op_s aux_mc_ops[] = { AUX_DEV_OP(MC_GET_POS), AUX_DEV_OP(MC_GOTO_FAST), AUX_DEV_OP(MC_SET_POS), AUX_DEV_OP(MC_SET_POS_GUIDERATE), AUX_DEV_OP(MC_SET_NEG_GUIDERATE), AUX_DEV_OP(MC_REMOTE_SWITCH_ALIVE), AUX_DEV_OP(MC_SWITCH_STATE_CHANGE), AUX_DEV_OP(MC_AG_STATE_CHANGE), AUX_DEV_OP(MC_LEVEL_START), AUX_DEV_OP(MC_PEC_RECORD_START), AUX_DEV_OP(MC_PEC_PEC_PLAYBACK), AUX_DEV_OP(MC_GET_PEC), AUX_DEV_OP(MC_SET_POS_BACKLASH), AUX_DEV_OP(MC_SET_NEG_BACKLASH), AUX_DEV_OP(MC_LEVEL_DONE), AUX_DEV_OP(MC_GOTO_DONE), AUX_DEV_OP(MC_PEC_STATE_CHANGE), AUX_DEV_OP(MC_PECTRAIN_DONE), AUX_DEV_OP(MC_CANCEL_PECTRAIN), AUX_DEV_OP(MC_GOTO_SLOW), AUX_DEV_OP(MC_IS_INDEX_FOUND), AUX_DEV_OP(MC_FIND_INDEX), AUX_DEV_OP(MC_SET_USER_LIMIT_MIN), AUX_DEV_OP(MC_SET_USER_LIMIT_MAX), AUX_DEV_OP(MC_GET_USER_LIMIT_MIN), AUX_DEV_OP(MC_GET_USER_LIMIT_MAX), AUX_DEV_OP(MC_IS_USER_LIMIT_ENA), AUX_DEV_OP(MC_SET_USER_LIMIT_ENA), AUX_DEV_OP(MC_SET_CUSTOM_RATE9), AUX_DEV_OP(MC_GET_CUSTOM_RATE9), AUX_DEV_OP(MC_SET_CUSTOM_RATE9_ENA), AUX_DEV_OP(MC_GET_CUSTOM_RATE9_ENA), AUX_DEV_OP(MC_MOVE_POS), AUX_DEV_OP(MC_MOVE_NEG), AUX_DEV_OP(MC_AUX_GUIDE), AUX_DEV_OP(MC_IS_AUX_GUIDE_ACTIVE), AUX_DEV_OP(MC_CALIBRATION_ENABLE), AUX_DEV_OP(MC_IS_CALIBRATED), AUX_DEV_OP(MC_GET_LIMITS), AUX_DEV_OP(MC_EEPROM_READ), AUX_DEV_OP(MC_EEPROM_WRITE), AUX_DEV_OP(MC_PROGRAM_READ), AUX_DEV_OP(MC_ENABLE_CORDWRAP), AUX_DEV_OP(MC_DISABLE_CORDWRAP), AUX_DEV_OP(MC_SET_CORDWRAP_POS), AUX_DEV_OP(MC_POLL_CORDWRAP), AUX_DEV_OP(MC_GET_CORDWRAP_POS), AUX_DEV_OP(MC_SET_SHUTTER), AUX_DEV_OP(MC_GET_POS_BACKLASH), AUX_DEV_OP(MC_GET_NEG_BACKLASH), AUX_DEV_OP(MC_SET_AUTOGUIDE_RATE), AUX_DEV_OP(MC_GET_AUTOGUIDE_RATE), AUX_DEV_OP(MC_SET_SWITCH_CALIBRATION), AUX_DEV_OP(MC_GET_SWITCH_CALIBRATION), AUX_DEV_OP(MC_SET_PRN_VALUE), AUX_DEV_OP(MC_GET_PRN_VALUE), AUX_DEV_OP(MC_SEND_WARNING), AUX_DEV_OP(MC_SEND_ERROR), AUX_DEV_OP(MC_SET_PID_KP), AUX_DEV_OP(MC_SET_PID_KI), AUX_DEV_OP(MC_SET_PID_KD), AUX_DEV_OP(MC_ENABLE_PID_ANALYSIS), AUX_DEV_OP(MC_GET_HARDSWITCH_ENA), AUX_DEV_OP(MC_SET_HARDSWITCH_ENA), AUX_DEV_OP(MC_GET_CHIPVER), AUX_DEV_OP(MC_GET_BOOTVER), AUX_DEV_OP(MC_GET_APPROACH), AUX_DEV_OP(MC_SET_APPROACH), {0xff, NULL} }; // GPS ops: #define GPS_GET_LAT 0x01 #define GPS_GET_LNG 0x02 #define GPS_GET_DATE 0x03 #define GPS_GET_YEAR 0x04 #define GPS_GET_SAT_INFO 0x07 #define GPS_GET_RCVR_STATUS 0x08 #define GPS_GET_TIME 0x33 #define GPS_TIME_VALID 0x36 #define GPS_LINKED 0x37 #define GPS_GET_HW_VER 0x55 #define GPS_GET_COMPASS 0xa0 static const struct auxdev_op_s aux_gps_ops[] = { AUX_DEV_OP(GPS_GET_LAT), AUX_DEV_OP(GPS_GET_LNG), AUX_DEV_OP(GPS_GET_DATE), AUX_DEV_OP(GPS_GET_YEAR), AUX_DEV_OP(GPS_GET_SAT_INFO), AUX_DEV_OP(GPS_GET_RCVR_STATUS), AUX_DEV_OP(GPS_GET_TIME), AUX_DEV_OP(GPS_TIME_VALID), AUX_DEV_OP(GPS_LINKED), AUX_DEV_OP(GPS_GET_HW_VER), AUX_DEV_OP(GPS_GET_COMPASS), {0xff, NULL} }; // DEW controller ops: #define DEW_BB_OPCODE_05 0x05 #define DEW_QUERY_INPUT_POWER 0x00 #define DEW_GET_LED_BRIGHTNESS 0x02 #define DEW_SET_INPUT_LIMIT 0x03 #define DEW_QUERY_INPUT_LIMITS 0x04 #define DEW_GET_NUM_PORTS 0x10 #define DEW_QUERY_PORT 0x11 #define DEW_QUERY_HEATER 0x12 // channels 0,1; Numbering is weird with more than 2 #define DEW_ENABLE_PORT 0x14 #define DEW_SET_AUTO_AGGR 0x16 #define DEW_SET_MANUAL_PWM 0x17 #define DEW_QUERY_ENV_SENSORS 0x18 #define DEW_RECALIBRATE_ENV_SENSOR 0x19 #define DEW_QUERY_ENV_CALIBRATING 0x1a // "is calibrated?" #define DEW_SET_LED_BRIGHTNESS 0x20 #define DEW_UNKNOWN_21 0x21 // CPWI during startup static const struct auxdev_op_s aux_dew_ops[] = { AUX_DEV_OP(DEW_QUERY_INPUT_POWER), AUX_DEV_OP(DEW_GET_LED_BRIGHTNESS), AUX_DEV_OP(DEW_SET_INPUT_LIMIT), AUX_DEV_OP(DEW_QUERY_INPUT_LIMITS), AUX_DEV_OP(DEW_GET_NUM_PORTS), AUX_DEV_OP(DEW_QUERY_PORT), AUX_DEV_OP(DEW_QUERY_HEATER), AUX_DEV_OP(DEW_ENABLE_PORT), AUX_DEV_OP(DEW_SET_AUTO_AGGR), AUX_DEV_OP(DEW_SET_MANUAL_PWM), AUX_DEV_OP(DEW_QUERY_ENV_SENSORS), AUX_DEV_OP(DEW_RECALIBRATE_ENV_SENSOR), AUX_DEV_OP(DEW_QUERY_ENV_CALIBRATING), AUX_DEV_OP(DEW_SET_LED_BRIGHTNESS), AUX_DEV_OP(DEW_UNKNOWN_21), {0xff, NULL} }; // StarSense AutoAlign ops: #define SSAA_SET_PROFILE 0x3e #define SSAA_GET_PROFILE 0x3f #define SSAA_CAPTURE_BEGIN 0x90 #define SSAA_CAPTURE_GET_STATUS 0x91 #define SSAA_CAPTURE_GET_PLATE 0x92 #define SSAA_CAPTURE_GET_RESULT 0x94 #define SSAA_AIS_RESET 0x9f static const struct auxdev_op_s aux_ssaa_ops[] = { AUX_DEV_OP(SSAA_SET_PROFILE), AUX_DEV_OP(SSAA_GET_PROFILE), AUX_DEV_OP(SSAA_CAPTURE_BEGIN), AUX_DEV_OP(SSAA_CAPTURE_GET_STATUS), AUX_DEV_OP(SSAA_CAPTURE_GET_PLATE), AUX_DEV_OP(SSAA_CAPTURE_GET_RESULT), AUX_DEV_OP(SSAA_AIS_RESET), {0xff, NULL} }; // StarSense AutoGuider ops: #define SSAG_START_PLATE_SOLVE 0x01 // ack #define SSAG_GET_PLATE_STATUS 0x02 // 9-bytes in: 00..=inProgress; otherwise 01.. : PlateSolveResult(SOLVED, RaDec(155.184971527816 degs, 69.1558292709192 degs)) 01-40-2D-57-E1-3F-9A-7E-E0 #define SSAG_START_CALIBRATE 0x03 // 8-bytes out, ack: RaDec(213.908604168867 degs, 19.1693596951982 degs) 40-6E-F0-2E-3E-AB-4C-8E #define SSAG_GET_CALIBRATE_STATUS 0x04 // 5-bytes in: 00..=inProgress; otherwise 01/ff xx.. : CenterCalibrationStatus(SOLVED, x=65134, y=64192) 01-FE-6E-FA-C0 #define SSAG_07 0x07 // 1-byte in (00,01) #define SSAG_SET_GUIDING 0x20 // 1-byte out, ack; 00=disabled; 69: west=True altaz=True #define SSAG_GET_GUIDING_DATA 0x23 // 10-bytes in: Error(0, 0) Correction(0, 0) MoveRa = False MoveDec = False StopAll = TrueGuideStars = 0Quality = 0 00-00-00-00-91-00-00-00-00-00 #define SSAG_SET_MOUNT_INFO 0x30 // 8-bytes out, ack: mountModel = 4, isAltAz = True, azmFirmwareVersion = [40, 99] 04-01-28-63-00-00 #define SSAG_SET_GEOLOCATION 0x31 // 8-bytes out, ack: location = [66, 34, 203, 75, 194, 149, 204, 45] 42-22-CB-4B-C2-95-CC-2D #define SSAG_SET_TIME_NOW 0x32 // 4-bytes out, ack: time = [44, 137, 45, 19] 2C-89-2D-13 (2023-09-04 20:04:35.701 -04:00) #define SSAG_45 0x45 // 4-bytes in static const struct auxdev_op_s aux_ssag_ops[] = { AUX_DEV_OP(SSAG_START_PLATE_SOLVE), AUX_DEV_OP(SSAG_GET_PLATE_STATUS), AUX_DEV_OP(SSAG_START_CALIBRATE), AUX_DEV_OP(SSAG_GET_CALIBRATE_STATUS), AUX_DEV_OP(SSAG_07), AUX_DEV_OP(SSAG_SET_GUIDING), AUX_DEV_OP(SSAG_GET_GUIDING_DATA), AUX_DEV_OP(SSAG_SET_MOUNT_INFO), AUX_DEV_OP(SSAG_SET_GEOLOCATION), AUX_DEV_OP(SSAG_SET_TIME_NOW), AUX_DEV_OP(SSAG_45), {0xff, NULL} }; // Various oddball ops: #define BATT_GET_VOLTAGE 0x10 #define BATT_MAX_CURRENT 0x18 #define LEDS_LEVEL 0x10 // Always returns levels; sets new levels if 1-byte of data supplied. #define PWRCHG_GET_MODE 0x10 #define UART_SET_BAUD 0x51 #define SET_EVO_WIFI 0x10 // All devices ops: #define DEV_PROGRAM_ENA 0x8a #define DEV_GET_VERSION 0xfe #define DEV_GET_MODEL 0x05 #define DEV_BAD_OP 0xf0 // Devices return 'f0 xx' when 'xx' is an unsupported opcode static const struct auxdev_op_s aux_misc_ops[] = { AUX_DEV_OP(BATT_GET_VOLTAGE), AUX_DEV_OP(BATT_MAX_CURRENT), AUX_DEV_OP(LEDS_LEVEL), AUX_DEV_OP(PWRCHG_GET_MODE), AUX_DEV_OP(UART_SET_BAUD), AUX_DEV_OP(SET_EVO_WIFI), AUX_DEV_OP(DEV_PROGRAM_ENA), AUX_DEV_OP(DEV_GET_VERSION), // special handling AUX_DEV_OP(DEV_GET_MODEL), // special handling AUX_DEV_OP(DEV_BAD_OP), // special handling {0xff, NULL} // MUST update misc_ops_dev[] below whenever changing this array! }; static const byte misc_ops_devs[] = { DEV_BATT, DEV_BATT, DEV_LEDS, DEV_PWRCHG, DEV_UART, DEV_EVWIFI, 0, // any dev: GET_PROGRAM_ENA 0, // any dev: GET_VERSION 0, // any dev: GET_MODEL 0, // any dev: GET_BAD_OP 0xff // MUST update this array whenever changing aux_misc_ops[] }; static byte find_op_by_name_array (const char *name, const struct auxdev_op_s *a) { for (const char *aname; (aname = a->name) != NULL; a++) { if (0 == strcasecmp(name, aname)) break; while (*aname != '_' && *++aname); if (*aname == '_' && *++aname && 0 == strcasecmp(name, aname)) break; } return a->op; } // find_op_by_name() is for use with the "send" command: static byte find_op_by_name (const char *name) { byte op = find_op_by_name_array(name, aux_mc_ops); if (op != 0xff) return op; op = find_op_by_name_array(name, aux_misc_ops); if (op != 0xff) return op; op = find_op_by_name_array(name, aux_ssag_ops); if (op != 0xff) return op; op = find_op_by_name_array(name, aux_ssaa_ops); if (op != 0xff) return op; op = find_op_by_name_array(name, aux_gps_ops); if (op != 0xff) return op; return find_op_by_name_array(name, aux_dew_ops); } static const char *op_lookup (byte op, const struct auxdev_op_s *dev_ops) { while (dev_ops->name) { if (op == dev_ops->op) return dev_ops->name; ++dev_ops; } return NULL; } static const char *get_model_uint16 (byte *data, char *dest) { if (data[1] == 0x04) sprintf(dest, "0x%02x", data[5]); else if (data[1] == 0x05) sprintf(dest, "0x%04x", (((uint16_t)data[5]) << 8) | data[6]); return dest; } static const char *get_model_decoder (byte *data) { static const char Model[] = "Model: "; static char ver[32]; const char *desc = NULL; strcpy(ver, Model); char *v = ver + strlen(Model); byte len = data[1]; byte src = data[2]; uint16_t model = 0; if (len == 0x05) model = (((uint16_t)data[5]) << 8) | data[6]; else if (len == 0x04) model = data[5]; if (src == DEV_DEWBB) { if (len != 0x05) return NULL; v += sprintf(v, "%s", dev_to_name(DEV_DEW, true)); if (model == 0x0107) strcat(v, " 2X"); else sprintf(v, " 0x%04x", model); return ver; } if (src == DEV_FOCUS) { if (model == 0x1ba8) strcat(v, "Focus Motor"); else sprintf(v, " 0x%04x", model); return ver; } if (src != DEV_AZM && src != DEV_ALT) return NULL; if (len == 0x04) { if (model == 0x01) desc = "Nexstar-GPS"; else sprintf(v, "0x%02x", data[5]); } else if (len == 0x05) { switch (model) { case 0x0100: desc = "Nexstar-GPS"; break; case 0x0783: desc = "Nexstar-SLT"; break; case 0x0b83: desc = "Nexstar-4/5SE"; break; case 0x0c82: desc = "Nexstar-6/8SE"; break; case 0x1189: desc = "CPC-Deluxe"; break; case 0x1283: desc = "GT-Series"; break; case 0x1485: desc = "AVX"; break; case 0x1788: desc = "CGX"; break; case EVOLUTION_MODEL: desc = "Evolution";break; default: { switch (model >> 8) { case 0x05: desc = "CGE"; break; case 0x06: desc = "Advanced-GT"; break; case 0x09: desc = "CPC"; break; case 0x0a: desc = "GT"; break; case 0x0d: desc = "CGE-Pro"; break; case 0x0e: desc = "CGEM-DX"; break; case 0x0f: desc = "LCM"; break; case 0x10: desc = "SkyProdigy"; break; case 0x13: desc = "Starseeker"; break; case 0x15: desc = "Cosmos"; break; case 0x18: desc = "CGX-L"; break; case 0x19: desc = "Astrofi"; break; case 0x1a: desc = "SkyWatcher"; break; default: sprintf(v, "0x%04x", model); } } } } done: if (desc) strcat(v, desc); return ver; } static const char *get_version_decoder (byte *data) { static const char Version[] = "Version: "; static char ver[32]; char *v = ver + strlen(Version); byte len = data[1]; strcpy(ver, Version); if (len == 0x07) sprintf(v, "%u.%u.%04u", data[5], data[6], (((uint16_t)data[7]) << 8) | data[8]); else if (len == 0x05) sprintf(v, "%u.%u", data[5], data[6]); else return NULL; return ver; } static uint32_t get_24bit_value (byte *p) { uint32_t val = p[0]; val = (val << 8) | p[1]; val = (val << 8) | p[2]; return val; } static void print_24bits (byte *data) { int32_t val = get_24bit_value(data); int is_neg = val & 0x800000; int32_t v = val; if (is_neg) v = -(val | 0xff000000); double degrees = v * 360.0 / 0x1000000; if (degrees < 0.001) degrees = 0; if (is_neg && degrees) degrees = -degrees; SERIAL_PRINTF("%7.3f", degrees); } static const char *op_decoder (byte *data) { byte op = data[4], src = data[2], dst = data[3]; if (op == DEV_GET_VERSION) { if (data[1] != 0x03) return get_version_decoder(data); if (src == DEV_ESP32 && dst == DEV_DUMMY) return "KEEP-ALIVE"; return "GET_VERSION"; } if (op == DEV_GET_MODEL) { if (data[1] != 0x03) return get_model_decoder(data); return "GET_MODEL"; } if (op == MC_GET_POS && (src == DEV_AZM || src == DEV_ALT)) { SERIAL_PRINT(" MC_GET_POS "); print_24bits(data + 5); return NULL; } if (src == DEV_AZM) { if (op == MC_GET_CORDWRAP_POS) { SERIAL_PRINT(" MC_GET_CORDWRAP_POS "); print_24bits(data + 5); return NULL; } if (op == MC_SET_CORDWRAP_POS) { SERIAL_PRINT(" MC_SET_CORDWRAP_POS "); print_24bits(data + 5); return NULL; } } if (op == MC_GET_POS && src == DEV_FOCUS) { SERIAL_PRINTF(" MC_GET_POS %lu", get_24bit_value(data + 5)); return NULL; } if (src == DEV_BATT) { if (op == BATT_MAX_CURRENT) { uint bc = ((uint)data[5] << 8) | data[6]; SERIAL_PRINTF(" BATT_MAX_CURRENT %u.%03uA", bc / 1000, bc % 1000); return NULL; } if (op == BATT_GET_VOLTAGE) { const char *state; ulong bv = (((ulong)data[7] << 24) | ((ulong)data[8] << 16) | ((ulong)data[9] << 8) | data[10]) / 1000; switch (data[5] & 3) { case 00: state = "Discharging"; break; case 01: state = "Charging"; break; case 02: state = "Charged"; break; default: state = "ChargeFault"; break; } SERIAL_PRINTF(" BATT_GET_VOLTAGE %s %u.%02uV", state, bv / 1000, bv % 1000); return NULL; } } switch (dst) { case DEV_FOCUS: if (op == MC_GOTO_FAST) { SERIAL_PRINTF(" MC_GOTO_FAST %lu", get_24bit_value(data + 5)); return NULL; } case DEV_AZM: case DEV_ALT: return op_lookup(op, aux_mc_ops); case DEV_GPS: return op_lookup(op, aux_gps_ops); case DEV_DEW: return op_lookup(op, aux_dew_ops); case DEV_SSAA: return op_lookup(op, aux_ssaa_ops); case DEV_SSAG: if (src == DEV_AZM || src == DEV_ALT) return op_lookup(op, aux_mc_ops); return op_lookup(op, aux_ssag_ops); } static uint32_t ssswi_last_challenge = 0; if (op == SSSWI_GET_CHALLENGE) { if (dst == DEV_SSSWI) return "GET_CHALLENGE"; if (src == DEV_SSSWI) { ssswi_last_challenge = *(uint32_t *)(data + 5); SERIAL_PRINTF(" CHALLENGE 0x%08x", ssswi_last_challenge); return NULL; } } else if (op == SSSWI_RESPONSE) { if (dst == DEV_SSSWI) { uint32_t response = *(uint32_t *)(data + 5); SERIAL_PRINTF(" RESPONSE 0x%08x", response); if (0) SERIAL_PRINTF("\r\nCR: 0x%08lx 0x%08lx", ssswi_last_challenge, response); return NULL; } if (src == DEV_SSSWI) return data[5] ? "RESPONSE_OK" : "RESPONSE_FAIL"; } for (byte d = 0; aux_misc_ops[d].name; ++d) { if (op == aux_misc_ops[d].op && (!misc_ops_devs[d] || dst == misc_ops_devs[d])) return aux_misc_ops[d].name; } if (op == MC_SEND_WARNING || op == MC_SEND_ERROR) { if (src == DEV_AZM || src == DEV_ALT) { if (data[1] == 0x04 && data[5] == 0x00) return "WARNING LOW_BATTERY"; return op_lookup(op, aux_mc_ops); } return (op == MC_SEND_WARNING) ? "MC_SEND_WARNING?" : "MC_SEND_ERROR?"; } return NULL; } static bool dew_debug = false; static byte trace_dev1 = 0; static byte trace_dev2 = 0; #define TRACING_DEV(dev) ((dev) == trace_dev1 || (dev) == trace_dev2) #define TVERBOSE(data) (verbose || ((data)[0] == 0x3b && (TRACING_DEV((data)[2]) || TRACING_DEV((data)[3]))) || ((data)[0] == 0x3c && TRACING_DEV(DEV_SSAA))) #define TVERBOSE2(from_ssaa, data) (((from_ssaa) && TRACING_DEV(DEV_SSAA)) | TVERBOSE(data)) // Evo mount periodically sends these on the test rig (no battery). Battery LOW events. // auxbus_rx: 3b 04 10 0d 50 00 8f // AZM unsolicited data to HC // auxbus_rx: 3b 03 0d 10 50 90 // HC replying to AZM static bool esp32_wifi_off = true; static char advertisement[128]; static WiFiServer W2000(2000); /* Listen on port 2000 for SkyPortal connections */ static WiFiServer W3000(3000); /* Listen on port 3000 for management/debug commands */ static bool wifi_relay_mode = false; // true: AUX bus relay over WiFi static bool wifi_client_mode = false; // true=client; false=SoftAP static bool wifi_client_mode_override = false; // true=client; false=whichever // get a non-zero timeout: static inline long get_timeout (uint msecs) { long timeout = millis() + msecs; return timeout ? timeout : 1; } // Compare against current time, handling wraparound: static inline bool time_after (ulong a, ulong b) {return ((long)(b - a)) < 0;} #define time_before(a,b) time_after((b),(a)) static inline ulong time_delta (long a, long b) {return (a > b) ? a - b : b - a;} // Receive buffers for various interfaces: static struct rxbuf_s { uint16_t len; // for 0x3b packets only byte data[AUXBUS_PKT_MAX]; byte csum; // current accumulated checksum for either 0x3b or 0x3c (Starsense) packets uint16_t discard_len; // Used to discard echo-back of forwarded StarSense Camera 0x3c packet uint16_t sscount; // current Starsense total received count uint32_t sslen32; // Starsense length field, (8 * 100) max uint16_t sslen; // Starsense expected total bytecount, increased as padding is encountered byte sspadding; // Starsense current padding count (max 2): exclude from csum byte requestor; // Used to prevent echoing commands back to originator of the command. char name[12]; struct bus_s *bus; } w2000_rxbuf, bt_rxbuf; #if ETHERNET_ENABLED static struct rxbuf_s e2000_rxbuf; static void e2000_tx (bool from_ssaa, byte *data, uint16_t count); #endif static void init_rxbuf (struct rxbuf_s *rxbuf, const char *name) { memset(rxbuf, 0, sizeof(*rxbuf)); strcpy(rxbuf->name, name); routing_remove_sofware_devs(); } enum {pkt_fail, pkt_ssinprogress, pkt_inprogress, pkt_complete, pkt_sscomplete}; static byte packet_decoder (struct rxbuf_s *rxbuf, byte b); enum {forward_yes, // Pass this packet along to other potential recipients. forward_no, // Do not pass this packet along any further (to prevent echo-back storms). forward_drop, // Like forward_no, but also suppress tracing of the messages. forward_discard, // StarSense Camera data: completely discard the echo-backs. forward_auxtest // Test message from auxtest; no forwarding. }; struct pkt_s { struct rxbuf_s *from_rxbuf; uint16_t len; byte data[AUXBUS_PKT_MAX]; byte forward; bool regular_pkt; // true here indicates a full/normal 0x3b packet bool indirect_data; }; #define REGULAR_PKT true struct txq_s { HardwareSerial *uart; struct pkt_s pkts[AUXBUS_TXQ_SIZE]; byte head; byte tail; byte busyin_pin; byte busyout_pin; long busy_timestamp; char name[12]; bool is_alive; uint16_t last_tx_len; byte last_tx[AUXBUS_SSAA_PKT_MAX]; ulong pending_baud; #define TXQ_MONITOR_SIZE true #if TXQ_MONITOR_SIZE byte pkts_in_use; byte pkts_max; #endif }; static inline void txq_advance_tail (struct txq_s *txq) { txq->tail++; if (txq->tail >= AUXBUS_TXQ_SIZE) txq->tail = 0; auxtest_timer = 0; } static void txq_free_pkt (struct txq_s *txq, struct pkt_s *p) { #if TXQ_MONITOR_SIZE txq->pkts_in_use--; #endif if (p->indirect_data) { p->indirect_data = false; byte *pdata = *(byte **)p->data; free(pdata); } p->len = 0; // discard/free the txq entry txq_advance_tail(txq); } static void txq_discard_all (struct txq_s *txq) { while (true) { struct pkt_s *p = &txq->pkts[txq->tail]; if (p->len == 0) break; /* txq is empty. */ txq_free_pkt(txq, p); } } static void txq_init (struct txq_s *txq, const char *name, HardwareSerial *uart, byte rx_pin, byte tx_pin, byte busyin_pin, byte busyout_pin) { memset(txq, 0, sizeof(*txq)); strcpy(txq->name, name); txq->uart = uart; txq->busyin_pin = busyin_pin; txq->busyout_pin = busyout_pin; pinMode(busyin_pin, INPUT); if (busyout_pin == busyin_pin) { pinMode(rx_pin, INPUT); // get rid of internal pull-up for this scenario only } else { pinMode(busyout_pin, OUTPUT); digitalWrite(busyout_pin, HIGH); // Tri-state BUSYOUT } } static struct bus_s { struct rxbuf_s *rxbuf; struct txq_s *txq; long delay; uint16_t count; byte data[96]; // Large enough to efficiently buffer Starsense 0x3c packets byte tx_pin; byte rx_pin; const char *name; } auxbus, auxrelay; static bool auxrelay_detected; // Gets set when bus_init() is called from setup() static inline void mkname (char *dst, const char *base, const char *suffix) { strcpy(dst, base); strcat(dst, suffix); } static bool bus_detect (byte busyout_pin, byte rx_pin, byte tx_pin) { bool detected = false; for (byte attempts = 3; !detected && attempts--;) { detected = true; delay(50); pinMode(rx_pin, INPUT); pinMode(tx_pin, OUTPUT); pinMode(busyout_pin, OUTPUT); digitalWrite(busyout_pin, LOW); byte test = 0xa5; while (test != 0) { int bit = (test & 1) ? HIGH : LOW; test >>= 1; digitalWrite(tx_pin, bit); delayMicroseconds(50); if (digitalRead(rx_pin) != bit) { detected = false; break; } } } pinMode(busyout_pin, INPUT); pinMode(tx_pin, INPUT); return detected; } // Possibly useful for debugging hardware faults: static void bus_test (struct bus_s *bus) { struct txq_s *txq = bus->txq; bool ok = false; // Don't attempt test on older single-BUSY designs if (!txq) { SERIAL_PRINTF("%s: not present.\r\n", bus->name); return; } if (txq->busyin_pin == txq->busyout_pin) { SERIAL_PRINTF("%s: busyin==busyout, cannot test this.\r\n", bus->name); return; } txq->uart->end(); pinMode(txq->busyout_pin, OUTPUT); digitalWrite(txq->busyout_pin, HIGH); // not-asserted pinMode(txq->busyin_pin, INPUT); delay(100); for (byte attempt = 1; !ok && attempt < 3; ++attempt) { pinMode(txq->busyout_pin, INPUT); // tri-stated delay(1); // was 10 ok = true; if (digitalRead(txq->busyin_pin) != HIGH) { SERIAL_PRINTF("%s: %u: BusyIn not HIGH when BusyOut tri-stated\r\n", bus->name, attempt); ok = false; } pinMode(txq->busyout_pin, OUTPUT); digitalWrite(txq->busyout_pin, LOW); // asserted delay(1); if (digitalRead(txq->busyin_pin) != LOW) { SERIAL_PRINTF("%s: %u: BusyIn not LOW when BusyOut LOW\r\n", bus->name, attempt); ok = false; } digitalWrite(txq->busyout_pin, HIGH); // not-asserted delay(1); if (digitalRead(txq->busyin_pin) != HIGH) { SERIAL_PRINTF("%s: %u: BusyIn not HIGH when BusyOut HIGH\r\n", bus->name, attempt); ok = false; } } pinMode(txq->busyout_pin, INPUT); // tri-stated SERIAL_PRINTF("%s: Busy test %s\r\n", bus->name, ok ? "passed" : "failed"); ok = bus_detect(txq->busyout_pin, bus->rx_pin, bus->tx_pin); SERIAL_PRINTF("%s: Tx/Rx test %s\r\n", bus->name, ok ? "passed" : "failed"); txq->uart->begin(19200, (auxbus_stopbits == 1) ? SERIAL_8N1 : SERIAL_8N2, bus->rx_pin, bus->tx_pin); } static bool bus_init (struct bus_s *bus, bool do_loopback_test, const char *name, HardwareSerial *uart, int rx_pin, byte tx_pin, byte busyin_pin, byte busyout_pin) { char tname[12]; memset(bus, 0, sizeof(*bus)); bus->name = name; if (rx_pin == -1 || (do_loopback_test && !bus_detect(busyout_pin, rx_pin, tx_pin))) return false; bus->rxbuf = (struct rxbuf_s *)malloc(sizeof(struct rxbuf_s)); bus->tx_pin = tx_pin; bus->rx_pin = rx_pin; mkname(tname, name, "_rx"); init_rxbuf(bus->rxbuf, tname); bus->rxbuf->bus = bus; bus->txq = (struct txq_s *)malloc(sizeof(struct txq_s)); mkname(tname, name, "_tx"); txq_init(bus->txq, tname, uart, rx_pin, tx_pin, busyin_pin, busyout_pin); uart->setRxBufferSize((2 * AUXBUS_PKT_MAX) + AUXBUS_SSAA_PKT_MAX); // Large enough for SSAA packets, plus some uart->begin(19200, (auxbus_stopbits == 1) ? SERIAL_8N1 : SERIAL_8N2, rx_pin, tx_pin); return true; } /* * Keep track of which connection various devices has been seen on, * and don't send packets destined for it to anywhere else. */ #define MAX_ROUTES 20 static struct dev_route_s { byte dev; struct rxbuf_s *route; } routing_table[MAX_ROUTES] = {{0,NULL},}; // one extra for zero-termination static byte num_routes = 0; static bool routing_check_dev (byte dev, struct rxbuf_s *route) { for (byte i = 0; i < num_routes; ++i) { struct dev_route_s *db = routing_table + i; if (db->dev == dev) return (db->route && db->route == route); } return true; } static void routing_add_dev (byte dev, struct rxbuf_s *route) { byte i, empty_slot = (MAX_ROUTES - 1); for (i = 0; i < num_routes; ++i) { struct dev_route_s *db = &routing_table[i]; if (db->dev == dev) return; // don't check or update the route: it cannot change, and echo-backs could confuse us! if (!db->dev && i < empty_slot) empty_slot = i; } add_route: if (empty_slot < num_routes) { i = empty_slot; } else { if (num_routes < MAX_ROUTES) num_routes++; i = num_routes - 1; // Re-uses final slot when table is full. } struct dev_route_s *db = &routing_table[i]; db->dev = dev; db->route = route; SERIAL_PRINTF("routing[%u]: %s (%02x) on %s\r\n", i, dev_to_name(dev, false), dev, route ? route->name : "NULL"); } static void routing_remove_dev (byte dev) { for (byte i = 0; i < num_routes; ++i) { struct dev_route_s *db = &routing_table[i]; if (db->dev == dev) { struct rxbuf_s *route = db->route; db->dev = 0; db->route = NULL; if (i == (num_routes - 1)) --num_routes; SERIAL_PRINTF("routing[%u]: %s (%02x) removed from %s\r\n", i, dev_to_name(dev, false), dev, route ? route->name : "NULL"); return; } } } static void routing_remove_sofware_devs (void) { // The various "software" devices can move around when the connection method changes. // So remove all them when closing a connection. The known ones use 0x2x device numbers: for (byte dev = 0x20; dev <= 0x2f; ++dev) routing_remove_dev(dev); } static inline void auxbus_raw_break (void) { if (auxbus_raw_count) { auxbus_raw_timeout = auxbus_raw_count = 0; SERIAL_ENDLINE(); } } static const char BAD_CHECKSUM[] = "BAD-CHECKSUM"; // Special string shared between packet_decoder() and print_packet() static const char TIMED_OUT [] = "TIMED-OUT"; // Special string shared between packet_decoder() and print_packet() static char spaces [69]; // used to align opcodes display in print_packet() static void print_packet (const char *prefix1, const char *prefix2, byte *data, uint16_t len) { const char *opname = NULL; if (aux_only_tracing) { char c = *prefix1; if ((c == 'w' && 0 == strncmp(prefix1, "w2000_", 6)) || (c == 'b' && 0 == strncmp(prefix1, "bt_", 3)) #if ETHERNET_ENABLED || (c == 'e' && 0 == strncmp(prefix1, "e2000_", 6)) #endif || (c == 'm' && 0 == strncmp(prefix1, "musb_", 5))) return; } if (prefix2 == TIMED_OUT) { opname = TIMED_OUT; prefix2 = NULL; } else if (prefix2 == BAD_CHECKSUM) { opname = BAD_CHECKSUM; prefix2 = NULL; } auxbus_raw_break(); SERIAL_PRINTF("%09lu %9s: ", millis(), prefix1); uint count = 9 + 1 + 9 + 2; if (prefix2) { SERIAL_PRINTF("%s: ", prefix2); count += strlen(prefix2) + 2; } byte begin_data = count; for (uint16_t i = 0; i < len; ++i) { if (i && !(i % 16)) { count = begin_data; SERIAL_PRINTF("\r\n%s", spaces + (sizeof(spaces) - 1 - count)); } SERIAL_PRINTF("%02x ", data[i]); count += 3; } if (data[0] == 0x3b && data[1] == (len - 3)) { static const char tabs[] = "\t\t\t\t\t\t\t\t\t"; // Lighter load on Serial port using tabs! uint t = count / 8; if (t < strlen(tabs)) SERIAL_PRINTF("%s", tabs + t); SERIAL_PRINTF("[%-6s -> %-6s]", dev_to_name(data[2], false), dev_to_name(data[3], false)); if (!opname) opname = op_decoder(data); if (opname) SERIAL_PRINTF(" %s", opname); } SERIAL_ENDLINE(); } // Update regular_pkt checksum in-situ static void update_checksum (byte *data) { byte len = data[1] + 1, csum = 0; while (len--) csum += *++data; *++data = 0 - csum; } static void tx_enq (struct rxbuf_s *from_rxbuf, struct bus_s *bus, byte forward, bool regular_pkt, const byte *data, uint16_t len) { if (!auxrelay_detected) { if (bus == &auxrelay) return; } else if (regular_pkt) { byte dst = data[3]; if (!routing_check_dev(dst, bus->rxbuf)) // Destination is "bus->rxbuf", not "from_rxbuf" return; } struct txq_s *txq = bus->txq; struct pkt_s *p = &txq->pkts[txq->head]; if (p->len != 0) { SERIAL_PRINTF("%s overflow\r\n", txq->name); // auxbus_tx overflow txq_discard_all(txq); bus->rxbuf->discard_len = 0; return; } #if TXQ_MONITOR_SIZE txq->pkts_in_use++; if (txq->pkts_in_use > txq->pkts_max) { txq->pkts_max = txq->pkts_in_use; if (verbose) SERIAL_PRINTF("%s: pkts_max=%u\r\n", txq->name, txq->pkts_max); } #endif p->regular_pkt = regular_pkt; p->from_rxbuf = from_rxbuf; // needed for internally handled requests (eg. GPS) p->forward = forward; p->indirect_data = false; byte *pdata = p->data; if (len <= sizeof(p->data)) { memcpy(pdata, data, len); } else { if (EMULATING_SSAA && data == ssaa_image_pkt) { pdata = (byte *)data; // Drop "const" for this assignment } else { // Try to avoid memory fragmentation by always allocating the same size, when possible: uint16_t alloc_len = (len <= sizeof(bus->data)) ? sizeof(bus->data) : len; pdata = (byte *)malloc(alloc_len); if (!pdata) { SERIAL_PRINTF("%s: large pkt: malloc(%u) failed\r\n", txq->name, alloc_len); return; } memcpy(pdata, data, len); } *(byte **)(p->data) = pdata; // Pass pointer to the real data as first 4-bytes of p->data[]: p->indirect_data = true; // bus_tx() will free() the buffer after sending. } p->len = len; txq->head++; if (txq->head >= AUXBUS_TXQ_SIZE) txq->head = 0; } // Use this instead of tx_enq() when dst could be an emulated device on the HBG3 itself! static void auxbus_send_msg (byte *data, uint16_t len) { bool regular_pkt = (data[0] == 0x3b); if (regular_pkt) { data[1] = len - 3; update_checksum(data); } tx_enq(NULL, &auxbus, forward_yes, regular_pkt, data, len); if (auxrelay_detected) tx_enq(NULL, &auxrelay, forward_no, regular_pkt, data, len); // Needed for Nunchuck controlling Focus Motor, and SSAA tests } #define AUXBUS_SEND_MSG(...) do { byte _msg[] = {0x3b, 0x00, __VA_ARGS__, 0x00}; auxbus_send_msg(_msg, sizeof(_msg)); } while (0) // **** Begin Mount-USB (MUSB) ********************************************************************************************************** static struct rxbuf_s musb_rxbuf; static bool musb_selected = false; static bool musb_rfkill = true; // nvram static long musb_connected = 0; static uint32_t musb_rxcount = 0; static uint32_t musb_txcount = 0; static bool musb_debug = false; static bool musb_timestamps = false; static ulong musb_last4 = 0; static long musb_switch_timer = 0; static bool musb_switch_is_on = false; static void monitor_musb_switch (void) { bool old = musb_switch_is_on; musb_switch_is_on = (READ_MUSB_SELECT_PIN() == LOW); if (old != musb_switch_is_on) { musb_switch_timer = (musb_switch_is_on == musb_selected) ? 0 : get_timeout(1500); SERIAL_PRINTF("MUSB switch changed to %s\r\n", musb_switch_is_on ? "LOW" : "HIGH"); } else if (musb_switch_timer && time_after(millis(), musb_switch_timer)) { SERIAL_PRINTLN("MUSB switch reset triggered"); musb_switch_timer = 0; esp32_reset_pending = get_timeout(50); } } static void musb_disconnect (void) { SERIAL_PRINTLN(__func__); musb_txcount = musb_rxcount = 0; init_rxbuf(&musb_rxbuf, "musb_rx"); musb_connected = 0; musb_last4 = 0; musb_uart->updateBaudRate(19200); p2000_timeout = P2000_NORMAL_TIMEOUT; SERIAL_PRINTLN("MUSB baud 19200"); } static void musb_tx (bool from_ssaa, byte *data, uint16_t count) { if (musb_selected && musb_connected) { musb_uart->write(data, count); musb_txcount += count; musb_uart->flush(true); // Necessary for CPWI !! if (TVERBOSE2(from_ssaa, data)) print_packet(__func__, NULL, data, count); } } /* * CPWI Mount-USB handshake/baudrate sequence: * * CPWI sends 'V' at 9600 ; hand-controller would respond with '#', but we don't even see it at 19200. * CPWI sends 78 e6 98 e0 at 19200. * MUSB changes to 115200 change, CPWI-2.5.2 waits 3.0 seconds, then sends: * 0a 00 02 08 00 4b 00 00 d0 c0 ; first byte 0a is length, 4b00 is 19200. * CPWI-2.5.2 waits 2.0 seconds after previous transmission, then begins AUX protocol at 115200: * 3b 03 20 10 fe cf * * NexRemote does it differently, at 19200 or 9600: * 78 e6 * 78 e6 .. * Not sure what response it is expecting? */ static inline void musb_autobaud (byte b) { musb_last4 = (musb_last4 << 8) | b; // CPWI sends 0x78e698e0 at 19200 to enter "bootloader mode" on hand-controller. Sometimes leading 78 is different. // When CPWI "reconnnects", it stays at 115200 and the data at 19200 is f8 f8 f8 f8.. if ((musb_last4 & 0x00ffffff) == 0x00e698e0 || musb_last4 == 0xf8f89898) { musb_last4 = 0; musb_uart->updateBaudRate(115200); if (musb_debug) SERIAL_ENDLINE(); SERIAL_PRINTLN("MUSB baud 115200"); musb_connected = get_timeout(9 * 1000); // CPWI-2.5.2 takes 5 seconds to begin sending AUX commands } } static void musb_loop (void) { if (musb_selected) { if (musb_connected && time_after(millis(), musb_connected)) musb_disconnect(); while (musb_uart->available()) { musb_rxcount++; byte b = musb_uart->read(); if (musb_debug) { static byte musb_count = 0; if (musb_debug) { if (musb_timestamps) { musb_count = 0; SERIAL_PRINTF("%09lu: %02x\r\n", millis(), b); } else { if (musb_count && (b == 0x3b || musb_count >= 16)) { musb_count = 0; SERIAL_ENDLINE(); } ++musb_count; SERIAL_PRINTF("%02x ", b); } } } if (pkt_complete == packet_decoder(&musb_rxbuf, b)) musb_connected = get_timeout(p2000_timeout); else if (!musb_connected) musb_autobaud(b); } } } // **** End Mount-USB (MUSB) ********************************************************************************************************** static inline byte emulate_begin (const char *devname, byte *data, byte *reply) { // Initialize response header: reply[0] = 0x3b; reply[1] = 3; // length reply[2] = data[3]; // Destination device (local/emulated GPS,DEW,FOCUS,SSAA,SSAG..) reply[3] = data[2]; // Originating device (SW,HC5,ESP32..) reply[4] = data[4]; // opcode if (TVERBOSE(data)) print_packet(devname, NULL, data, data[1] + 3); return reply[1] + 2; // overall length, not including the checksum byte } static void emulate_send_reply (const char *devname, struct rxbuf_s *rxbuf, byte *data, uint16_t len); static byte long_to_4bytes (byte *dest, ulong v) { byte *b = (byte *)&v; *dest++ = b[3]; *dest++ = b[2]; *dest++ = b[1]; *dest++ = b[0]; return 4; } #if EMULATE_GPS #if !NTP_ENABLED #define ntp_time_is_valid 0 #else static long ntp_time_is_valid = 0; // Timestamp of most recent NTP update. static byte ntp_fast_update_secs = 0; // Interval in seconds for next NTP update, when requested. static struct ntp_time_s { uint16_t year; uint8_t month; uint8_t day; uint8_t hh; uint8_t mm; uint8_t ss; } ntp_time; static void timesecs_to_utc (ulong ntp_secs) { struct tm t; const ulong seventy_years = 2208988800ul; time_t secs = ntp_secs - seventy_years; // NTP returns secs since 1900; gmtime() wants secs since 1970 gmtime_r(&secs, &t); ntp_time.year = t.tm_year + 1900; ntp_time.month = t.tm_mon + 1; ntp_time.day = t.tm_mday; ntp_time.hh = t.tm_hour; ntp_time.mm = t.tm_min; ntp_time.ss = t.tm_sec; } #include static WiFiUDP udp_ntp; void ntp_loop (void) { if (!gps_detected) return; // No need for NTP if not using the GPS functionality static bool ntp_initialized = false; if (!ntp_initialized) { ntp_initialized = true; udp_ntp.begin(8888); // Local port used for receiving NTP responses; could be anything. } static long fast_update_time = 0; static long ntp_nexttime = 0; long now = millis(); byte pkt[48]; if (ntp_fast_update_secs) { if (fast_update_time && time_after(now, fast_update_time)) { fast_update_time = 0; ntp_nexttime = 0; } if (!fast_update_time) { fast_update_time = get_timeout(ntp_fast_update_secs * 1000); ntp_fast_update_secs = 0; } } if (!ntp_nexttime || time_after(now, ntp_nexttime)) { if (WiFi.status() != WL_CONNECTED) { ntp_nexttime = get_timeout(5 * 1000); return; } memset(pkt, 0, sizeof(pkt)); pkt[ 0] = 0b11100011; // LI, Version, Mode pkt[ 1] = 0; // Stratum, or type of clock pkt[ 2] = 6; // Polling Interval pkt[ 3] = 0xec; // Peer Clock Precision pkt[12] = 49; pkt[13] = 0x4e; pkt[14] = 49; pkt[15] = 52; udp_ntp.beginPacket("pool.ntp.org", 123); // Destination port 123 (NTP) udp_ntp.write(pkt, sizeof(pkt)); udp_ntp.endPacket(); ntp_nexttime = get_timeout(4 * 60 * 1000); return; } if (!udp_ntp.parsePacket()) return; ntp_time_is_valid = millis(); udp_ntp.read(pkt, sizeof(pkt)); // get UDP response packet into pkt[] ulong ntp_secs = ((ulong)pkt[40] << 24) | ((ulong)pkt[41] << 16) | ((ulong)pkt[42] << 8) | pkt[43]; timesecs_to_utc(ntp_secs); if (verbose) SERIAL_PRINTF("ntp_time: %02u/%02u/%04u UTC %02u:%02u:%02u\r\n", ntp_time.day, ntp_time.month, ntp_time.year, ntp_time.hh, ntp_time.mm, ntp_time.ss); } #endif /* NTP_ENABLED */ static bool gps_debug = false; // GPS info. Can be toggled at run-time (hit 'g' on serial monitor) static bool GPS_debug = false; // GPS NMEA sentences. Can be toggled at run-time (hit 'G' on serial monitor) //#include "NMEA_playback.h" // For debugging GPS NMEA decoding. // ****Begin TinyGPS++ ********************************************************************************************************** /* * A small GPS library for Arduino providing universal NMEA parsing * Heavily distilled from TinyGPS++, Copyright (C) 2008-2013 Mikal Hart * All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ //#define _GPS_VERSION "1.0.3+" // software version of this library #define _GPS_MAX_FIELD_SIZE 15 static bool _gps_is_valid_ (long *timeout) { if (*timeout && time_after(millis(), *timeout)) *timeout = 0; return 0 != *timeout; } struct RawDegrees { uint16_t deg; uint32_t billionths; bool negative; public: RawDegrees() : deg(0), billionths(0), negative(false) {} }; struct GPSLocation { friend class GPSPlus; public: GPSLocation() : validTimeout(0) {} double lat(); double lng(); bool is_valid() {return _gps_is_valid_(&validTimeout);} private: RawDegrees rawLatData, rawLngData, rawNewLatData, rawNewLngData; long validTimeout; void commit(); }; struct GPSDate { friend class GPSPlus; public: GPSDate() : validTimeout(0), have_newval(0) {} uint16_t year(); uint8_t month(); uint8_t day(); bool is_valid() {return _gps_is_valid_(&validTimeout);} private: bool have_newval; uint32_t date, newval; long validTimeout; void commit(); }; struct GPSTime { friend class GPSPlus; public: GPSTime() : validTimeout(0), have_newval(0) {} bool getHMS(byte *dest); bool is_valid() {return _gps_is_valid_(&validTimeout);} private: uint8_t hour(); uint8_t minute(); uint8_t second(); bool have_newval; uint32_t time, newval; long validTimeout; void commit(); }; struct GPSInteger { friend class GPSPlus; public: GPSInteger() : validTimeout(0), have_newval(0) {} uint32_t value() {return validTimeout ? val : 0;} bool is_valid() {return _gps_is_valid_(&validTimeout);} private: bool have_newval; uint32_t val, newval; long validTimeout; void commit(); void set(const char *term); void cond_set2(const char *term); }; class GPSPlus { public: GPSPlus(); GPSDate date; GPSTime time; GPSLocation location; GPSInteger satellites; uint8_t satellitesInView(); bool encode(char c); // process one character received from GPS void invalidate(void); private: enum {GPS_SENTENCE_OTHER, GPS_SENTENCE_GPGGA, GPS_SENTENCE_GPRMC, GPS_SENTENCE_GNGLL, GPS_SENTENCE_GPGSV, GPS_SENTENCE_GLGSV, GPS_SENTENCE_GQGSV, GPS_SENTENCE_GAGSV, GPS_SENTENCE_GBGSV}; // parsing state variables: uint8_t parity; bool isChecksumTerm; char term[_GPS_MAX_FIELD_SIZE]; uint8_t curSentenceType; uint8_t curTermNumber; uint8_t curTermOffset; bool sentenceHasFix; bool endOfTermHandler(); GPSInteger satellitesInViewGP; GPSInteger satellitesInViewGL; GPSInteger satellitesInViewGQ; GPSInteger satellitesInViewGA; GPSInteger satellitesInViewGB; }; /******************************* GPS++.cpp *****************************/ GPSPlus::GPSPlus() : parity(0) , isChecksumTerm(false) , curSentenceType(GPS_SENTENCE_OTHER) , curTermNumber(0) , curTermOffset(0) , sentenceHasFix(false) { term[0] = '\0'; } uint8_t GPSPlus::satellitesInView (void) { return satellitesInViewGP.value() + satellitesInViewGL.value() + satellitesInViewGQ.value() + satellitesInViewGA.value() + satellitesInViewGB.value();} bool GPSPlus::encode(char c) { if (c == 0) { curTermOffset = 0; return false; } switch(c) { case ',': // term terminators parity ^= (uint8_t)c; case '\r': case '\n': case '*': { bool isValidSentence = false; if (curTermOffset < sizeof(term)) { term[curTermOffset] = 0; isValidSentence = endOfTermHandler(); } ++curTermNumber; curTermOffset = 0; isChecksumTerm = c == '*'; return isValidSentence; } break; case '$': // sentence begin curTermNumber = curTermOffset = 0; parity = 0; curSentenceType = GPS_SENTENCE_OTHER; isChecksumTerm = false; sentenceHasFix = false; break; default: // ordinary characters if (curTermOffset < sizeof(term) - 1) term[curTermOffset++] = c; if (!isChecksumTerm) parity ^= c; break; } return false; } static int fromHex(char a) { if (a >= 'A' && a <= 'F') return a - 'A' + 10; else if (a >= 'a' && a <= 'f') return a - 'a' + 10; else return a - '0'; } static bool get_digits (const char *term, byte n, uint32_t *ret_p, bool excess_ok) { uint32_t t = 0; for (byte i = 0; i < n; ++i) { char c = term[i]; if (c < '0' || c > '9') return false; t = (t * 10) + ((unsigned)c - '0'); } if (!excess_ok && term[n] >= '0' && term[n] <= '9') return false; *ret_p = t; return true; } static bool get_time (const char *term, uint32_t *time) { uint32_t t1 = 0, t2 = 0; if (!get_digits(term, 6, &t1, false) || term[6] != '.' || !get_digits(term + 7, 2, &t2, true)) return false; *time = t1 * 100 + t2; return true; } // Parse degrees in that funny NMEA format DDMM.MMMM static void parseDegrees(const char *term, struct RawDegrees °) { uint32_t leftOfDecimal = (uint32_t)atol(term); uint16_t minutes = (uint16_t)(leftOfDecimal % 100); uint32_t multiplier = 10000000UL; uint32_t tenMillionthsOfMinutes = minutes * multiplier; deg.deg = (int16_t)(leftOfDecimal / 100); while (isdigit(*term)) ++term; if (*term == '.') { while (isdigit(*++term)) { multiplier /= 10; tenMillionthsOfMinutes += (*term - '0') * multiplier; } } deg.billionths = (5 * tenMillionthsOfMinutes + 1) / 3; deg.negative = false; } #define COMBINE(sentence_type, term_number) (((unsigned)(sentence_type) << 5) | term_number) // Processes a just-completed term // Returns true if new sentence has just passed checksum test and is validated bool GPSPlus::endOfTermHandler (void) { static uint8_t prevSentenceType = ~0; // If it's the checksum term, and the checksum checks out, commit if (isChecksumTerm) { byte checksum = 16 * fromHex(term[0]) + fromHex(term[1]); if (checksum != parity) { //if (GPS_debug) SERIAL_PRINTLN("GPS: bad csum\r\n"); switch(curSentenceType) { case GPS_SENTENCE_GPGSV: satellitesInViewGP.have_newval = false; break; case GPS_SENTENCE_GLGSV: satellitesInViewGL.have_newval = false; break; case GPS_SENTENCE_GQGSV: satellitesInViewGQ.have_newval = false; break; case GPS_SENTENCE_GAGSV: satellitesInViewGA.have_newval = false; break; case GPS_SENTENCE_GBGSV: satellitesInViewGB.have_newval = false; break; } prevSentenceType = ~0; return false; } if (curSentenceType != prevSentenceType) { switch(prevSentenceType) { case GPS_SENTENCE_GPGSV: satellitesInViewGP.commit(); break; case GPS_SENTENCE_GLGSV: satellitesInViewGL.commit(); break; case GPS_SENTENCE_GQGSV: satellitesInViewGQ.commit(); break; case GPS_SENTENCE_GAGSV: satellitesInViewGA.commit(); break; case GPS_SENTENCE_GBGSV: satellitesInViewGB.commit(); break; } prevSentenceType = curSentenceType; } switch(curSentenceType) { case GPS_SENTENCE_GPRMC: date.commit(); time.commit(); if (sentenceHasFix) location.commit(); break; case GPS_SENTENCE_GPGGA: time.commit(); if (sentenceHasFix) location.commit(); satellites.commit(); break; case GPS_SENTENCE_GNGLL: time.commit(); if (sentenceHasFix) location.commit(); break; } return true; } // the first term determines the sentence type if (curTermNumber == 0 && strlen(term) == 5) { if (!strcmp(term, "GPRMC") || !strcmp(term, "GNRMC") || !strcmp(term, "GLRMC")) curSentenceType = GPS_SENTENCE_GPRMC; else if (!strcmp(term, "GPGGA") || !strcmp(term, "GNGGA") || !strcmp(term, "GLGGA")) curSentenceType = GPS_SENTENCE_GPGGA; else if (!strcmp(term, "GNGLL") || !strcmp(term, "GPGLL")) curSentenceType = GPS_SENTENCE_GNGLL; else if (!strcmp(term, "GPGSV")) curSentenceType = GPS_SENTENCE_GPGSV; else if (!strcmp(term, "GLGSV")) curSentenceType = GPS_SENTENCE_GLGSV; else if (!strcmp(term, "GQGSV")) curSentenceType = GPS_SENTENCE_GQGSV; else if (!strcmp(term, "GAGSV")) curSentenceType = GPS_SENTENCE_GAGSV; else if (!strcmp(term, "GBGSV")) curSentenceType = GPS_SENTENCE_GBGSV; else curSentenceType = GPS_SENTENCE_OTHER; return false; } if (curSentenceType != GPS_SENTENCE_OTHER && term[0]) { switch(COMBINE(curSentenceType, curTermNumber)) { case COMBINE(GPS_SENTENCE_GPRMC, 1): // Time in both sentences case COMBINE(GPS_SENTENCE_GPGGA, 1): case COMBINE(GPS_SENTENCE_GNGLL, 5): time.have_newval = get_time(term, &time.newval); break; case COMBINE(GPS_SENTENCE_GPRMC, 2): // GPRMC validity case COMBINE(GPS_SENTENCE_GNGLL, 6): // Fix data (GPGGA) sentenceHasFix = term[0] == 'A'; break; case COMBINE(GPS_SENTENCE_GPRMC, 3): // Latitude case COMBINE(GPS_SENTENCE_GPGGA, 2): case COMBINE(GPS_SENTENCE_GNGLL, 1): parseDegrees(term, location.rawNewLatData); break; case COMBINE(GPS_SENTENCE_GPRMC, 4): // N/S case COMBINE(GPS_SENTENCE_GPGGA, 3): case COMBINE(GPS_SENTENCE_GNGLL, 2): location.rawNewLatData.negative = term[0] == 'S'; break; case COMBINE(GPS_SENTENCE_GPRMC, 5): // Longitude case COMBINE(GPS_SENTENCE_GPGGA, 4): case COMBINE(GPS_SENTENCE_GNGLL, 3): parseDegrees(term, location.rawNewLngData); break; case COMBINE(GPS_SENTENCE_GPRMC, 6): // E/W case COMBINE(GPS_SENTENCE_GPGGA, 5): case COMBINE(GPS_SENTENCE_GNGLL, 4): location.rawNewLngData.negative = term[0] == 'W'; break; case COMBINE(GPS_SENTENCE_GPRMC, 9): // Date (GPRMC) date.have_newval = get_digits(term, 6, &date.newval, false); break; case COMBINE(GPS_SENTENCE_GPGGA, 6): // Fix data (GPGGA) sentenceHasFix = term[0] > '0'; break; case COMBINE(GPS_SENTENCE_GPGGA, 7): // Satellites used (GPGGA) satellites.set(term); break; /* * The "SV" (Sats-in-View) sentences are problematic. * We want to keep only the maximum value from each sequence of each type. * But the current implementation can be confused when checksums fail. * A much better solution would be to NOT parse lines until AFTER checksums are validated!! */ case COMBINE(GPS_SENTENCE_GPGSV, 3): satellitesInViewGP.cond_set2(term); break; case COMBINE(GPS_SENTENCE_GLGSV, 3): satellitesInViewGL.cond_set2(term); break; case COMBINE(GPS_SENTENCE_GQGSV, 3): satellitesInViewGQ.cond_set2(term); break; case COMBINE(GPS_SENTENCE_GAGSV, 3): satellitesInViewGA.cond_set2(term); break; case COMBINE(GPS_SENTENCE_GBGSV, 3): satellitesInViewGB.cond_set2(term); break; } } return false; } #define GPS_TIMEOUT 15000lu void GPSLocation::commit (void) { rawLatData = rawNewLatData; rawLngData = rawNewLngData; validTimeout = get_timeout(GPS_TIMEOUT); } double GPSLocation::lat (void) { double ret = rawLatData.deg + rawLatData.billionths / 1000000000.0; return rawLatData.negative ? -ret : ret; } double GPSLocation::lng (void) { double ret = rawLngData.deg + rawLngData.billionths / 1000000000.0; return rawLngData.negative ? -ret : ret; } void GPSDate::commit (void) { if (have_newval) { have_newval = false; date = newval; validTimeout = get_timeout(GPS_TIMEOUT); } } void GPSTime::commit (void) { if (have_newval) { have_newval = false; time = newval; validTimeout = get_timeout(GPS_TIMEOUT); } } bool GPSTime::getHMS (byte dest[3]) { byte h, m, s; ulong adjust; if (is_valid()) { h = hour(); m = minute(); s = second(); adjust = (((ulong)(1 + millis())) - (validTimeout - GPS_TIMEOUT)) / 1000lu; #if NTP_ENABLED } else if (ntp_time_is_valid) { h = ntp_time.hh; m = ntp_time.mm; s = ntp_time.ss; adjust = (((ulong)(1 + millis())) - ntp_time_is_valid) / 1000lu; #endif /* NTP_ENABLED */ } else { dest[2] = dest[1] = dest[0] = 0; return false; } // It could be several seconds since last non-corrupted GPS/NTP time update. // So if feasible, adjust the returned time for better accuracy: s += adjust; while (s >= 60) { if (h == 23 && m == 59) { s = 59; // Don't even attempt to handle date changes when crossing midnight: too complex. #if NTP_ENABLED if (!is_valid()) ntp_fast_update_secs = 1; #endif /* NTP_ENABLED */ } else { s -= 60; if (++m >= 60) { m -= 60; h += 1; } } } dest[0] = h; dest[1] = m; dest[2] = s; return true; } uint16_t GPSDate::year() { return (date % 100) + 2000; } uint8_t GPSDate::month() { return (date / 100) % 100; } uint8_t GPSDate::day() { return date / 10000; } uint8_t GPSTime::hour() { return time / 1000000; } uint8_t GPSTime::minute() { return (time / 10000) % 100; } uint8_t GPSTime::second() { return (time / 100) % 100; } void GPSInteger::set(const char *term) { newval = atol(term); have_newval = true;} void GPSInteger::cond_set2 (const char *term) { uint32_t v = 0; if (get_digits(term, 2, &v, false)) { if (!have_newval || v > newval) { have_newval = true; newval = v; } } } void GPSInteger::commit (void) { if (have_newval) { have_newval = false; val = newval; validTimeout = get_timeout(GPS_TIMEOUT); } } void GPSPlus::invalidate (void) { date.validTimeout = 0; time.validTimeout = 0; location.validTimeout = 0; satellites.validTimeout = 0; satellitesInViewGP.validTimeout = 0; satellitesInViewGL.validTimeout = 0; satellitesInViewGQ.validTimeout = 0; satellitesInViewGA.validTimeout = 0; satellitesInViewGB.validTimeout = 0; } // *** End TinyGPS++ ************************************************************************************************************ static GPSPlus gps; static ulong gps_uart_baud = 0; static long gps_last_baud_change = 0; /* * There is only one HardwareSerial port availble for GPS, MUSB, or AUXRELAY. * If AUXRELAY is enabled, then it gets that port, and MUSB is disabled, * and GPS uses a SoftwareSerial port instead. * * If no AUXRELAY, and MUSB is selected at boot, then MUSB gets the HardwareSerial port, * and GPS again uses a SoftwareSerial port instead. * * Otherwise, GPS uses the HardwareSerial port (normal mode of operation). */ #include SoftwareSerial swserial0; static bool gps_using_swserial; #define SWSERIAL_bufCapacity 96 // library default is 64; WARNING: uses a LOT of memory! static void GPS_UART_WRITE (byte b) { if (GPS_TX_PIN != -1) {gps_using_swserial ? swserial0.write(b) : hwserial1.write(b);} } #ifndef NMEA_playback_h static byte GPS_UART_READ (void) { return gps_using_swserial ? swserial0.read() : hwserial1.read(); } static int GPS_UART_AVAILABLE (void) { return gps_using_swserial ? swserial0.available() : hwserial1.available(); } #endif static void GPS_UART_FLUSH (void) { gps_using_swserial ? swserial0.flush() : hwserial1.flush(false);} static const char *GPS_UART_NAME (void) { return gps_using_swserial ? "swserial0" : "hwserial1"; } static void GPS_UART_BEGIN (ulong speed) { if (gps_using_swserial) { swserial0.begin(speed, SWSERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN, false, SWSERIAL_bufCapacity); } else { hwserial1.setRxBufferSize(1024); hwserial1.begin(speed, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); } } static void GPS_UART_SET_BAUD (ulong baud) { gps_uart_baud = baud; gps_last_baud_change = millis(); if (verbose || gps_debug || GPS_debug) SERIAL_PRINTF("GPS_UART_SET_BAUD(%lu)\r\n", gps_uart_baud); if (gps_using_swserial) { swserial0.end(); swserial0.begin(baud, SWSERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN, false, SWSERIAL_bufCapacity); } else { hwserial1.updateBaudRate(baud); } } // For more details how convert GPS position into 24 bit format, // see "NexStar Communication Protocol", section "GPS Commands". // https://www.nexstarsite.com/download/manuals/NexStarCommunicationProtocolV1.2.zip static const double GPS_MULT_FACTOR = 46603.37778; // = 2^24 / 360 static bool gps_has_date_time = false; static bool gps_has_fix = false; static bool gps_has_recent_location = false; static double gps_recent_lat = 0; static double gps_recent_lng = 0; static double gps_saved_lat = 0; static double gps_saved_lng = 0; static bool gps_use_saved_location = false; static bool gps_location_force = false; static bool gps_has_saved_location (void) { return (gps_saved_lat < (BAD_FLOAT_VAL - 10.0) && gps_saved_lng < (BAD_FLOAT_VAL - 10.0)); } enum {no_fix, true_fix, recent_fix, saved_fix}; static const char *gps_fix_type[] = {"no_fix", "true_fix", "recent_fix", "saved_fix"}; static inline byte gps_get_fix_type (void) // for oled_location_updatefn() { if (gps_has_fix) return true_fix; if (gps_has_date_time || ntp_time_is_valid) { if (gps_has_recent_location) return recent_fix; if (gps_has_saved_location() && (gps_use_saved_location || gps_location_force)) return saved_fix; } return no_fix; } static byte gps_get_lat_lng (bool do_mult_factor, double *lat_p, double *lng_p) { double lat, lng; byte fix_type = no_fix; if (gps_has_fix) { lat = gps.location.lat(); lng = gps.location.lng(); fix_type = true_fix; } else if (gps_has_date_time || ntp_time_is_valid) { if (gps_has_recent_location) { lat = gps_recent_lat; lng = gps_recent_lng; fix_type = recent_fix; } else if (gps_has_saved_location() && (gps_use_saved_location || gps_location_force)) { lat = gps_saved_lat; lng = gps_saved_lng; fix_type = saved_fix; } } if (fix_type != no_fix) { if (lat_p) *lat_p = do_mult_factor ? lat * GPS_MULT_FACTOR : lat; if (lng_p) *lng_p = do_mult_factor ? lng * GPS_MULT_FACTOR : lng; } return fix_type; } static void fakegps_command (char *cmd) { if (2 == sscanf(cmd, "%lf %lf", &gps_saved_lat, &gps_saved_lng)) { gps_detected = true; gps_location_force = true; nvram_save_int("gps.location.force", gps_location_force); nvram_save_float("gps.location.lat", gps_saved_lat); nvram_save_float("gps.location.lng", gps_saved_lng); SERIAL_PRINTF("fakegps: lat=%lf lng=%lf fix_type=%u\r\n", gps_saved_lat, gps_saved_lng, gps_get_fix_type()); } else { gps_saved_lat = BAD_FLOAT_VAL; gps_saved_lng = BAD_FLOAT_VAL; gps_detected = false; // A real GPS will turn this right back on again gps_location_force = 0; nvram_erase_val("gps.location.force"); nvram_erase_val("gps.location.lat"); nvram_erase_val("gps.location.lng"); SERIAL_PRINTLN("fakegps data cleared"); } } static void gps_save_location (void) { double lat, lng; byte fix_type = gps_get_lat_lng(false, &lat, &lng); if (fix_type == true_fix || recent_fix) { gps_saved_lat = lat; gps_saved_lng = lng; nvram_save_float("gps.location.lat", gps_saved_lat); nvram_save_float("gps.location.lng", gps_saved_lng); } else { gps_saved_lat = BAD_FLOAT_VAL; gps_saved_lng = BAD_FLOAT_VAL; nvram_erase_val("gps.location.lng"); nvram_erase_val("gps.location.lng"); } gps_use_saved_location = false; } static void gps_restore_location (void) { gps_use_saved_location = gps_has_saved_location(); } static void gps_handle_request (struct rxbuf_s *rxbuf, byte *data) { byte reply[AUXBUS_PKT_MAX], len, op = data[4]; #if NTP_ENABLED // TO-DO: pass NTP date/time to the GPS receiver to help it get a fix earlier? if (!gps_has_date_time) ntp_fast_update_secs = 60; #endif /* NTP_ENABLED */ len = emulate_begin("gps_rx", data, reply); switch (OP(op,data[1])) { case OP(GPS_TIME_VALID,3): { bool has_date_time = gps_has_date_time || ntp_time_is_valid; reply[len++] = has_date_time; if (gps_debug) SERIAL_PRINTF("GPS TIME_VALID: %s %02u/%02u\r\n", has_date_time ? "true" : "false", gps.satellites.value(), gps.satellitesInView()); break; } case OP(GPS_LINKED,3): { byte fix_type = gps_get_fix_type(); reply[len++] = (fix_type != no_fix); if (gps_debug) SERIAL_PRINTF("GPS LINKED: %s %02u/%02u\r\n", gps_fix_type[fix_type], gps.satellites.value(), gps.satellitesInView()); break; } case OP(GPS_GET_TIME,3): { gps.time.getHMS(reply + len); len += 3; if (gps_debug) SERIAL_PRINTLN("GPS GET_TIME"); break; } case OP(GPS_GET_HW_VER,3): { reply[len++] = 0xeb; // Homebrew GPS hardware version, to avoid "mount confusion" in CFM if (gps_debug) SERIAL_PRINTLN("GPS GET_HW_VER"); break; } case OP(GPS_GET_YEAR,3): { if (gps_has_date_time || !ntp_time_is_valid) { uint16_t year = gps.date.year(); reply[len++] = year >> 8; reply[len++] = year; #if NTP_ENABLED } else { reply[len++] = ntp_time.year >> 8; reply[len++] = ntp_time.year; #endif /* NTP_ENABLED */ } if (gps_debug) SERIAL_PRINTLN("GPS GET_YEAR"); break; } case OP(GPS_GET_DATE,3): { if (gps_has_date_time || !ntp_time_is_valid) { reply[len++] = gps.date.month(); // 01..12 reply[len++] = gps.date.day(); // 01..32 #if NTP_ENABLED } else { reply[len++] = ntp_time.month; reply[len++] = ntp_time.day; #endif /* NTP_ENABLED */ } if (gps_debug) SERIAL_PRINTLN("GPS GET_DATE"); break; } case OP(GPS_GET_LAT,3): { double lat; gps_get_lat_lng(true, &lat, NULL); int32_t ilat = lat + ((lat > 0) ? 0.499999 : -0.499999); uint8_t* latBytePtr = (uint8_t*)&ilat; reply[len++] = latBytePtr[2]; reply[len++] = latBytePtr[1]; reply[len++] = latBytePtr[0]; if (gps_debug) SERIAL_PRINTLN("GPS GET_LAT"); break; } case OP(GPS_GET_LNG,3): { double lng; gps_get_lat_lng(true, NULL, &lng); int32_t ilng = lng + ((lng > 0) ? 0.499999 : -0.499999); uint8_t* lngBytePtr = (uint8_t*)&ilng; reply[len++] = lngBytePtr[2]; reply[len++] = lngBytePtr[1]; reply[len++] = lngBytePtr[0]; if (gps_debug) SERIAL_PRINTLN("GPS GET_LNG"); break; } case OP(GPS_GET_SAT_INFO,3): { reply[len++] = gps.satellitesInView(); reply[len++] = gps.satellites.value(); if (gps_debug) SERIAL_PRINTLN("GPS GET_SAT_INFO"); break; } case OP(DEV_GET_VERSION,3): { reply[len++] = 2; // Version 2.0 reply[len++] = 0; if (gps_debug) SERIAL_PRINTLN("GPS GET_VERSION"); break; } case OP(GPS_GET_RCVR_STATUS,3): { reply[len++] = gps_has_fix ? 0xe0 : 0x60; // "3D Fix", or "Acquiring Satellites" reply[len++] = gps_has_fix ? 0x20 : 0x80; // "Position Lock", or "Cold Start" if (gps_debug) SERIAL_PRINTLN("GPS_GET_RCVR_STATUS"); break; } default: if (!auxbus_raw_tracing) SERIAL_PRINTF("GPS OP-0x%02x len=%u (unknown)\r\n", op, data[1]); } emulate_send_reply("gps_tx", rxbuf, reply, len); } static inline byte gps_dump_lbuf (char *lbuf, byte lcnt) { if (lcnt && GPS_debug) { lbuf[lcnt] = 0; SERIAL_PRINTF("GPS: %s\r\n", lbuf); } return 0; } static void gps_receive (void) { static char lbuf[128]; static byte lcnt = 0; static bool baudrate_ok = false; static byte nmea_successes = 0; static uint16_t rxcount = 0; while (GPS_UART_AVAILABLE()) { char c = GPS_UART_READ(); if (++rxcount > 31 && !gps_detected) { // Early detection (even if wrong baudrate), needed for SSHC gps_detected = true; SERIAL_PRINTLN("GPS detected."); } if (gps.encode(c) && !baudrate_ok && ++nmea_successes > 3) { baudrate_ok = true; SERIAL_PRINTF("GPS baud %lu.\r\n", gps_uart_baud); } if (c != '\r' && c != '\n') lbuf[lcnt++] = (c >= ' ' && c <= '~') ? c : '.'; if (c == '\n' || lcnt == (sizeof(lbuf) - 1)) lcnt = gps_dump_lbuf(lbuf, lcnt); } // Auto-switch baud rate: the BE-xxx series default to 38400 instead of 9600. if (gps_detected && !baudrate_ok) { if ((rxcount > 200 && !nmea_successes) || time_after(millis(), gps_last_baud_change + 1500)) { lcnt = gps_dump_lbuf(lbuf, lcnt); rxcount = 0; nmea_successes = 0; gps.encode(0); GPS_UART_SET_BAUD((gps_uart_baud == 9600) ? 38400 : 9600); // updates gps_last_baud_change delay(5); // short delay needed to guarantee flushing data from before baudrate change GPS_UART_FLUSH(); } } } static void gps_loop (void) { bool old_fix = gps_has_fix; gps_receive(); gps_has_date_time = gps.date.is_valid() && gps.time.is_valid(); gps_has_fix = gps_has_date_time && gps.location.is_valid(); if (gps_has_fix) { gps_recent_lat = gps.location.lat(); gps_recent_lng = gps.location.lng(); gps_has_recent_location = true; } if (gps_has_fix != old_fix) { old_fix = gps_has_fix; if (gps_debug) SERIAL_PRINTF("GPS Fix: %s\r\n", gps_has_fix ? "true" : "false"); } } #endif /* EMULATE_GPS */ /************************** Begin SSAG StarSense Auto Guider Emulation **************************/ #if EMULATE_SSAG static void ssag_handle_request (struct rxbuf_s *rxbuf, byte *data) { static long cmd_timer = 0; byte reply[AUXBUS_PKT_MAX], len, op = data[4]; len = emulate_begin("ssag_rx", data, reply); switch (OP(op,data[1])) { case OP(DEV_GET_VERSION,3): { reply[len++] = 35; // Version 35.10.30 reply[len++] = 10; reply[len++] = 30; break; } case OP(SSAG_START_PLATE_SOLVE,3): { cmd_timer = get_timeout(4000); break; } case OP(SSAG_GET_PLATE_STATUS,3): { // 9-bytes in: 00..=inProgress; otherwise 01.. : PlateSolveResult(SOLVED, RaDec(155.184971527816 degs, 69.1558292709192 degs)) 01-40-2D-57-E1-3F-9A-7E-E0 if (cmd_timer && time_before(millis(), cmd_timer)) { reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; } else { cmd_timer = 0; reply[len++] = 0x01; // Ideally this should be different for each of three runs reply[len++] = 0x40; reply[len++] = 0xa5; reply[len++] = 0x88; reply[len++] = 0x57; reply[len++] = 0x3e; reply[len++] = 0x4a; reply[len++] = 0x9e; reply[len++] = 0x66; } break; } case OP(SSAG_START_CALIBRATE,3): { // 8-bytes out: RaDec(213.908604168867 degs, 19.1693596951982 degs) 40-6E-F0-2E-3E-AB-4C-8E cmd_timer = get_timeout(4000); break; } case OP(SSAG_GET_CALIBRATE_STATUS,8): { // 5-bytes in: 00..=inProgress; otherwise 01/ff xx.. : CenterCalibrationStatus(SOLVED, x=65134, y=64192) 01-FE-6E-FA-C0 if (cmd_timer && time_before(millis(), cmd_timer)) { reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; } else { cmd_timer = 0; reply[len++] = 0x01; reply[len++] = 0xff; reply[len++] = 0x51; reply[len++] = 0xfd; reply[len++] = 0xc6; } break; } case OP(SSAG_07,3): { // 1-byte in (00,01) reply[len++] = 0x01; break; } case OP(SSAG_SET_GUIDING,4): { // 1-byte out: 00=disabled; 69: west=True altaz=True break; } case OP(SSAG_GET_GUIDING_DATA,13): { // 10-bytes in: Error(0, 0) Correction(0, 0) MoveRa = False MoveDec = False StopAll = TrueGuideStars = 0Quality = 0 00-00-00-00-91-00-00-00-00-00 reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x91; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; reply[len++] = 0x00; break; } case OP(SSAG_SET_MOUNT_INFO,11): { // 8-bytes out: mountModel = 4, isAltAz = True, azmFirmwareVersion = [40, 99] 04-01-28-63-00-00 break; } case OP(SSAG_SET_GEOLOCATION,11): { // 8-bytes out: location = [66, 34, 203, 75, 194, 149, 204, 45] 42-22-CB-4B-C2-95-CC-2D break; } case OP(SSAG_SET_TIME_NOW,7): { // 4-bytes out: time = [44, 137, 45, 19] 2C-89-2D-13 (2023-09-04 20:04:35.701 -04:00) break; } case OP(SSAG_45,3): { // 4-bytes in, sometimes all zeros when SSAG is still solving latest plate. "get pointing error??" reply[len++] = 0x42; reply[len++] = 0xe1; reply[len++] = 0x79; reply[len++] = 0x0e; break; } default: if (!auxbus_raw_tracing) SERIAL_PRINTF("SSAG OP-0x%02x len=%u (unknown)\r\n", op, data[1]); } emulate_send_reply("ssag_tx", rxbuf, reply, len); } #endif /************************** End SSAG StarSense Auto Guider Emulation **************************/ /************************** Begin SSAA StarSense Auto Align Accessory/Camera Simulator *****************/ #if EMULATE_SSAA // Database of 100 real stars, which are randomly assembled into plates. Plate solving will fail, but one can at least test packet flow! static struct star_s { byte xy[8]; } stars[100] = { {0x90,0x80,0x86,0x43,0x4f,0x11,0x48,0x44}, {0xb3,0x70,0x71,0x44,0xfb,0x58,0x1c,0x44}, {0xb3,0xe0,0x2a,0x44,0x72,0x10,0x36,0x44}, {0xc0,0x14,0xfe,0x43,0xb3,0xb4,0xc4,0x43}, {0x37,0x2e,0x93,0x44,0x58,0x24,0x50,0x44}, {0x23,0x2e,0x51,0x43,0xfe,0xd1,0xf0,0x43}, {0x77,0x70,0x9b,0x44,0x5b,0x44,0x0e,0x44}, {0x19,0x35,0x59,0x44,0xd5,0x9e,0xbb,0x42}, {0x74,0xdb,0xf6,0x41,0x5f,0xb6,0x5a,0x44}, {0x3e,0xff,0x2e,0x43,0x9b,0x9d,0x5d,0x44}, {0xa8,0xa5,0x72,0x44,0x43,0x3f,0x38,0x44}, {0x58,0xfb,0x39,0x44,0xf5,0x08,0x5b,0x44}, {0x44,0xcb,0x3f,0x43,0x06,0x15,0x2e,0x44}, {0x66,0xca,0x8a,0x44,0x0c,0x84,0x4d,0x44}, {0x92,0x89,0x39,0x44,0x9a,0x14,0x1b,0x44}, {0x51,0xa1,0xe9,0x43,0xaf,0xa5,0x48,0x42}, {0xad,0x8f,0x2f,0x44,0x0c,0xbc,0x2d,0x44}, {0x58,0xe9,0x8f,0x44,0x92,0xde,0x4f,0x43}, {0xbd,0x31,0x8c,0x44,0xf0,0x17,0xe2,0x42}, {0x91,0x3f,0x33,0x43,0xee,0xbf,0x54,0x44}, {0x53,0x88,0x83,0x44,0x2c,0xbb,0x0b,0x44}, {0x35,0x81,0x59,0x43,0x76,0xc3,0x4f,0x44}, {0xce,0x35,0x99,0x44,0xa1,0x81,0x6b,0x43}, {0x0e,0x2f,0xf8,0x43,0xbf,0x26,0xb7,0x43}, {0xb3,0x1c,0x1c,0x43,0xf1,0x64,0xc0,0x43}, {0xf9,0x6c,0x16,0x44,0xa1,0x55,0xb3,0x43}, {0xb5,0xfb,0x2e,0x44,0xd0,0x75,0x6c,0x44}, {0x89,0x4b,0x79,0x44,0x1b,0x95,0x6b,0x44}, {0x20,0x3a,0x3d,0x44,0xe0,0x74,0x4d,0x44}, {0x04,0xd8,0x74,0x44,0x67,0xed,0x52,0x43}, {0xc1,0xb2,0x44,0x44,0xe7,0x45,0xbf,0x43}, {0xe2,0xd1,0x73,0x44,0xee,0x63,0xe2,0x43}, {0xc8,0x8b,0xa6,0x43,0x8d,0x43,0x2f,0x44}, {0x95,0x9b,0xe6,0x43,0xab,0x3c,0x33,0x44}, {0xf1,0xfa,0xbc,0x43,0x6a,0x03,0xe7,0x43}, {0x0e,0x14,0x83,0x44,0xfd,0x24,0x5f,0x44}, {0xe8,0x69,0x83,0x44,0xe7,0x4c,0x05,0x43}, {0x3e,0xcb,0x93,0x44,0x49,0x41,0xb9,0x43}, {0x7f,0x6a,0x8d,0x44,0x69,0xd6,0xc9,0x42}, {0xe6,0xae,0x6e,0x44,0x4f,0x32,0xd9,0x43}, {0xb6,0x6d,0x1a,0x44,0xe9,0x60,0x01,0x44}, {0x57,0x5a,0x4c,0x44,0x9b,0x59,0x1f,0x43}, {0x1d,0x41,0xa0,0x43,0x2a,0x0c,0xfb,0x43}, {0x6c,0xed,0x66,0x43,0x56,0xdd,0xc9,0x41}, {0xa1,0xf2,0x5e,0x44,0x7f,0xec,0x67,0x43}, {0x43,0xd6,0xd4,0x42,0x85,0xea,0x9e,0x43}, {0xcf,0xa6,0x90,0x44,0xe7,0x25,0x36,0x44}, {0x8d,0xe9,0x78,0x44,0x9c,0x42,0x4f,0x44}, {0x07,0x11,0x93,0x44,0x7b,0x40,0x8a,0x42}, {0xa9,0x00,0x00,0x44,0x16,0x6f,0x63,0x44}, {0x31,0x88,0xdd,0x43,0x67,0x2d,0x48,0x43}, {0x3b,0x50,0xc2,0x43,0x24,0xcc,0xc5,0x43}, {0xd5,0x14,0x0c,0x44,0xc7,0x5d,0x32,0x43}, {0x08,0xde,0x51,0x44,0x4f,0xe6,0x3c,0x44}, {0x98,0x09,0xa4,0x43,0x27,0x41,0x5f,0x44}, {0x98,0x07,0x81,0x43,0xd7,0xe5,0x80,0x43}, {0x05,0x7a,0x6b,0x44,0xfb,0x01,0x85,0x43}, {0xc0,0xed,0x06,0x44,0x71,0xfb,0x8a,0x43}, {0x1a,0x6c,0x1e,0x44,0x7b,0x8d,0x2c,0x43}, {0xcc,0x6b,0x6d,0x44,0x63,0xc7,0xbc,0x43}, {0xb3,0x11,0x31,0x44,0x53,0x5a,0x6c,0x44}, {0xb7,0x09,0x4b,0x44,0x19,0x43,0x21,0x44}, {0xe2,0xa4,0x10,0x44,0x1a,0xda,0x25,0x44}, {0x4a,0x76,0xe3,0x43,0xa4,0xb4,0x66,0x44}, {0xe7,0x4d,0x71,0x44,0xa3,0xde,0x2e,0x44}, {0xd8,0x08,0x97,0x44,0x46,0x52,0x1e,0x44}, {0xf0,0x54,0x53,0x44,0xd0,0xc3,0x9a,0x42}, {0xc7,0xd0,0x5a,0x44,0x53,0x14,0x36,0x44}, {0x38,0x96,0x90,0x42,0x91,0x88,0xdb,0x43}, {0x98,0xa3,0x8d,0x43,0x8d,0xa2,0x5d,0x44}, {0xb7,0xd4,0xd4,0x42,0xf8,0xb4,0xe6,0x43}, {0xad,0x78,0x7a,0x42,0x5d,0x1a,0xa7,0x43}, {0x6c,0x07,0x2e,0x44,0x3c,0x9e,0x49,0x44}, {0xec,0xdc,0x7b,0x43,0xd3,0x28,0x27,0x44}, {0x7b,0x21,0xe8,0x42,0x23,0x63,0x3a,0x44}, {0x2e,0xfa,0xb2,0x43,0x9d,0x15,0x94,0x43}, {0xa4,0xe8,0x65,0x44,0x6d,0x70,0xbb,0x43}, {0x8d,0x07,0x31,0x44,0xd8,0x85,0x64,0x44}, {0x45,0x6f,0x6a,0x42,0xbc,0xfa,0xa5,0x43}, {0x42,0xa7,0x16,0x44,0xa5,0xca,0x4a,0x43}, {0x8f,0x6f,0x6c,0x43,0xd5,0x01,0xd1,0x43}, {0x2c,0xa4,0x75,0x44,0xd6,0xf4,0x6c,0x44}, {0x09,0x87,0xca,0x42,0x01,0xc0,0x02,0x44}, {0x46,0xa2,0xc4,0x43,0xeb,0xd7,0x8a,0x43}, {0x55,0x2a,0x4d,0x44,0xa9,0x98,0x3a,0x44}, {0xf0,0x3e,0xe3,0x43,0x1c,0x59,0x17,0x44}, {0xd6,0xa2,0xb2,0x43,0x7a,0x38,0xc9,0x43}, {0x71,0x93,0x8d,0x43,0x05,0x4d,0x40,0x44}, {0x76,0x24,0x1d,0x44,0x6c,0xda,0x39,0x44}, {0xd1,0x96,0x4f,0x44,0x5e,0xd2,0x41,0x43}, {0x69,0xf6,0x04,0x44,0x9c,0x44,0x6a,0x44}, {0x99,0x98,0x66,0x43,0x40,0x01,0x2c,0x44}, {0x3f,0x34,0x6a,0x42,0xaa,0x32,0x9a,0x42}, {0x8f,0x6e,0x6a,0x42,0x31,0x4a,0x3f,0x44}, {0x97,0x82,0x46,0x44,0x5a,0xaf,0x32,0x44}, {0xb3,0x02,0xe2,0x43,0x00,0x40,0x33,0x44}, {0x54,0x13,0x29,0x44,0xd2,0x91,0x1b,0x44}, {0xdc,0x6a,0x7d,0x43,0x44,0xa2,0x31,0x44}, {0x01,0x00,0x6a,0x42,0x30,0x66,0x29,0x44}, {0x57,0x14,0x29,0x44,0xd5,0x92,0x1b,0x44} }; static byte *ssaa_genpkt_emit (byte *p, byte b) { *p++ = b; if (b == 0x3b) { *p++ = 0x02; *p++ = 0x02; } return p; } static byte ssaa_random_index (bool used[]) { byte s = micros() % 100; while (used[s]) s = (s + 1) % 100; used[s] = true; return s; } static uint16_t ssaa_genpkt (bool do_random, byte *pkt, byte *nstars_p) { start_again: byte nonrandom = 0; bool used[100] = {false,}; uint16_t real_stars = do_random ? micros() % 100 + 1 : 99; uint16_t zero_stars = do_random ? 0 : 1; uint16_t total_stars = real_stars + zero_stars; uint16_t zpadding = 0; byte csum = 0, *p = pkt; ulong len = total_stars * sizeof(struct star_s); // 8-bytes per star while ((len & 0xff00) == 0x3b00) { // 0x3b not permitted in length field, so make it longer with extra zeroes at the end len += sizeof(struct star_s); zero_stars++; } total_stars = real_stars + zero_stars; if (total_stars > 100) { do { if (zero_stars) zero_stars--; else if (real_stars) real_stars--; else goto start_again; len -= sizeof(struct star_s); } while ((len & 0xff00) == 0x3b00); // 0x3b not permitted in length field, so make it longer with extra zeroes at the end total_stars = real_stars + zero_stars; } *p++ = 0x3c; // Protocol identifier *p++ = len >> 24; // 32-bit payload length, exclusive of itself, 0x3c, any padding, and final checksum byte csum += len >> 24; *p++ = len >> 16; csum += len >> 16; *p++ = len >> 8; csum += len >> 8; *p++ = len; csum += len; while (real_stars--) { // 8-bytes per star, possibly with 2 padding bytes (0x02 0x02) after any 0x3b bytes within byte s = do_random ? ssaa_random_index(used) : nonrandom++; byte *xy = stars[s].xy; for (byte i = 0; i < 8; ++i) { csum += xy[i]; p = ssaa_genpkt_emit(p, xy[i]); } } zero_stars *= sizeof(struct star_s); while (zero_stars--) *p++ = 0x00; *p++ = 0 - csum; *nstars_p = total_stars; return (uint16_t)(p - pkt); } static inline uint16_t make_uint16 (byte msb, byte lsb) { return (((uint16_t)msb) << 8) | lsb; } static void ssaa_handle_request (struct rxbuf_s *rxbuf, byte *data) { static byte ssaa_image_nstars = 0; static uint16_t ssaa_image_len = 0; static bool do_random = false; // first plate is always real; the rest are random #define SSAA_NUM_PROFILES 3 static struct ssaa_profile_s { uint16_t x_coord; uint16_t x_extra; uint16_t y_coord; uint16_t y_extra; } ssaa_profiles[SSAA_NUM_PROFILES] = {{640,0,480,0}, {640,0,480,0}, {640,0,480,0}}; static uint16_t ssaa_x_coord = 640, ssaa_y_coord = 480; static enum {ssaa_idle = 1, ssaa_captured = 3, ssaa_finish = 9} ssaa_status = ssaa_idle; static byte plate_index = 0; byte reply[AUXBUS_PKT_MAX], len, op = data[4]; len = emulate_begin("ssaa_rx", data, reply); switch (OP(op,data[1])) { case OP(DEV_GET_MODEL,3): { reply[len++] = 0x01; do_random = false; // for first plate solve of session SERIAL_PRINTLN("SSAA GET_MODEL"); break; } case OP(DEV_GET_VERSION,3): { len += long_to_4bytes(reply + len, 0x0102341f); // version 1.2.13343 do_random = false; // for first plate solve of session SERIAL_PRINTLN("SSAA GET_VERSION"); break; } case OP(SSAA_CAPTURE_BEGIN,6): { /* 3 data bytes: 0x03 0xe8 (1000, gain?), and either 0x00, 0x01, or 0x02 for the final byte */ ssaa_status = ssaa_captured; plate_index = data[7]; // ?? if (!ssaa_image_pkt) ssaa_image_pkt = (byte *)malloc(AUXBUS_SSAA_PKT_MAX); if (!ssaa_image_pkt) { SERIAL_PRINTF("%s: no memory\r\n", __func__); } else { ssaa_image_len = ssaa_genpkt(do_random, ssaa_image_pkt, &ssaa_image_nstars); // "Capture an image" SERIAL_PRINTF("SSAA CAPTURE_BEGIN %u, %u stars (%s)\r\n", plate_index, ssaa_image_nstars, do_random ? "random" : "real"); do_random = true; // for all subsequent plates in this session } break; } case OP(SSAA_CAPTURE_GET_STATUS,3): { const char *msg = ""; reply[len++] = ssaa_status; switch (ssaa_status) { case ssaa_idle: { reply[len++] = 0x00; msg = "Idle"; break; } case ssaa_captured: { reply[len++] = (plate_index % 3) ? ssaa_image_nstars : 0x00; // Number of stars found? msg = "Capturing"; break; } case ssaa_finish: { reply[len++] = ssaa_image_nstars; msg = "Capture done"; break; } } SERIAL_PRINTF("SSAA CAPTURE_GET_STATUS %s\r\n", msg); break; } case OP(SSAA_CAPTURE_GET_PLATE,3): { SERIAL_PRINTLN("SSAA GET_PLATE"); if (!ssaa_image_pkt || ssaa_image_len == 0 || ssaa_status == ssaa_idle) break; // Unable to get a plate right now reply[len++] = 0x09; reply[len++] = ssaa_image_nstars; emulate_send_reply("ssaa_tx", rxbuf, reply, len); if (ssaa_image_len != 0) { if ((plate_index % 3) == 2) ssaa_status = ssaa_idle; SERIAL_PRINTF("SSAA GET_PLATE %u stars\r\n", ssaa_image_nstars); emulate_send_reply("ssaa_tx", rxbuf, ssaa_image_pkt, ssaa_image_len); ssaa_image_pkt = NULL; // bus_tx() will free the memory } return; } case OP(SSAA_CAPTURE_GET_RESULT,4): { ssaa_status = ssaa_finish; plate_index = data[5]; reply[len++] = 0x09; reply[len++] = ssaa_image_nstars; SERIAL_PRINTLN("SSAA GET_RESULT"); break; } case OP(SSAA_SET_PROFILE,12): { struct ssaa_profile_s *p = &ssaa_profiles[data[13]]; p->x_coord = make_uint16(data[ 6], data[ 5]); p->x_extra = make_uint16(data[ 8], data[ 7]); p->y_coord = make_uint16(data[10], data[ 9]); p->y_extra = make_uint16(data[12], data[11]); reply[len++] = 0x11; // SSAA always returns this here, regardless of slot reply[len++] = 0x00; // SSAA always returns this here, regardless of slot SERIAL_PRINTF("SSAA SET_PROFILE %u %u,%u\r\n", data[13], p->x_coord, p->y_coord); break; } case OP(SSAA_GET_PROFILE,4): { struct ssaa_profile_s *p = &ssaa_profiles[data[5]]; reply[len++] = p->x_coord; reply[len++] = p->x_coord >> 8; reply[len++] = p->x_extra; reply[len++] = p->x_extra >> 8; reply[len++] = p->y_coord; reply[len++] = p->y_coord >> 8; reply[len++] = p->y_extra; reply[len++] = p->y_extra >> 8; SERIAL_PRINTF("SSAA GET_PROFILE %u %u,%u\r\n", data[5], p->x_coord, p->y_coord); break; } case OP(SSAA_AIS_RESET,3): { // aka. "CAPTURE_STOP" ssaa_status = ssaa_idle; ssaa_image_len = 0; if (ssaa_image_pkt) { free(ssaa_image_pkt); ssaa_image_pkt = NULL; } do_random = true; // for all subsequent plates in this session SERIAL_PRINTLN("SSAA AIS_RESET"); break; } default: if (!auxbus_raw_tracing) SERIAL_PRINTF("SSAA OP-0x%02x len=%u (unknown)\r\n", op, data[1]); } emulate_send_reply("ssaa_tx", rxbuf, reply, len); } #endif /* EMULATE_SSAA */ /************************** End SSAA StarSense Auto Align Accessory/Camera Simulator *****************/ /************************** Begin FRAM support *******************************************************/ #define FRAM_USE_FOR_NVRAM false // work-in-progress #define FRAM_I2C_ADDR 0x50 static bool fram_detected = false; #define FRAM_NVRAM_OFFSET 0 #define FRAM_STEPPER_POS 0x800 static bool i2c_detect_device (byte addr) { Wire.setClock(100000ul); Wire.beginTransmission(addr); if (addr == FRAM_I2C_ADDR) delay(40); // Some FRAM chips need at least 37msecs of delay here. else Wire.write(0x00); // Some models of Nunchuck require this here for successful detection. return (Wire.endTransmission() == 0); } static void fram_setup (void) { fram_detected = i2c_detect_device(FRAM_I2C_ADDR); } static void fram_write (uint16_t addr, void *datap, uint16_t len) { if (!fram_detected) return; //if (verbose) SERIAL_PRINTF("%s: addr=0x%x len=%lu\r\n", __func__, addr, len); Wire.setClock(400000ul); byte *data = (byte *)datap; while (len) { uint16_t this_len = (len > 128) ? 128 : len; // Break up transfer to prevent WiFi interrupt errors len -= this_len; Wire.beginTransmission(FRAM_I2C_ADDR); Wire.write((byte)(addr >> 8)); Wire.write((byte)(addr )); addr += this_len; while (this_len-- > 0) Wire.write(*data++); Wire.endTransmission(); } } static bool fram_read (uint16_t addr, void *datap, uint16_t len) { if (!fram_detected) return false; //if (verbose) SERIAL_PRINTF("%s: addr=0x%x len=%lu\r\n", __func__, addr, len); Wire.setClock(400000ul); byte *data = (byte *)datap; while (len) { uint16_t this_len = (len > 128) ? 128 : len; // Break up transfer to prevent WiFi interrupt errors len -= this_len; Wire.beginTransmission(FRAM_I2C_ADDR); Wire.write((byte)(addr >> 8)); Wire.write((byte)(addr )); addr += this_len; Wire.endTransmission(); Wire.requestFrom(FRAM_I2C_ADDR, this_len); while (this_len-- > 0) *data++ = Wire.read(); } return true; } /************************** End FRAM support *******************************************************/ /************************** Begin Stepper motor emulation of Celestron Focus Motor *************************/ #if EMULATE_FOCUS #define FOCUS_ZERO_POS 30000ul // "zero position" for positive-only values; use +/- around zero #define FOCUS_ONE_ROTATION (200 * stepper_microsteps) #define FOCUS_MAX_TRAVEL (FOCUS_ZERO_POS) #define FOCUS_LIMIT_MIN ((ulong)((FOCUS_ZERO_POS) - (FOCUS_MAX_TRAVEL))) #define FOCUS_LIMIT_MAX ((ulong)((FOCUS_ZERO_POS) + (FOCUS_MAX_TRAVEL))) static bool focus_calibrated = false; // saved in NVRAM static ulong focus_limit_min = FOCUS_LIMIT_MIN; // saved in NVRAM static ulong focus_limit_max = FOCUS_LIMIT_MAX; // saved in NVRAM static bool focus_debug = false; static long focus_fake_calibration_running = 0; static byte focus_backlash_pos = 0; // saved in NVRAM static byte focus_backlash_neg = 0; // saved in NVRAM static long focus_backlash_timeout = 0; // Used by backlash compensation logic static byte focus_prev_move_dev = 0; // Used by backlash compensation logic static byte focus_prev_move_op = 0; // Used by backlash compensation logic static int focus_speed = 0; // Used by backlash compensation logic static long focus_target_pos = FOCUS_ZERO_POS; static bool stepper_is_stopping = false; static uint stepper_microsteps = 0; // Configurable using MS1/MS2/MS2 pins on STEP/DIR motor drivers static double focus_factor = 1.0; // saved in NVRAM static inline ulong STEPPER_TO_FOCUS_POS (long spos) { return (spos / focus_factor) + (long)FOCUS_ZERO_POS; } static inline long FOCUS_TO_STEPPER_POS (long fpos) { return (fpos - (long)FOCUS_ZERO_POS) * focus_factor; } #define STEPPER_DRIVER_NONE 0 #define STEPPER_DRIVER_STEPDIR 1 #define STEPPER_DRIVER_ULN2003 2 static const char *STEPPER_DRIVER_NAMES[] = {"(none)", "STEP/DIR", "ULN2003A"}; static byte stepper_driver = STEPPER_DRIVER_NONE; // Do not edit: auto-detected during setup() // For STEP/DIR, wire STEPPER_PIN_D to ground on the driver board. // For ULN2003, put 1K-ohm resistor between STEPPER_PIN_A and STEPPER_PIN_D on the driver board. // The absence of any such resistor means "no stepper motor connected". // These are the four pins used for either type of stepper motor: enum { STEPPER_PIN_A = 23, // (potential conflict with [Ethernet] SPI MOSI) STEPPER_PIN_B = 19, // (potential conflict with [Ethernet] SPI MISO) STEPPER_PIN_C = 18, // (potential conflict with [Ethernet] SPI SCLK) STEPPER_PIN_D = 5 // (potential conflict with [Ethernet] SPI SS/CS) }; // STEP/DIR driver chips use the pins like this: #define STEPDIR_STEP_PIN STEPPER_PIN_A #define STEPDIR_ENA_PIN STEPPER_PIN_B #define STEPDIR_DIR_PIN STEPPER_PIN_C // STEPDIR_DETECT_PIN STEPPER_PIN_D // Per above, GROUND this pin so HBG3 can auto-detect this style of motor driver chip. // ULN2003A driver chip uses the same pins like this instead: #define ULN2003_PINS STEPPER_PIN_A, STEPPER_PIN_C,STEPPER_PIN_B,STEPPER_PIN_D // For ULN2003 IN1,IN3,IN2,IN4, as well as GND to '-' and 5V to '+' static bool stepper_test_pins (byte out_pin, int out_val, byte in_pin, uint in_mode) { pinMode(in_pin, in_mode); pinMode(out_pin, OUTPUT); digitalWrite(out_pin, out_val); delay(1); return digitalRead(in_pin) == out_val; } static void stepper_disable_pins (void) { pinMode(STEPPER_PIN_A, INPUT); pinMode(STEPPER_PIN_B, INPUT); pinMode(STEPPER_PIN_C, INPUT); pinMode(STEPPER_PIN_D, INPUT); } static void stepper_autodetect (void) { stepper_disable_pins(); // Yes, this line is necessary here! // STEP/DIR chips should have PIN_D tied directly to GND: if (!stepper_test_pins(STEPPER_PIN_A, HIGH, STEPPER_PIN_D, INPUT_PULLUP)) { stepper_driver = STEPPER_DRIVER_STEPDIR; } else { // ULN2003 chips should have a 1K-ohm loopback resistor between PIN_A(IN1) and PIN_D(IN4): if (stepper_test_pins(STEPPER_PIN_A, LOW, STEPPER_PIN_D, INPUT) && stepper_test_pins(STEPPER_PIN_A, HIGH, STEPPER_PIN_D, INPUT) && stepper_test_pins(STEPPER_PIN_D, LOW, STEPPER_PIN_A, INPUT) && stepper_test_pins(STEPPER_PIN_D, HIGH, STEPPER_PIN_A, INPUT)) stepper_driver = STEPPER_DRIVER_ULN2003; } stepper_disable_pins(); if (stepper_driver == STEPPER_DRIVER_NONE) SERIAL_PRINTF("Stepper driver not detected, Focus Motor emulation disabled.\r\n", __func__); else SERIAL_PRINTF("Detected %s stepper driver, Focus Motor emulation enabled.\r\n", STEPPER_DRIVER_NAMES[stepper_driver]); } // STEP/DIR drivers use the FastAccelStepper library: #include static FastAccelStepperEngine stepdirEngine = FastAccelStepperEngine(); static FastAccelStepper *stepdir = NULL; // ULN2003 drivers use the AccelStepper library: #include static AccelStepper astepper(AccelStepper::HALF4WIRE, ULN2003_PINS, false); static AccelStepper *uln2003 = &astepper; static bool uln2003_enabled = false; static inline ulong stepper_maxSpeed (void) { return ((stepper_driver == STEPPER_DRIVER_STEPDIR) ? 200 : 125) * stepper_microsteps; } static inline ulong stepper_accel (void) { return ((stepper_driver == STEPPER_DRIVER_STEPDIR) ? 3 : 4) * stepper_maxSpeed(); } static void stepdir_set_microsteps (uint steps) { if (stepper_driver == STEPPER_DRIVER_STEPDIR) { if (steps && steps <= 64) stepper_microsteps = steps; else stepper_microsteps = 16; SERIAL_PRINTF("%s: %u\r\n", __func__, stepper_microsteps); } } static void stepper_doStop (void) { stepper_is_stopping = true; if (stepper_driver == STEPPER_DRIVER_STEPDIR) stepdir->stopMove(); else uln2003->stop(); } static bool stepper_stop (void) { if (!stepper_isRunning()) return false; if (!stepper_is_stopping) { stepper_doStop(); if (focus_debug) SERIAL_PRINTLN("FOCUS stepper_doStop"); } return true; } static void stepper_setSpeed (uint speed) { if (focus_debug) SERIAL_PRINTF("%s: maxspeed=%lu accel=%lu\r\n", __func__, speed, stepper_accel()); if (!speed) { stepper_stop(); } else if (stepper_driver == STEPPER_DRIVER_STEPDIR) { stepdir->setSpeedInHz(speed); stepdir->setAcceleration(stepper_accel()); } else { uln2003->setMaxSpeed(speed); uln2003->setAcceleration(stepper_accel()); } } // FastAccelStepper doesn't always STOP when told to, so give it some help: static inline void stepper_stop_assist (void) { static long stop_timeout = 0; if (!stepper_is_stopping) { stop_timeout = 0; } else if (stepper_getSpeed() == 0) { stepper_is_stopping = 0; stop_timeout = 0; } else if (!stop_timeout) { stop_timeout = get_timeout(200); } else if (time_after(millis(), stop_timeout)) { stop_timeout = get_timeout(100); if (focus_debug) SERIAL_PRINTLN("FOCUS Forcing STOP"); stepper_doStop(); } } static void stepper_moveTo (long pos) { stepper_is_stopping = false; if (stepper_driver == STEPPER_DRIVER_STEPDIR) { stepdir->moveTo(pos); } else { if (!uln2003_enabled) { uln2003_enabled = true; uln2003->enableOutputs(); } uln2003->moveTo(pos); } } static long stepper_getPos (void) { if (stepper_driver == STEPPER_DRIVER_STEPDIR) return stepdir->getCurrentPosition(); else return uln2003->currentPosition(); } static inline bool stepper_isRunning (void) { return (stepper_driver == STEPPER_DRIVER_STEPDIR) ? stepdir->isRunning() : uln2003->isRunning(); } static inline uint stepper_getSpeed (void) { if (stepper_driver == STEPPER_DRIVER_STEPDIR) return stepdir->getCurrentSpeedInMilliHz() / 1000; else return uln2003->speed(); } static uint stepper_set_speed_0_to_9 (byte from_dev, byte speed_0_to_9) { static const byte stepdir_speeds_nexstar[10] = {0,2,2,2,2,2,2,2,10,40}; // CPWI and hand-controller use only indexes 7,8,9 static const byte stepdir_speeds_nchuck [10] = {0,1,3,8,20,30,40,60,80,100}; // Nunchuck enables a much wider speed range static const byte uln2003_speeds_nexstar[10] = {0,2,2,2,2,2,2,10,30,100}; // CPWI and hand-controller use only indexes 7,8,9 static const byte uln2003_speeds_nchuck [10] = {0,2,5,15,30,60,70,85,100,125}; // Nunchuck enables a much wider speed range uint speed = 0; if (speed_0_to_9 == 0) { stepper_stop(); } else { if (NCHUCK_ENABLED && from_dev == DEV_ESP32) // from Nunchuck! speed = (stepper_driver == STEPPER_DRIVER_STEPDIR) ? stepdir_speeds_nchuck [speed_0_to_9] : uln2003_speeds_nchuck [speed_0_to_9]; else speed = (stepper_driver == STEPPER_DRIVER_STEPDIR) ? stepdir_speeds_nexstar[speed_0_to_9] : uln2003_speeds_nexstar[speed_0_to_9]; speed *= stepper_microsteps; stepper_setSpeed(speed); } return speed; } static void stepper_setup (void) { stepper_autodetect(); if (stepper_driver == STEPPER_DRIVER_NONE) return; long pos = stepper_restore_pos(); focus_target_pos = STEPPER_TO_FOCUS_POS(pos); if (stepper_driver == STEPPER_DRIVER_STEPDIR) { stepdir_set_microsteps(nvram_get_int("focus.microsteps")); stepdirEngine.init(); stepdir = stepdirEngine.stepperConnectToPin(STEPDIR_STEP_PIN, DRIVER_RMT); if (stepdir) { stepdir->setEnablePin (STEPDIR_ENA_PIN); stepdir->setDirectionPin (STEPDIR_DIR_PIN); stepdir->setAutoEnable(true); stepper_setSpeed(1); stepper_doStop(); stepdir->setCurrentPosition(pos); } } else { stepper_microsteps = 8; uln2003_enabled = false; uln2003->enableOutputs(); // need to enable first; otherwise disableOutputs() does nothing. uln2003->disableOutputs(); uln2003->setCurrentPosition(pos); } } static ulong stepper_restore_pos (void) { long pos = 0; if (fram_read(FRAM_STEPPER_POS, &pos, sizeof(pos))) { ulong fpos = STEPPER_TO_FOCUS_POS(pos); if (fpos >= FOCUS_LIMIT_MIN && fpos <= FOCUS_LIMIT_MAX) return pos; } return FOCUS_TO_STEPPER_POS(FOCUS_ZERO_POS); } static inline void stepper_save_pos (long pos) { if (fram_detected) { static long saved_pos = ~0ul; if (pos != saved_pos) { saved_pos = pos; fram_write(FRAM_STEPPER_POS, &saved_pos, sizeof(saved_pos)); } } } static inline ulong focus_currentPosition (void) { long pos = stepper_getPos(); stepper_save_pos(pos); return STEPPER_TO_FOCUS_POS(pos); } static void focus_check_backlash_timeout (void) { if (focus_backlash_timeout && time_after(millis(), focus_backlash_timeout)) { focus_backlash_timeout = 0; if (focus_prev_move_op) focus_move(focus_prev_move_dev, focus_prev_move_op, focus_speed); } } static void focus_set_calibration (ulong min, ulong max) { char tbuf[16], *flimits = (char *)nvram_get_val("focus.limits"); if (min < FOCUS_LIMIT_MIN || min >= max || max > FOCUS_LIMIT_MAX) { focus_calibrated = 0; focus_limit_min = FOCUS_LIMIT_MIN; focus_limit_max = FOCUS_LIMIT_MAX; tbuf[0] = '\0'; } else { focus_calibrated = 1; focus_limit_min = min; focus_limit_max = max; sprintf(tbuf, "%lu %lu", min, max); } if (strcmp(tbuf, flimits)) { strcpy(flimits, tbuf); nvram_delayed_save = get_timeout(nvram_save_delay); } SERIAL_PRINTF("%s(%lu,%lu): limits=%lu:%lu focus_calibrated=%u\r\n", __func__, min, max, focus_limit_min, focus_limit_max, focus_calibrated); } static inline void stepper_loop (void) { if (stepper_driver == STEPPER_DRIVER_NONE) return; stepper_stop_assist(); if (focus_fake_calibration_running && time_after(millis(), focus_fake_calibration_running)) { focus_fake_calibration_running = 0; focus_set_calibration(FOCUS_LIMIT_MIN, FOCUS_LIMIT_MAX); } focus_check_backlash_timeout(); if (stepper_driver == STEPPER_DRIVER_ULN2003) { if (!stepper_isRunning()) { if (uln2003_enabled) { uln2003_enabled = false; uln2003->disableOutputs(); } } if (uln2003_enabled) uln2003->run(); } stepper_save_pos(stepper_getPos()); // update position } static inline long focus_apply_limits (ulong pos) { if (pos > focus_limit_max) pos = focus_limit_max; else if (pos < focus_limit_min) pos = focus_limit_min; return pos; } static void focus_move (byte src, byte op, byte speed_0_to_9) { uint speed = stepper_set_speed_0_to_9(src, speed_0_to_9); if (focus_debug) SERIAL_PRINTF("FOCUS MOVE_%s: s=%d speed=%d pos=%ld\r\n", (op == MC_MOVE_NEG) ? "NEG" : "POS", speed_0_to_9, speed, focus_currentPosition()); if (speed) stepper_moveTo((op == MC_MOVE_NEG) ? FOCUS_TO_STEPPER_POS(focus_limit_min) : FOCUS_TO_STEPPER_POS(focus_limit_max)); } static inline bool focus_begin_backlash_compensation (byte op, uint amount) { focus_move(DEV_ESP32, op, 9); focus_backlash_timeout = get_timeout(amount * 2); if (focus_debug) SERIAL_PRINTF("%s: op=0x%02x %lumsecs\r\n", __func__, op, amount * 2lu); return true; // Compensation in-progress } static bool focus_handle_request (struct rxbuf_s *rxbuf, byte *data) { static bool slew_done = true; byte reply[AUXBUS_PKT_MAX], len, op = data[4], src = data[2]; len = emulate_begin("focus_rx", data, reply); switch (OP(op,data[1])) { case OP(MC_GET_POS,3): { ulong p = focus_currentPosition(); if (focus_debug && src == DEV_SW) { static ulong last_pos = -1; if (p != last_pos) { // Prevent SkyPortal FLOODING us with these last_pos = p; SERIAL_PRINTF("FOCUS GET_POS: %lu\r\n", p); } } reply[len++] = p >> 16; reply[len++] = p >> 8; reply[len++] = p; break; } case OP(MC_GOTO_SLOW,6): case OP(MC_GOTO_FAST,6): { slew_done = false; focus_backlash_timeout = 0; focus_target_pos = focus_apply_limits(get_24bit_value(data + 5)); long delta = focus_target_pos - focus_currentPosition(); long target = FOCUS_TO_STEPPER_POS(focus_target_pos); stepper_setSpeed(stepper_maxSpeed()); stepper_moveTo(target); if (focus_debug) SERIAL_PRINTF("FOCUS GOTO_%s: %lu -> %lu (%+ld)\r\n", (op == MC_GOTO_SLOW) ? "SLOW" : "FAST", focus_currentPosition(), focus_target_pos, delta); break; } case OP(MC_GOTO_DONE,4): case OP(MC_GOTO_DONE,3): { if (!slew_done && !stepper_isRunning()) slew_done = true; reply[len++] = slew_done ? 0xff : 0x00; if (focus_debug) SERIAL_PRINTF("FOCUS GOTO_DONE: %s pos=%ld/%ld speed=%ld\r\n", slew_done ? "Yes" : "No ", focus_currentPosition(), focus_target_pos, stepper_getSpeed()); break; } case OP(MC_MOVE_NEG,4): case OP(MC_MOVE_POS,4): { slew_done = true; if (focus_backlash_timeout) return true; focus_backlash_timeout = 0; int speed_0_to_9 = data[5]; if (focus_debug) SERIAL_PRINTF("FOCUS MOVE_%s %u\r\n", (op == MC_MOVE_NEG) ? "NEG" : "POS", speed_0_to_9); if (speed_0_to_9 && focus_prev_move_op != op) { // Apply backlash compensation: focus_prev_move_dev = src; focus_prev_move_op = op; focus_speed = speed_0_to_9; if (op == MC_MOVE_POS) { if (focus_backlash_pos && focus_begin_backlash_compensation(op, focus_backlash_pos)) break; } else { if (focus_backlash_neg && focus_begin_backlash_compensation(op, focus_backlash_neg)) break; } } focus_move(src, op, speed_0_to_9); break; } case OP(MC_GET_LIMITS,3): { // two 4-byte positions (low, high) or zeros len += long_to_4bytes(reply + len, focus_calibrated ? focus_limit_min : 0); len += long_to_4bytes(reply + len, focus_calibrated ? focus_limit_max : 0); if (focus_debug) SERIAL_PRINTLN("FOCUS GET_LIMITS"); break; } case OP(MC_IS_CALIBRATED,3): { // 00=No, 01=Yes reply[len++] = focus_calibrated; if (focus_debug) { static long last_time = 0; if (time_after(millis(), last_time + 5000)) { // prevent SkyPortal FLOODING us with these last_time = millis(); SERIAL_PRINTF("FOCUS IS_CALIBRATED: %s\r\n", focus_calibrated ? "Yes" : "No"); } } break; } case OP(MC_CALIBRATION_ENABLE,4): { // 1-byte 0=force_stop; non-zero=begin_calibration if (data[5] == 0) { focus_fake_calibration_running = 0; } else { const ulong FAKE_DELAY = 1000; ulong odelay = nvram_save_delay; nvram_save_delay = FAKE_DELAY + 500; // Save wear-and-tear: prevent focus_set_calibration() from writing to NVRAM focus_set_calibration(0,0); nvram_save_delay = odelay; focus_fake_calibration_running = get_timeout(FAKE_DELAY); // Pretend to be doing something briefly } if (focus_debug) SERIAL_PRINTF("FOCUS CALIBRATION_ENABLE %s\r\n", data[5] ? "begin" : "clear"); break; } case OP(DEV_GET_VERSION,3): { len += long_to_4bytes(reply + len, 0x07102454); // version 7.16.9300 if (focus_debug) SERIAL_PRINTLN("FOCUS GET_VERSION"); break; } case OP(MC_SET_POS_BACKLASH,4): { focus_backlash_pos = (data[5] < 100) ? data[5] : 99; nvram_save_int("focus.backlash.pos", focus_backlash_pos); if (focus_debug) SERIAL_PRINTF("FOCUS SET_POS_BACKLASH: %u\r\n", data[5]); break; } case OP(MC_SET_NEG_BACKLASH,4): { focus_backlash_neg = (data[5] < 100) ? data[5] : 99; nvram_save_int("focus.backlash.neg", focus_backlash_neg); if (focus_debug) SERIAL_PRINTF("FOCUS SET_NEG_BACKLASH: %u\r\n", data[5]); break; } case OP(MC_GET_POS_BACKLASH,3): { reply[len++] = focus_backlash_pos; if (focus_debug) SERIAL_PRINTF("FOCUS GET_POS_BACKLASH: %u\r\n", focus_backlash_pos); break; } case OP(MC_GET_NEG_BACKLASH,3): { reply[len++] = focus_backlash_neg; if (focus_debug) SERIAL_PRINTF("FOCUS GET_NEG_BACKLASH: %u\r\n", focus_backlash_neg); break; } case OP(DEV_GET_MODEL,3): reply[len++] = 0x1b; reply[len++] = 0xa8; if (focus_debug) SERIAL_PRINTLN("FOCUS GET_MODEL"); break; default: if (!auxbus_raw_tracing) SERIAL_PRINTF("FOCUS OP-0x%02x len=%u (unknown)\r\n", op, data[1]); } emulate_send_reply("focus_tx", rxbuf, reply, len); return false; } /************************** End Stepper motor emulation of Celestron Focus Motor *************************/ #endif /* EMULATE_FOCUS */ // *** Begin Nunchuck support **************************************************************************************** #if NCHUCK_ENABLED static bool nchuck_focus_only = false; struct button_s { const char *name; ulong timeout; /* millis */ ulong position; /* Focus Motor position */ bool have_position; byte approach; }; static struct button_s z_button = {"focus.presetZ", 0, 0, false, 0}; static struct button_s c_button = {"focus.presetC", 0, 0, false, 0}; static struct button_s *fm_preset_pending = NULL; static long fm_position_timeout = 0; static bool fm_goto_pending = false; static byte fm_goto_stage2 = 0; static bool fm_goto_stage1 = false; static uint32_t fm_goto_destination = 0; static byte fm_last_approach_cmd = MC_MOVE_POS; static void fm_restore_preset (struct button_s *b) { if (b->have_position) return; char *preset = (char *)nvram_get_val(b->name); if (!preset || !preset[0]) return; long val = atoi(preset); if (!val) return; if (nchuck_debug) SERIAL_PRINTF("%s: %s: %s (%ld)\r\n", __func__, b->name, preset, val); if (val < 0) { b->position = -val; b->approach = MC_MOVE_NEG; } else { b->position = val; b->approach = MC_MOVE_POS; } b->have_position = true; } static void nchuck_save_preset (struct button_s *b); static void fm_request_position (const char *who, bool set_timeout) { if (nchuck_debug) SERIAL_PRINTF("%s: from %s (%u)\r\n", __func__, who, set_timeout); fm_position_timeout = set_timeout ? get_timeout(1000) : 0; AUXBUS_SEND_MSG(DEV_ESP32, DEV_FOCUS, MC_GET_POS); } static void fm_query_slew_done (void) { AUXBUS_SEND_MSG(DEV_ESP32, DEV_FOCUS, MC_GOTO_DONE); } static void fm_goto_position (byte op, uint32_t pos) { if (nchuck_debug) SERIAL_PRINTF("%s: op=0x%02x %ld\r\n", __func__, op); AUXBUS_SEND_MSG(DEV_ESP32, DEV_FOCUS, op, (byte)(pos >> 16), (byte)(pos >> 8), (byte)pos); } static long fm_delay_timer = 0; static void fm_goto_begin_moving (uint32_t current_position) { uint32_t destination = fm_goto_destination; fm_goto_pending = false; fm_goto_stage1 = true; fm_delay_timer = 0; if (fm_goto_stage2 == MC_MOVE_POS && destination < current_position) destination -= 125; else if (fm_goto_stage2 == MC_MOVE_NEG && destination > current_position) destination += 125; else fm_goto_stage2 = 0; // first goto will be the final goto; stage2 not needed fm_goto_position(MC_GOTO_FAST, destination); } static void fm_goto (uint32_t destination, byte approach) { if (verbose || nchuck_debug) SERIAL_PRINTF("%s: Move to position %lu\r\n", __func__, destination); fm_goto_destination = destination; fm_goto_stage1 = false; fm_goto_stage2 = approach; fm_goto_pending = true; fm_delay_timer = 0; fm_request_position(__func__, false); // This will trigger fm_goto_begin_moving() when position comes back } static void cancel_fm_preset_pending (bool clear) { fm_position_timeout = 0; if (fm_preset_pending && clear) { fm_preset_pending->timeout = 0; fm_preset_pending = NULL; } } static void handle_preset_button (bool moving, bool button_down, struct button_s *b) { if (fm_position_timeout && time_after(millis(), fm_position_timeout)) cancel_fm_preset_pending(true); if (moving) { b->timeout = 0; fm_preset_pending = NULL; return; } if (button_down) { if (!b->timeout) { if (verbose || nchuck_debug) SERIAL_PRINTLN("Button down "); b->timeout = get_timeout(1500); // Hold button for 1.500 seconds to save current focus position. if (b == fm_preset_pending) fm_preset_pending = NULL; return; } if (time_after(millis(), b->timeout)) { set_blue_led(LED_ON); if (!fm_preset_pending) { b->have_position = false; fm_preset_pending = b; fm_request_position(__func__, true); } } } else if (b->timeout) { b->timeout = 0; if (verbose || nchuck_debug) SERIAL_PRINTLN("Button up"); set_blue_led(LED_OFF); if (!b->have_position) fm_restore_preset(b); if (b->have_position) { if (b == fm_preset_pending) fm_preset_pending = NULL; else fm_goto(b->position, b->approach); } } } static void fm_delayed_query_slew_done (void) { if (!fm_delay_timer) { if (fm_goto_stage1 || fm_goto_stage2) fm_delay_timer = get_timeout(250); return; } if (time_before(millis(), fm_delay_timer)) return; fm_delay_timer = 0; fm_query_slew_done(); } static void nchuck_handle_fm_response (byte *data) { byte len = data[1], op = data[4]; if (nchuck_debug) print_packet(__func__, NULL, data, len + 3); if (OP(op,len) == OP(MC_GET_POS,6)) { uint32_t pos = get_24bit_value(data + 5); fm_position_timeout = 0; if (fm_preset_pending && !fm_preset_pending->have_position) { fm_preset_pending->position = pos; if (nchuck_debug) SERIAL_PRINTF("Current Focus Position: %u\r\n", fm_preset_pending->position); fm_preset_pending->have_position = true; fm_preset_pending->approach = fm_last_approach_cmd ? fm_last_approach_cmd : MC_MOVE_POS; nchuck_save_preset(fm_preset_pending); } if (fm_goto_pending) fm_goto_begin_moving(pos); return; } if (OP(op,len) == OP(MC_GOTO_DONE,3) || OP(op,len) == OP(MC_GOTO_DONE,4)) { if (data[5] == 0) // Still slewing? return; if (fm_goto_stage1) { fm_goto_stage1 = false; fm_delay_timer = 0; if (nchuck_debug) SERIAL_PRINTLN("Slew DONE"); if (fm_goto_stage2) { if (nchuck_debug) SERIAL_PRINTLN("Slew adjust"); fm_goto_position(MC_GOTO_SLOW, fm_goto_destination); } } else if (fm_goto_stage2) { fm_goto_stage2 = 0; fm_delay_timer = 0; if (nchuck_debug) SERIAL_PRINTLN("Slew adjust DONE"); } } } static byte azm_guiderate[16] = {0,}, alt_guiderate[16] = {0,}; // Monitor Motor Controller (MC) requests to keep track of most recent guiderate commands. static void nchuck_handle_mc_request (byte *buf) { if (buf[4] != MC_SET_POS_GUIDERATE && buf[4] != MC_SET_NEG_GUIDERATE) return; byte *guiderate = (buf[3] == DEV_AZM) ? azm_guiderate : alt_guiderate; memcpy(guiderate, buf, buf[1] + 3); } static void resend_most_recent_guiderate (byte mc_dev) { byte *guiderate = (mc_dev == DEV_AZM) ? azm_guiderate : alt_guiderate; if (guiderate[0] == 0x3b) { guiderate[2] = DEV_ESP32; update_checksum(guiderate); tx_enq(NULL, &auxbus, forward_no, REGULAR_PKT, guiderate, guiderate[1] + 3); } } void send_mc_msg (byte dev, byte cmd, byte speed) { fm_goto_stage1 = false; fm_goto_stage2 = 0; fm_delay_timer = 0; AUXBUS_SEND_MSG(DEV_ESP32, dev, cmd, speed); if (speed == 0 && dev != DEV_FOCUS) resend_most_recent_guiderate(dev); } static bool nchuck_reverse_alt = false; static void nchuck_send_mc_cmd (bool fast_range, uint8_t dev, int speed) { static const uint8_t slew_speeds_slow [5] = {4, 4, 5, 5, 6}; // must be in range 1..9; 0 is STOP static const uint8_t slew_speeds_fast [5] = {4, 5, 6, 7, 9}; // must be in range 1..9; 0 is STOP bool reversed = (dev == DEV_ALT) && nchuck_reverse_alt; uint8_t cmd = reversed ? MC_MOVE_NEG : MC_MOVE_POS; if (speed) { if (speed < 0) { speed = -speed; cmd = reversed ? MC_MOVE_POS : MC_MOVE_NEG; } speed--; // make into array index from 0..N if (dev != DEV_FOCUS) { speed = fast_range ? slew_speeds_fast[speed] : slew_speeds_slow[speed]; #if EMULATE_FOCUS } else if (stepper_driver != STEPPER_DRIVER_NONE) { // HomeBrew Focus Motor uses the full speed range 1..9. static const uint8_t focus_speeds_slow[5] = {1, 2, 3, 4, 5}; // must be in range 1..9; 0 is STOP static const uint8_t focus_speeds_fast[5] = {5, 6, 7, 8, 9}; // must be in range 1..9; 0 is STOP speed = fast_range ? focus_speeds_fast[speed] : focus_speeds_slow[speed]; #endif /* EMULATE_FOCUS */ } else { // Celestron Focus Motor only uses speeds 7,8,9. Lower speeds do nothing useful. Updated Dec/2023. static const uint8_t focus_speeds_slow[5] = {7, 7, 7, 7, 8}; // must be in range 1..9; 0 is STOP static const uint8_t focus_speeds_fast[5] = {7, 7, 8, 8, 9}; // must be in range 1..9; 0 is STOP speed = fast_range ? focus_speeds_fast[speed] : focus_speeds_slow[speed]; } fm_last_approach_cmd = cmd; } if (verbose || nchuck_debug #if EMULATE_FOCUS || (focus_debug && dev == DEV_FOCUS && stepper_driver != STEPPER_DRIVER_NONE) #endif /* EMULATE_FOCUS */ ) SERIAL_PRINTF("%s: dev=0x%02x cmd=0x%02x fast=%u speed=0x%02x\r\n", __func__, dev, cmd, fast_range, speed); send_mc_msg(dev, cmd, speed); } static byte nchuck_buffer[6]; static byte nchuck_x_thumb() {return nchuck_buffer[0];} static byte nchuck_y_thumb() {return nchuck_buffer[1];} static int nchuck_x_accel() {return ((int)(nchuck_buffer[2] << 2) | ((nchuck_buffer[5] >> 2) & 3));} static int nchuck_y_accel() {return ((int)(nchuck_buffer[3] << 2) | ((nchuck_buffer[5] >> 4) & 3));} static int nchuck_z_accel() {return ((int)(nchuck_buffer[4] << 2) | ((nchuck_buffer[5] >> 6) & 3));} static bool nchuck_z_button() {return ( nchuck_buffer[5] & 0x01) == 0;} static bool nchuck_c_button() {return ( (nchuck_buffer[5] >> 1) & 0x01) == 0;} static bool nchuck_detected = false; static int nchuck_oldx = 0, nchuck_oldy = 0; static byte nchuck_measuring_middle, nchuck_x_mhi, nchuck_x_mlo, nchuck_y_mhi, nchuck_y_mlo; static bool nchuck_blink_led = false; #define NCHUCK_POLL_INTERVAL 25 /* milli-seconds */ #define NCHUCK_MIDDLE_SAMPLES (750 / NCHUCK_POLL_INTERVAL) #define NCHUCK_I2C_ADDR 0x52 static void nchuck_request_data (void) { if (!nchuck_detected || (nchuck_detected = i2c_detect_device(NCHUCK_I2C_ADDR))) return; // Stop movements when Nunchuck is unplugged mid-operation: SERIAL_PRINTLN("Nunchuck unplugged"); if (nchuck_blink_led) { nchuck_blink_led = false; set_blue_led(LED_OFF); } if (nchuck_oldx) { nchuck_oldx = 0; nchuck_send_mc_cmd(0, DEV_AZM, 0); } if (nchuck_oldy) { nchuck_oldy = 0; nchuck_send_mc_cmd(1, DEV_ALT, 0); } nchuck_send_mc_cmd(0, DEV_FOCUS, 0); } static bool nchuck_get_data (void) { static long nextpoll = 0; long now = millis(); if (time_before(now, nextpoll)) return false; nextpoll = now + NCHUCK_POLL_INTERVAL; Wire.requestFrom(NCHUCK_I2C_ADDR, sizeof(nchuck_buffer)); byte bytecount; for (bytecount = 0; bytecount < sizeof(nchuck_buffer) && Wire.available(); bytecount++) { byte b = Wire.read(); nchuck_buffer[bytecount] = b; } nchuck_request_data(); // for next time return (bytecount == sizeof(nchuck_buffer)); } static byte nchuck_cdata[16]; // Nunchuck calibration data // Reference: https://www.xarg.org/2016/12/using-a-wii-nunchuk-with-arduino/ // Clone: {80 80 80 00},{b4 b4 b4 00},{ff 00 80},{ff 00 80},{ef 44} // Clone2: {00 00 00 00},{00 00 00 00},{ff 00 80},{ff 00 80},{00 00} // CN user "dewo" // Nintendo: {82 80 7e 0a},{b4 b2 b3 0f},{00 00 7b},{00 00 82},{04 59} // // [ 0] 0G value of X-axis [9:2] // [ 1] 0G value of Y-axis [9:2] // [ 2] 0G value of Z-axis [9:2] // [ 3] LSB of 0G value for X,Y,Z axis // // [ 4] 1G value of X-axis [9:2] // [ 5] 1G value of Y-axis [9:2] // [ 6] 1G value of Z-axis [9:2] // [ 7] LSB of 1G value for X,Y,Z axis // // [ 8] Thumbstick X-axis maximum // [ 9] Thumbstick X-axis minimum // [10] Thumbstick X-axis centre // // [11] Thumbstick Y-axis maximum // [12] Thumbstick Y-axis minimum // [13] Thumbstick Y-axis centre // // [14] Checksum/CRC? // [15] Checksum/CRC? static byte nchuck_xranges[10], nchuck_yranges[10]; static int nchuck_interpret (byte *ranges, uint16_t val) { if (val >= ranges[0]) return 5; if (val >= ranges[1]) return 4; if (val >= ranges[2]) return 3; if (val >= ranges[3]) return 2; if (val >= ranges[4]) return 1; /* middle */ if (val <= ranges[9]) return -5; if (val <= ranges[8]) return -4; if (val <= ranges[7]) return -3; if (val <= ranges[6]) return -2; if (val <= ranges[5]) return -1; return 0; } static void nchuck_set_ranges (char axis, byte *ranges, byte *cdata, byte mhi, byte mlo) { byte max = cdata[0] ? cdata[0] : 0xff; if (mhi < cdata[2]) max -= cdata[2] - mhi; byte min = cdata[1]; if (mlo > cdata[2]) min += mlo - cdata[2]; const byte min_motion = 3; ranges[0] = max - min_motion; ranges[1] = max - (max - mhi) / 6; ranges[2] = mhi + (max - mhi) / 2; ranges[3] = mhi + (max - mhi) / 3; ranges[4] = mhi + min_motion; ranges[5] = mlo - min_motion; ranges[6] = mlo - (mlo - min) / 3; ranges[7] = mlo - (mlo - min) / 2; ranges[8] = min + (mlo - min) / 6; ranges[9] = min + min_motion; if (nchuck_debug) SERIAL_PRINTF("%s: %c-axis: (%u,%u,%u,%u): %u %u %u %u %u <> %u %u %u %u %u\r\n", __func__, axis, max, mhi, mlo, min, ranges[0], ranges[1], ranges[2], ranges[3], ranges[4], ranges[5], ranges[6], ranges[7], ranges[8], ranges[9]); } static bool nchuck_detect (bool blink_led) { static long next_poll = 0; if (nchuck_detected || (next_poll && time_before(millis(), next_poll))) return nchuck_detected; next_poll = get_timeout(3000); Wire.setClock(100000ul); Wire.beginTransmission(NCHUCK_I2C_ADDR); Wire.write(0xf0); Wire.write(0x55); nchuck_detected = (Wire.endTransmission() == 0); if (nchuck_detected) { Wire.beginTransmission(NCHUCK_I2C_ADDR); Wire.write(0xfb); Wire.write(0x00); nchuck_detected = (Wire.endTransmission() == 0); } if (!nchuck_detected) return false; next_poll = 0; Wire.beginTransmission(NCHUCK_I2C_ADDR); Wire.write(0xfa); Wire.endTransmission(); byte b[6]; Wire.requestFrom(NCHUCK_I2C_ADDR, sizeof(b)); for (byte i = 0; i < sizeof(b); ++i) b[i] = Wire.read(); bool not_nchuck = (b[0] != 0 && b[0] != 0xff) || b[1] || b[2] != 0xa4 || b[3] != 0x20 || b[4] || b[5]; if (verbose || not_nchuck || nchuck_debug) { SERIAL_PRINTF("%s: %02x %02x %02x %02x %02x %02x\r\n", __func__, b[0], b[1], b[2], b[3], b[4], b[5]); if (not_nchuck) { SERIAL_PRINTLN("I2C Device is not a Nunchuck"); next_poll = get_timeout(10 * 1000); nchuck_detected = false; return nchuck_detected; } } // Newly plugged-in Nunchuck: turn on BLUE LED while calibrating, except during boot. SERIAL_PRINTLN("nchuck detected"); if (blink_led) { nchuck_blink_led = blink_led; set_blue_led(LED_ON); } // Fetch calibration data from the Nunchuck Wire.beginTransmission(NCHUCK_I2C_ADDR); Wire.write(0x20); Wire.endTransmission(); Wire.requestFrom(NCHUCK_I2C_ADDR, sizeof(nchuck_cdata)); for (byte i = 0; i < sizeof(nchuck_cdata); ++i) nchuck_cdata[i] = Wire.read(); #define NCHUCK_FAKE_DATA false #if NCHUCK_FAKE_DATA static const byte genuine[16] = {0x75,0x81,0x7a,0x19,0xaa,0xb1,0xae,0x14,0xe1,0x1e,0x7c,0xe0,0x1d,0x88,0xfb,0x50}; // Nintento brand memcpy(nchuck_cdata, genuine, sizeof(genuine)); //static const byte clone2[16] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x00,0x80,0xff,0x00,0x80,0x00,0x00}; // CN user "dewo" //memcpy(nchuck_cdata, clone2, sizeof(clone2)); #endif if (nchuck_cdata[10] == 0x00 || nchuck_cdata[13] == 0x00 || nchuck_cdata[10] == 0xff || nchuck_cdata[13] == 0xff) { nchuck_cdata[ 8] = 0xff; nchuck_cdata[ 9] = 0x00; nchuck_cdata[10] = 0x80; nchuck_cdata[11] = 0xff; nchuck_cdata[12] = 0x00; nchuck_cdata[13] = 0x80; } nchuck_measuring_middle = NCHUCK_MIDDLE_SAMPLES; nchuck_x_mhi = nchuck_x_mlo = nchuck_cdata[10]; nchuck_y_mhi = nchuck_y_mlo = nchuck_cdata[13]; if (nchuck_debug) { SERIAL_PRINTF("Nunchuck Calibration Data:"); for (byte i = 0; i < sizeof(nchuck_cdata); ++i) SERIAL_PRINTF(" %02x", nchuck_cdata[i]); SERIAL_PRINTF("\r\n"); } delay(50); nchuck_request_data(); delay(50); return nchuck_detected; } /* * Even though we use the supplied calibration data, it can be incorrect. * So measure/record the two centre ranges "at rest" over a 2-second period. */ static bool nchuck_measure_middle (byte newx, byte newy) { bool first = nchuck_measuring_middle == NCHUCK_MIDDLE_SAMPLES; if (nchuck_debug) { static byte prev[6]; if (first) memset(prev, 0, sizeof(prev)); if (0 != memcmp(prev, nchuck_buffer, sizeof(prev))) { memcpy(prev, nchuck_buffer, sizeof(prev)); SERIAL_PRINTF("%s: data: x=%u y=%u, {0x%02x 0x%02x 0x%02x 0x%02x}\r\n", __func__, prev[0], prev[1], prev[2], prev[3], prev[4], prev[5]); } } if (first || newx > nchuck_x_mhi) nchuck_x_mhi = newx; if (first || newx < nchuck_x_mlo) nchuck_x_mlo = newx; if (first || newy > nchuck_y_mhi) nchuck_y_mhi = newy; if (first || newy < nchuck_y_mlo) nchuck_y_mlo = newy; if (--nchuck_measuring_middle) return true; #if NCHUCK_FAKE_DATA nchuck_y_mhi = nchuck_y_mlo = 137; #endif nchuck_set_ranges('X', nchuck_xranges, nchuck_cdata + 8, nchuck_x_mhi, nchuck_x_mlo); nchuck_set_ranges('Y', nchuck_yranges, nchuck_cdata + 11, nchuck_y_mhi, nchuck_y_mlo); if (nchuck_blink_led) { nchuck_blink_led = false; set_blue_led(LED_OFF); } return false; } static void nchuck_loop (void) { if (nchuck_focus_only) fm_delayed_query_slew_done(); static int xp, yp, devx = 0, devy = 0; if (!nchuck_detect(true) || !nchuck_get_data()) return; int newx, x = nchuck_x_thumb(); int newy, y = nchuck_y_thumb(); // Find the middle "drift range" for when the Nunchuck is supposedly "at rest": if (nchuck_measuring_middle && nchuck_measure_middle(x, y)) return; newx = nchuck_interpret(nchuck_xranges, x); newy = 0 - nchuck_interpret(nchuck_yranges, y); if (nchuck_debug) { if (nchuck_oldx != newx) SERIAL_PRINTF("X=%3u, range: %2d -> %2d\r\n", x, nchuck_oldx, newx); if (nchuck_oldy != newy) SERIAL_PRINTF("Y=%3u, range: %2d -> %2d\r\n", y, nchuck_oldy, newy); } static bool presets_enabled = true; static bool mode_change_pending = false; static bool oldc = false, oldz = false; static long buttons_down = 0; bool newc = nchuck_c_button(), newz = nchuck_z_button(); // Whenever either button changes state, stop any previous motions-in-progress: if (oldc != newc || oldz != newz) { oldc = newc; oldz = newz; // Whenever either button changes state, stop any previous motions-in-progress: if (nchuck_oldx && devx) { nchuck_oldx = 0; if (!nchuck_focus_only) nchuck_send_mc_cmd(0, devx, 0); } if (nchuck_oldy && devy) { nchuck_oldy = 0; if (!nchuck_focus_only) nchuck_send_mc_cmd(1, devy, 0); } if (newc && newz) { cancel_fm_preset_pending(false); presets_enabled = false; z_button.timeout = 0; c_button.timeout = 0; if (!buttons_down) buttons_down = get_timeout(2000); } else if (!newc && !newz) { presets_enabled = true; buttons_down = 0; if (mode_change_pending) { mode_change_pending = false; if (!simple_mode) { nchuck_focus_only = !nchuck_focus_only; nvram_save_int("focus.only", !!nchuck_focus_only); SERIAL_PRINTF("focus_only=%u\r\n", nchuck_focus_only); } set_blue_led(LED_OFF); } } } if (buttons_down && time_after(millis(), buttons_down)) { mode_change_pending = true; set_blue_led(LED_ON); } // Handle the C/Z buttons: bool focus_selected = false; bool fast_range = false; // Selects between slow and fast ranges of speeds if (nchuck_focus_only || newc == 1) focus_selected = true; else if (newz == 1) fast_range = true; // Faster slew speeds when button-Z held // Handle a change in X-axis of the controller: if (nchuck_oldx != newx) { nchuck_oldx = newx; devx = focus_selected ? DEV_FOCUS : DEV_AZM; nchuck_send_mc_cmd(fast_range, devx, newx); } // Handle a change in Y-axis of the controller: if (nchuck_oldy != newy) { nchuck_oldy = newy; devy = focus_selected ? DEV_FOCUS : DEV_ALT; if (!focus_selected || !newx || !newy) // For focus, Prevent x and y from fighting each other! nchuck_send_mc_cmd(fast_range|focus_selected, devy, newy); } static long next_poll = 0; if (next_poll && time_after(millis(), next_poll)) next_poll = 0; if (nchuck_focus_only) { if (!fm_detected) { if (nchuck_z_button() || nchuck_c_button()) { if (!next_poll) { next_poll = get_timeout(500); fm_request_position(__func__, false); } } } else if (presets_enabled) { bool moving = (nchuck_oldy || nchuck_oldx); handle_preset_button(moving, nchuck_z_button(), &z_button); handle_preset_button(moving, nchuck_c_button(), &c_button); } } } #endif /* NCHUCK_ENABLED */ // *** End Nunchuck support ****************************************************************************************** // *** Begin DEW support ****************************************************************************************** #if EMULATE_DEW #if __has_include("ADC_LUT.h") #warning "Including ADC_LUT.h" #include "ADC_LUT.h" #else static const int ADC_LUT[4096] = { 0, 33,35,37,39,40,42,44,46,48,49,51,52,54,56,57, 59,60,62,63,65,66,67,67,68,69,70,71,72,73,74, 75,76,77,78,78,79,80,82,83,84,85,87,88,89,91, 92,93,94,96,97,98,99,100,101,102,103,104,105,106,107, 108,109,110,111,112,113,114,115,117,118,119,120,121,122,123, 124,126,127,128,129,130,131,132,133,134,135,136,137,138,139, 140,141,142,143,144,145,147,148,149,150,151,152,153,154,155, 156,157,158,159,160,162,163,164,165,166,167,168,169,171,172, 173,174,175,176,177,179,180,181,182,184,185,186,188,189,190, 191,193,194,195,196,197,198,200,201,202,203,204,205,207,208, 209,210,210,211,212,213,214,215,216,217,217,218,219,220,221, 222,223,224,225,226,227,228,229,230,231,232,233,234,235,236, 237,238,239,240,242,243,244,245,247,248,249,250,251,253,254, 255,256,258,259,260,261,262,263,264,266,267,268,269,270,271, 272,273,274,275,276,277,278,279,280,281,282,283,284,285,286, 287,288,289,290,291,292,293,294,295,296,298,299,300,301,302, 303,304,305,306,307,308,309,310,311,312,313,314,315,317,318, 319,320,321,322,323,325,326,327,328,329,331,332,333,334,335, 337,338,339,340,341,342,344,345,346,347,348,349,350,352,353, 354,355,356,357,358,359,360,361,362,363,364,365,366,367,368, 370,371,372,373,374,375,376,377,378,379,381,382,383,384,385, 386,387,388,389,390,391,392,393,394,395,396,397,398,399,400, 401,402,403,405,406,407,408,409,410,411,412,414,415,416,417, 418,420,421,422,423,424,426,427,428,429,431,432,433,434,435, 436,437,439,440,441,442,443,444,445,446,447,449,450,451,452, 453,454,455,456,457,458,459,460,461,462,463,464,465,466,467, 468,469,470,471,472,473,474,475,476,477,478,479,480,481,483, 484,485,486,488,489,490,491,493,494,495,496,497,498,499,500, 500,501,502,503,504,504,505,506,507,508,509,509,510,511,512, 513,514,515,516,517,518,519,520,521,522,523,524,525,526,527, 528,529,530,531,532,534,535,536,537,538,540,541,542,543,545, 546,547,548,549,550,551,552,553,554,555,556,557,558,559,560, 561,562,563,565,566,567,568,569,571,572,573,574,576,577,577, 578,579,580,581,582,582,583,584,585,586,586,587,588,589,590, 591,591,592,593,594,595,596,597,598,599,600,601,602,603,604, 605,606,606,607,608,610,611,612,613,614,615,616,617,618,620, 621,622,623,624,625,626,627,628,629,630,631,632,633,634,635, 636,637,638,639,640,641,642,643,644,646,647,648,649,650,652, 653,654,655,656,657,659,660,661,662,663,664,665,666,667,669, 670,671,672,673,674,676,677,678,679,681,682,683,684,686,687, 688,689,690,691,692,693,694,695,696,697,698,699,700,701,702, 703,704,705,707,708,709,710,711,712,714,715,716,717,718,719, 720,721,722,723,724,725,726,727,728,729,730,731,732,733,734, 735,736,737,738,740,741,742,744,745,746,748,749,750,752,753, 754,755,756,757,758,759,760,761,762,763,764,765,766,767,768, 769,770,771,772,773,774,775,776,777,778,779,779,780,781,782, 783,784,785,787,788,789,790,791,792,793,795,796,797,798,799, 800,801,802,803,805,806,807,808,809,810,811,812,813,814,815, 817,818,819,820,822,823,824,826,827,828,829,831,832,833,834, 835,836,836,837,838,839,840,841,842,843,843,844,845,846,847, 848,849,850,851,853,854,855,856,857,859,860,861,862,864,865, 866,867,868,869,870,871,872,873,874,875,876,877,878,879,880, 881,882,883,884,885,886,887,889,890,891,892,893,894,895,896, 897,898,900,901,902,903,904,905,906,907,908,909,910,911,913, 914,915,916,917,918,919,920,921,922,923,924,925,926,927,928, 929,930,931,932,933,935,936,937,938,939,940,941,943,944,945, 946,947,949,950,951,952,953,955,956,957,958,959,961,962,963, 964,964,965,966,967,968,969,970,971,972,973,974,975,976,977, 978,979,980,981,982,983,984,986,987,988,989,990,991,992,993, 994,995,996,997,998,999,1001,1002,1003,1004,1005,1006,1007,1009,1011, 1014,1016,1019,1021,1024,1025,1026,1028,1029,1030,1031,1033,1034,1035,1036, 1038,1039,1040,1041,1042,1043,1044,1045,1047,1048,1049,1050,1051,1052,1053, 1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068, 1068,1069,1070,1071,1072,1074,1075,1076,1077,1078,1079,1081,1082,1083,1084, 1085,1087,1088,1089,1090,1091,1092,1093,1093,1094,1095,1096,1097,1098,1099, 1100,1101,1102,1103,1104,1105,1106,1107,1109,1110,1111,1112,1113,1115,1116, 1117,1118,1119,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132, 1133,1135,1136,1137,1137,1138,1139,1140,1141,1142,1142,1143,1144,1145,1146, 1147,1147,1148,1149,1150,1151,1152,1152,1153,1154,1155,1156,1157,1158,1159, 1160,1161,1162,1163,1164,1165,1166,1167,1168,1170,1171,1172,1173,1175,1176, 1177,1178,1179,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192, 1193,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1204,1205,1206,1208, 1209,1210,1211,1213,1214,1215,1217,1218,1219,1220,1221,1222,1223,1224,1225, 1226,1227,1228,1229,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241, 1242,1243,1244,1245,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257, 1258,1259,1260,1261,1262,1264,1265,1266,1266,1267,1268,1269,1270,1271,1272, 1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287, 1288,1289,1290,1291,1292,1293,1295,1296,1297,1298,1299,1300,1301,1302,1304, 1305,1306,1307,1308,1309,1310,1312,1313,1314,1315,1316,1317,1318,1319,1320, 1321,1321,1322,1323,1324,1325,1326,1327,1328,1330,1331,1332,1334,1335,1336, 1338,1339,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353, 1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1367,1368,1369, 1370,1371,1372,1373,1374,1375,1376,1377,1378,1380,1381,1382,1383,1384,1385, 1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1398,1399, 1400,1401,1402,1403,1404,1405,1405,1406,1407,1408,1409,1411,1412,1413,1415, 1416,1417,1419,1420,1421,1423,1424,1425,1426,1427,1428,1430,1431,1432,1433, 1434,1435,1436,1437,1439,1440,1441,1441,1442,1443,1444,1445,1446,1447,1447, 1448,1449,1450,1451,1452,1452,1453,1454,1455,1456,1457,1458,1460,1461,1462, 1464,1465,1466,1467,1469,1470,1471,1473,1474,1474,1475,1476,1477,1478,1479, 1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1492,1493,1494,1495, 1497,1498,1499,1500,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512, 1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1525,1526, 1527,1528,1529,1530,1531,1532,1533,1533,1534,1535,1536,1537,1538,1539,1540, 1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555, 1556,1557,1558,1559,1560,1561,1562,1564,1565,1566,1567,1568,1569,1570,1571, 1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1586,1587, 1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1602,1603, 1604,1605,1606,1607,1609,1610,1611,1612,1613,1614,1615,1617,1618,1619,1620, 1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1636, 1637,1638,1639,1641,1642,1643,1644,1646,1647,1648,1649,1650,1651,1652,1653, 1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668, 1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683, 1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1694,1695,1696,1697, 1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712, 1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727, 1728,1730,1731,1732,1734,1735,1736,1737,1739,1740,1741,1742,1744,1745,1746, 1747,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1756,1757,1758,1759, 1760,1761,1762,1763,1765,1766,1767,1768,1769,1770,1772,1773,1774,1775,1776, 1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791, 1792,1793,1795,1796,1797,1799,1800,1801,1803,1804,1806,1807,1808,1809,1810, 1811,1812,1813,1814,1814,1815,1816,1817,1818,1819,1820,1821,1822,1822,1823, 1824,1825,1827,1828,1829,1830,1832,1833,1834,1835,1836,1838,1839,1840,1841, 1842,1843,1844,1845,1846,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857, 1859,1860,1861,1862,1864,1865,1866,1868,1869,1870,1871,1873,1873,1874,1875, 1876,1877,1878,1879,1880,1881,1882,1883,1884,1884,1885,1886,1887,1888,1889, 1890,1891,1892,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1905,1906, 1907,1908,1909,1910,1911,1913,1914,1915,1916,1917,1918,1920,1921,1922,1922, 1923,1924,1925,1926,1927,1928,1929,1929,1930,1931,1932,1933,1934,1935,1936, 1936,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951, 1952,1954,1955,1956,1958,1959,1961,1962,1963,1965,1966,1967,1969,1970,1971, 1972,1973,1974,1975,1977,1978,1979,1980,1981,1982,1983,1985,1985,1986,1987, 1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,1999,2000,2002, 2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2016,2017,2018, 2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2035, 2038,2041,2044,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059, 2060,2061,2062,2063,2064,2065,2066,2068,2069,2071,2072,2074,2075,2076,2078, 2079,2081,2081,2082,2083,2084,2085,2086,2087,2087,2088,2089,2090,2091,2092, 2093,2094,2094,2095,2096,2097,2099,2100,2102,2103,2105,2106,2108,2109,2111, 2112,2113,2114,2115,2116,2117,2118,2119,2119,2120,2121,2122,2123,2124,2125, 2126,2127,2128,2128,2130,2131,2133,2134,2135,2137,2138,2139,2141,2142,2143, 2145,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158, 2158,2159,2160,2161,2163,2164,2165,2166,2167,2168,2169,2170,2171,2173,2174, 2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189, 2190,2191,2192,2193,2194,2195,2197,2198,2199,2200,2201,2202,2204,2205,2206, 2207,2208,2209,2211,2212,2213,2214,2216,2217,2218,2219,2220,2222,2223,2224, 2225,2226,2227,2228,2229,2230,2231,2232,2234,2235,2236,2237,2238,2239,2240, 2241,2241,2242,2243,2244,2244,2245,2246,2246,2247,2248,2249,2249,2250,2251, 2252,2252,2253,2254,2255,2255,2256,2257,2258,2259,2260,2262,2263,2264,2265, 2266,2267,2268,2269,2270,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281, 2282,2283,2284,2285,2286,2287,2288,2290,2291,2293,2294,2295,2297,2298,2300, 2301,2302,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2314,2315, 2316,2317,2318,2319,2320,2322,2323,2324,2325,2327,2328,2329,2331,2332,2333, 2335,2336,2337,2338,2339,2340,2340,2341,2342,2343,2344,2345,2346,2347,2347, 2348,2349,2350,2351,2352,2353,2354,2356,2357,2358,2360,2361,2362,2364,2365, 2366,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381, 2383,2384,2385,2386,2387,2388,2389,2390,2391,2392,2393,2394,2395,2396,2397, 2398,2399,2400,2401,2402,2404,2405,2406,2407,2408,2409,2411,2412,2413,2414, 2415,2416,2417,2418,2419,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430, 2431,2432,2433,2434,2435,2436,2437,2438,2439,2440,2441,2442,2443,2444,2445, 2446,2447,2448,2449,2450,2451,2452,2453,2454,2455,2456,2457,2458,2460,2461, 2462,2463,2464,2465,2466,2467,2468,2470,2471,2472,2473,2474,2475,2477,2478, 2479,2480,2481,2482,2483,2485,2486,2487,2488,2489,2490,2491,2492,2493,2494, 2496,2497,2498,2499,2500,2501,2503,2504,2505,2506,2507,2509,2510,2511,2512, 2513,2514,2516,2517,2518,2519,2520,2521,2522,2524,2525,2526,2527,2528,2529, 2530,2531,2532,2533,2534,2535,2536,2537,2538,2539,2540,2541,2542,2543,2544, 2545,2545,2546,2547,2548,2549,2550,2550,2551,2552,2553,2554,2555,2555,2556, 2557,2558,2559,2559,2560,2562,2563,2564,2566,2567,2568,2569,2571,2572,2573, 2575,2576,2577,2578,2579,2579,2580,2581,2582,2583,2583,2584,2585,2586,2587, 2588,2588,2589,2590,2591,2592,2593,2594,2595,2596,2598,2599,2600,2602,2603, 2604,2606,2607,2608,2609,2611,2612,2613,2614,2616,2617,2618,2619,2621,2622, 2623,2624,2625,2626,2627,2628,2629,2630,2631,2632,2633,2634,2635,2636,2637, 2637,2638,2639,2640,2642,2643,2644,2645,2647,2648,2649,2651,2652,2653,2654, 2656,2657,2658,2659,2659,2660,2661,2662,2663,2664,2665,2666,2667,2668,2669, 2670,2670,2671,2672,2674,2675,2676,2678,2679,2680,2681,2683,2684,2685,2687, 2688,2689,2690,2691,2692,2693,2694,2695,2695,2696,2697,2698,2699,2700,2701, 2702,2703,2704,2705,2706,2707,2709,2710,2711,2712,2713,2715,2716,2717,2718, 2720,2721,2722,2723,2724,2724,2725,2726,2727,2728,2729,2730,2731,2732,2733, 2734,2735,2736,2737,2739,2740,2741,2743,2744,2745,2747,2748,2749,2751,2752, 2753,2754,2755,2756,2757,2758,2759,2760,2761,2762,2763,2764,2765,2766,2767, 2768,2769,2770,2771,2773,2774,2775,2776,2777,2778,2779,2780,2781,2783,2784, 2785,2785,2786,2787,2788,2789,2790,2790,2791,2792,2793,2794,2794,2795,2796, 2797,2798,2798,2799,2800,2801,2802,2803,2804,2805,2806,2807,2808,2809,2810, 2812,2813,2814,2815,2816,2817,2818,2818,2819,2820,2821,2822,2823,2824,2824, 2825,2826,2827,2828,2829,2830,2830,2831,2832,2834,2835,2836,2838,2839,2841, 2842,2844,2845,2846,2848,2849,2850,2851,2852,2853,2853,2854,2855,2856,2857, 2858,2859,2860,2861,2862,2862,2863,2864,2866,2867,2869,2870,2872,2873,2874, 2876,2877,2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891, 2892,2893,2894,2895,2896,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907, 2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2919,2920,2921,2922, 2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2934,2935,2936,2937,2938, 2939,2940,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2955, 2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970, 2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2982,2983,2984,2985,2986, 2987,2988,2989,2991,2992,2993,2994,2995,2996,2998,2999,3000,3001,3002,3003, 3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019, 3020,3021,3022,3023,3024,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033, 3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3049, 3050,3051,3052,3053,3054,3055,3057,3060,3063,3065,3068,3071,3073,3074,3075, 3076,3077,3078,3079,3080,3082,3083,3084,3085,3086,3087,3088,3090,3091,3092, 3093,3094,3096,3097,3098,3099,3100,3102,3103,3104,3105,3106,3107,3107,3108, 3109,3110,3111,3111,3112,3113,3114,3115,3115,3116,3117,3118,3119,3120,3120, 3122,3123,3124,3125,3127,3128,3129,3130,3132,3133,3134,3135,3137,3138,3138, 3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153, 3154,3155,3156,3158,3159,3160,3161,3162,3163,3164,3166,3167,3168,3169,3170, 3171,3172,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3181,3182,3183, 3184,3185,3186,3187,3189,3190,3191,3192,3193,3194,3195,3196,3197,3199,3200, 3201,3201,3202,3203,3204,3205,3206,3206,3207,3208,3209,3210,3211,3211,3212, 3213,3214,3215,3216,3216,3218,3219,3220,3221,3222,3223,3224,3225,3226,3228, 3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3240,3241,3242, 3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3255,3256,3257,3258, 3259,3260,3261,3263,3264,3265,3266,3267,3268,3269,3269,3270,3271,3272,3273, 3274,3275,3276,3277,3278,3279,3280,3281,3282,3284,3285,3286,3288,3289,3290, 3292,3293,3294,3296,3297,3297,3298,3299,3300,3300,3301,3302,3303,3304,3304, 3305,3306,3307,3307,3308,3309,3310,3311,3311,3312,3313,3314,3315,3316,3317, 3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3326,3327,3328,3329,3330, 3331,3332,3332,3333,3334,3335,3336,3337,3337,3338,3339,3340,3341,3342,3342, 3343,3344,3345,3346,3347,3347,3348,3349,3350,3351,3352,3352,3353,3354,3355, 3356,3357,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369, 3370,3371,3372,3373,3374,3375,3376,3377,3378,3378,3379,3380,3381,3382,3383, 3384,3385,3386,3387,3388,3389,3390,3391,3392,3394,3395,3396,3397,3398,3400, 3401,3402,3403,3404,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416, 3417,3417,3418,3419,3420,3421,3422,3423,3424,3425,3425,3426,3427,3428,3428, 3429,3430,3430,3431,3432,3432,3433,3434,3434,3435,3436,3436,3437,3438,3438, 3439,3440,3441,3442,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452, 3453,3454,3455,3455,3456,3457,3458,3459,3460,3461,3462,3462,3463,3464,3465, 3466,3467,3468,3468,3469,3470,3471,3472,3473,3474,3474,3475,3476,3477,3478, 3478,3479,3480,3481,3482,3483,3483,3484,3485,3486,3487,3487,3488,3489,3490, 3491,3492,3493,3494,3495,3496,3497,3498,3498,3499,3500,3501,3502,3503,3504, 3505,3506,3507,3508,3508,3509,3510,3511,3512,3513,3514,3515,3516,3516,3517, 3518,3519,3520,3521,3522,3522,3523,3524,3525,3526,3526,3527,3528,3529,3529, 3530,3531,3532,3533,3533,3534,3535,3536,3536,3537,3538,3539,3540,3540,3541, 3542,3543,3544,3544,3545,3546,3547,3548,3548,3549,3550,3551,3552,3553,3553, 3554,3555,3556,3557,3557,3558,3559,3560,3561,3562,3562,3563,3564,3565,3566, 3567,3567,3568,3569,3569,3570,3571,3571,3572,3572,3573,3573,3574,3575,3575, 3576,3576,3577,3577,3578,3579,3579,3580,3580,3581,3581,3582,3583,3583,3584, 3584,3585,3586,3587,3587,3588,3589,3590,3591,3591,3592,3593,3594,3594,3595, 3596,3597,3598,3598,3599,3600,3601,3602,3602,3603,3604,3605,3606,3607,3608, 3608,3609,3610,3611,3612,3613,3614,3614,3615,3616,3617,3618,3618,3619,3620, 3620,3621,3622,3623,3623,3624,3625,3625,3626,3627,3627,3628,3629,3630,3630, 3631,3632,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,3643,3644, 3644,3645,3646,3647,3648,3649,3650,3650,3651,3652,3652,3653,3654,3655,3655, 3656,3657,3657,3658,3659,3660,3660,3661,3662,3662,3663,3664,3665,3665,3666, 3666,3667,3668,3668,3669,3669,3670,3671,3671,3672,3673,3673,3674,3674,3675, 3676,3676,3677,3678,3678,3679,3679,3680,3681,3681,3682,3683,3683,3684,3685, 3686,3686,3687,3688,3688,3689,3690,3691,3691,3692,3693,3693,3694,3695,3695, 3696,3697,3698,3699,3699,3700,3701,3702,3703,3703,3704,3705,3706,3707,3708, 3708,3709,3710,3711,3712,3712,3713,3714,3714,3715,3715,3716,3717,3717,3718, 3719,3719,3720,3721,3721,3722,3722,3723,3724,3724,3725,3726,3726,3727,3728, 3728,3729,3729,3730,3730,3731,3731,3732,3732,3733,3733,3734,3734,3735,3735, 3736,3736,3737,3737,3738,3738,3739,3739,3740,3740,3741,3741,3742,3742,3743, 3743,3744,3744,3745,3746,3746,3747,3748,3749,3749,3750,3751,3751,3752,3753, 3753,3754,3755,3756,3756,3757,3758,3758,3759,3760,3760,3761,3762,3763,3764, 3765,3766,3766,3767,3768,3769,3770,3771,3772,3773,3773,3774,3775,3776,3777, 3777,3778,3779,3779,3780,3780,3781,3782,3782,3783,3784,3784,3785,3786,3786, 3787,3787,3788,3789,3789,3790,3791,3791,3792,3793,3793,3794,3794,3795,3795, 3796,3797,3797,3798,3798,3799,3799,3800,3801,3801,3802,3802,3803,3803,3804, 3805,3805,3806,3806,3807,3808,3808,3809,3809,3810,3811,3811,3812,3813,3813, 3814,3815,3815,3816,3817,3817,3818,3819,3819,3820,3820,3821,3822,3822,3823, 3824,3824,3825,3826,3826,3827,3828,3828,3829,3830,3830,3831,3832,3832,3833, 3834,3834,3835,3836,3836,3837,3838,3838,3839,3840,3840,3841,3842,3842,3843, 3844,3844,3845,3845,3846,3847,3847,3848,3849,3849,3850,3850,3851,3852,3852, 3853,3854,3854,3855,3856,3856,3857,3857,3858,3858,3859,3859,3860,3861,3861, 3862,3862,3863,3863,3864,3864,3865,3866,3866,3867,3867,3868,3868,3869,3869, 3870,3870,3871,3872,3872,3873,3873,3874,3875,3875,3876,3877,3877,3878,3879, 3879,3880,3881,3881,3882,3882,3883,3884,3884,3885,3886,3886,3887,3888,3888, 3889,3890,3890,3891,3892,3892,3893,3894,3894,3895,3895,3896,3897,3897,3898, 3899,3899,3900,3901,3901,3902,3903,3903,3904,3905,3905,3906,3906,3907,3907, 3908,3908,3909,3910,3910,3911,3911,3912,3912,3913,3913,3914,3915,3915,3916, 3916,3917,3917,3918,3918,3919,3920,3920,3921,3921,3922,3923,3923,3924,3925, 3925,3926,3926,3927,3928,3928,3929,3930,3930,3931,3931,3932,3933,3933,3934, 3935,3935,3936,3936,3937,3937,3938,3938,3939,3939,3940,3940,3941,3941,3942, 3942,3943,3943,3944,3944,3945,3945,3946,3946,3947,3947,3948,3948,3949,3949, 3950,3950,3951,3951,3952,3952,3953,3954,3954,3955,3955,3956,3957,3957,3958, 3958,3959,3959,3960,3961,3961,3962,3962,3963,3963,3964,3965,3965,3966,3966, 3967,3967,3968,3969,3969,3970,3971,3971,3972,3972,3973,3974,3974,3975,3976, 3976,3977,3978,3978,3979,3980,3980,3981,3981,3982,3983,3983,3984,3985,3985, 3986,3986,3987,3987,3988,3988,3989,3989,3990,3990,3991,3991,3992,3992,3993, 3993,3994,3994,3995,3995,3996,3996,3997,3997,3998,3998,3999,3999,4000,4000, 4001,4001,4002,4003,4003,4004,4004,4005,4006,4006,4007,4007,4008,4009,4009, 4010,4010,4011,4012,4012,4013,4013,4014,4015,4015,4016,4016,4017,4018,4018, 4019,4019,4020,4021,4021,4022,4022,4023,4024,4024,4025,4025,4026,4027,4027, 4028,4029,4029,4030,4030,4031,4032,4032,4033,4034,4034,4035,4036,4036,4037, 4038,4039,4039,4040,4041,4042,4042,4043,4044,4044,4045,4046,4047,4047,4048 }; #endif /* __has_include("ADC_LUT.h") */ #define DEW_NUMCHANNELS 2 #ifndef DEW_THM0_PIN #define DEW_THM0_PIN 34 // Must be from GPIOs 32..39 #define DEW_THM1_PIN 39 // Must be from GPIOs 32..39 #define DEW_PWM0_PIN 14 // Any available ouput pin #define DEW_PWM1_PIN 26 // Any available ouput pin #define DEW_RELAY_THM0_PIN 36 // Regular THM0 pin used instead for AUXRELAY #define DEW_RELAY_THM1_PIN DEW_THM1_PIN #define DEW_RELAY_PWM0_PIN 12 // Regular PWM0 pin used instead for AUXRELAY #define DEW_RELAY_PWM1_PIN 18 // Conflicts with SPI (Ethernet, Host-USB), and Stepper Motor. #endif /* DEW_THM0_PIN */ // These pins might get rearranged at run-time by dew_setup(): static byte dew_thm_pins[DEW_NUMCHANNELS] = {DEW_THM0_PIN, DEW_THM1_PIN}; static byte dew_pwm_pins[DEW_NUMCHANNELS] = {DEW_PWM0_PIN, DEW_PWM1_PIN}; // Support up to four Dew Control "channels", by defining pairs of non-zero pin numbers here: static bool dew_enabled = false; static double dew_input_current_limit = 3.000; // amperage static byte dew_pwm_percent [DEW_NUMCHANNELS] = { 0, 0}; // percent 0..100 static byte dew_aggression [DEW_NUMCHANNELS] = { 5, 5}; // 1..10 (NVRAM) static byte dew_manual_pwm [DEW_NUMCHANNELS] = { 0, 0}; // 0..100 (NVRAM) static double dew_temperature [DEW_NUMCHANNELS] = { 0, 0}; // degrees static double dew_max_amperage [DEW_NUMCHANNELS] = { 0, 0}; // kept in NVRAM static byte dew_aggression_to_degreesC [11] = {0, 2, 3, 4, 5, 7, 9, 10, 12, 14, 15}; static bool thermistor_detected [DEW_NUMCHANNELS] = {false, false}; static double dew_input_voltage = 12.0000; // hardcoded, unless ads1115 channel-3 is wired to measure it static char dew_oled_rows[4][22] = {{0,},}; // To-Do: get rid of this someday to save RAM. static bool sht3x_detected = false; static bool sht3x_calibration_running = false; static long sht3x_calibration_heating_done = false; static long sht3x_calibration_timeout = 0; static double sht3x_temperature = 0; static double sht3x_relhumidity = 0; static double sht3x_dewpoint = 0.0; #define SHT3X_HEATING_SECS 480 #define SHT3X_COOLING_SECS 480 // Dew heater port numbering is weird. Things get confused when (DEW_NUMCHANNELS != 2) #define DEW_NUM_12V_PORTS 1 // MUST be "1". Haven't figured out the protocol when > 1. static byte double_to_4bytes (byte out[4], double val) { long v = val * 1000; out[3] = v; v >>= 8; out[2] = v; v >>= 8; out[1] = v; v >>= 8; out[0] = v; return 4; } static byte double_to_2bytes (byte out[2], double val) { long v = val * 1000; out[1] = v; out[0] = v >> 8; return 2; } static void dew_set_aggression (byte channel, byte aggr) { if (aggr > 10) aggr = 10; if (aggr != dew_aggression[channel]) { dew_aggression[channel] = aggr; char name[] = "dew0.aggression"; name[3] += channel; nvram_save_int(name, aggr); } } static void dew_handle_request (struct rxbuf_s *rxbuf, byte *data) { static byte ports_enabled[DEW_NUMCHANNELS + DEW_NUM_12V_PORTS] = {0,}; static byte led_brightness = 0x7f; byte reply[AUXBUS_PKT_MAX], len, op = data[4], dev = data[3]; len = emulate_begin("dew_rx", data, reply); if (dev == DEV_DEWBB) { switch (OP(op,data[1])) { case OP(DEV_GET_VERSION,3): { data[len++] = 1; // Version 1.1.1270 data[len++] = 1; data[len++] = (byte)(1270 >> 8); data[len++] = (byte)(1270); if (dew_debug) SERIAL_PRINTF("DEWBB GET_VERSION\r\n"); goto done; } case OP(DEV_GET_MODEL,3): { reply[len++] = 0x01; reply[len++] = 0x07; if (dew_debug) SERIAL_PRINTLN("DEWBB GET_MODEL"); goto done; } default: if (!auxbus_raw_tracing) SERIAL_PRINTF("DEWBB OP-0x%02x len=%u (unknown)\r\n", op, data[1]); } return; } switch (OP(op,data[1])) { case OP(DEV_GET_VERSION,3): { reply[len++] = 1; reply[len++] = 0; if (dew_debug) SERIAL_PRINTLN("DEW GET_VERSION 1.0"); break; } case OP(DEW_QUERY_INPUT_POWER,3): { len += double_to_2bytes(reply + len, dew_input_voltage); double amps = 0; for (byte channel = 0; channel < DEW_NUMCHANNELS; ++channel) amps += dew_pwm_percent[channel] * dew_max_amperage[channel] / 100; len += double_to_2bytes(reply + len, amps); reply[len++] = 0x00; // ?? reply[len++] = 0x00; // ?? if (dew_debug) SERIAL_PRINTLN("DEW QUERY_INPUT_POWER"); break; } case OP(DEW_GET_LED_BRIGHTNESS,3): { reply[len++] = led_brightness; if (dew_debug) SERIAL_PRINTF("DEW GET_LED_BRIGHTNESS %u\r\n", led_brightness); break; } case OP(DEW_SET_INPUT_LIMIT,5): { dew_input_current_limit = (((uint16_t)data[5]) << 8 | data[6]) / 1000.0; if (dew_debug) { byte amps = dew_input_current_limit; uint16_t mamps = (dew_input_current_limit - amps) * 1000; SERIAL_PRINTF("SET_INPUT_LIMIT %u.%03u\r\n", amps, mamps); } break; } case OP(DEW_QUERY_INPUT_LIMITS,3): { len += double_to_2bytes(reply + len, dew_input_current_limit); // Present amperage limit (configurable) len += double_to_2bytes(reply + len, 10.000); // Maximum amperage limit (fixed) if (dew_debug) SERIAL_PRINTLN("DEW QUERY_INPUT_LIMITS"); break; } case OP(DEW_GET_NUM_PORTS,3): { reply[len++] = DEW_NUMCHANNELS + DEW_NUM_12V_PORTS; if (dew_debug) SERIAL_PRINTF("DEW GET_NUM_PORTS %u\r\n", reply[len-1]); break; } case OP(DEW_QUERY_PORT,4): { byte p1 = data[5]; if (dew_debug) SERIAL_PRINTF("DEW QUERY_PORT(%u)\r\n", p1); if (p1 == 0x00) { reply[len++] = DEW_NUM_12V_PORTS; break; } if (p1 == 0x01) { reply[len++] = DEW_NUMCHANNELS; break; } if (p1 == 0xff) { reply[len++] = 0x00; // Status of 2-port USB Hub. break; } byte pnum = p1 - 1; if (pnum <= DEW_NUM_12V_PORTS) { reply[len++] = 0x10 | pnum; reply[len++] = ports_enabled[p1]; reply[len++] = 0x00; // ?? len += double_to_2bytes(reply + len, 0.000); // watts len += double_to_2bytes(reply + len, 12.000); // volts break; } reply[len++] = 0; // otherwise: no (0) ports of this type break; } case OP(DEW_QUERY_HEATER,4): { byte channel = data[5]; reply[len++] = channel + 1; if (channel < DEW_NUMCHANNELS) { reply[len++] = (dew_aggression[channel] != 0); reply[len++] = dew_pwm_percent[channel] * 2.55 + 0.5; // percent as fraction of 255 double amps = dew_pwm_percent[channel] * dew_max_amperage[channel] / 100; len += double_to_2bytes(reply + len, amps); reply[len++] = dew_aggression[channel]; // aggression 1..10 len += double_to_4bytes(reply + len, dew_temperature[channel]); } if (dew_debug) SERIAL_PRINTF("DEW QUERY_HEATER #%u\r\n", channel); break; } case OP(DEW_ENABLE_PORT,5): { byte port = data[5]; if (port >= 2 && port <= sizeof(ports_enabled)) ports_enabled[port] = data[6]; if (dew_debug) SERIAL_PRINTF("DEW ENABLE_PORT %u %u\r\n", port, data[6]); break; } case OP(DEW_SET_AUTO_AGGR,5): { byte channel = data[5]; byte aggr = data[6]; // aggression 1..10 if (channel < DEW_NUMCHANNELS && aggr >= 1 && aggr <= 10) { dew_set_aggression(channel, aggr); oled_dew_update(channel); } if (dew_debug) SERIAL_PRINTF("DEW SET_AUTO_AGGR[%u] %u\r\n", channel, aggr); break; } case OP(DEW_SET_MANUAL_PWM,5): { byte channel = data[5]; byte pwm = data[6]; // percent = (pwm / 255) byte percent = pwm / 2.55 + 0.5; if (channel < DEW_NUMCHANNELS) { dew_set_aggression(channel, 0); dew_set_pwm_percent(channel, percent); oled_dew_update(channel); } if (dew_debug) SERIAL_PRINTF("DEW SET_MANUAL_PWM[%u] %u (%u%%)\r\n", channel, pwm, percent); break; } case OP(DEW_QUERY_ENV_SENSORS,3): { len += double_to_4bytes(reply + len, sht3x_temperature); len += double_to_4bytes(reply + len, sht3x_dewpoint); reply[len++] = sht3x_relhumidity + 0.5; if (dew_debug) SERIAL_PRINTLN("DEW QUERY_ENV_SENSORS"); break; } case OP(DEW_RECALIBRATE_ENV_SENSOR,4): { byte p1 = data[5]; // 1 == perform recalibration; otherwise.. abort?? if (p1) { sht3x_begin_calibration(); } else if (sht3x_calibration_running) { if (sht3x_calibration_heating_done || sht3x_heating_off()) { sht3x_calibration_heating_done = true; sht3x_calibration_timeout = 0; sht3x_calibration_running = false; } } if (dew_debug) SERIAL_PRINTF("DEW RECALIBRATE_ENV_SENSOR (0x%02x) %s\r\n", p1, sht3x_calibration_running ? "on" : "off"); break; } case OP(DEW_QUERY_ENV_CALIBRATING,3): { reply[len++] = sht3x_calibration_running; if (dew_debug) { SERIAL_PRINT("DEW QUERY_ENV_CALIBRATING "); if (!sht3x_calibration_running) { SERIAL_PRINTLN("off"); } else { long secs = time_delta(millis(), sht3x_calibration_timeout) / 1000; SERIAL_PRINTF("%s, %lu secs remaining\r\n", sht3x_calibration_heating_done ? "cooldown" : "heating", secs); } } break; } case OP(DEW_SET_LED_BRIGHTNESS,4): { led_brightness = data[5]; if (dew_debug) SERIAL_PRINTF("DEW SET_LED_BRIGHTNESS %u\r\n", data[5]); break; } case OP(DEW_UNKNOWN_21,3): { reply[len++] = 0xff; // ?? Celestron 2X replies with this if (dew_debug) SERIAL_PRINTLN("DEW UNKNOWN_21"); break; } default: if (!auxbus_raw_tracing) SERIAL_PRINTF("DEW OP-0x%02x len=%u (unknown)\r\n", op, data[1]); } done: emulate_send_reply("dew_tx", rxbuf, reply, len); } #define SHT3X_I2C_ADDR 0x44 static bool sht3x_send_command (uint8_t MSB, uint8_t LSB) { Wire.setClock(100000ul); Wire.beginTransmission(SHT3X_I2C_ADDR); Wire.write(MSB); Wire.write(LSB); return Wire.endTransmission() == 0; } static inline bool sht3x_softreset() { return sht3x_send_command(0x30, 0xa2); } static inline bool sht3x_heating_on() { return sht3x_send_command(0x30, 0x6D); } static inline bool sht3x_heating_off() { return sht3x_send_command(0x30, 0x66); } // Begin Measurement: single, high-repeatability w/clock-stretch, 12.5msecs duration static inline bool sht3x_begin_measurment() { return sht3x_send_command(0x2c, 0x06); } static void sht3x_begin_calibration (void) { if (!sht3x_calibration_running) { int ret = sht3x_heating_on(); SERIAL_PRINTF("%s: heating_on: %d\r\n", __func__, ret); if (ret) { sht3x_calibration_running = true; sht3x_calibration_heating_done = false; sht3x_calibration_timeout = get_timeout(SHT3X_HEATING_SECS * 1000); } } } static long sht3x_measurement_timeout = 0; static bool sht3x_crc (uint8_t MSB, uint8_t LSB, uint8_t CRC) { uint8_t i, crc = 0xff; crc ^= MSB; for (i = 0; i < 8; i++) crc = crc & 0x80 ? (crc << 1) ^ 0x31 : crc << 1; crc ^= LSB; for (i = 0; i < 8; i++) crc = crc & 0x80 ? (crc << 1) ^ 0x31 : crc << 1; return (crc == CRC); } static inline double calc_dewpoint (double T, double RH) { const double a = 17.625, b = 243.04; double alpha = log(RH / 100.0) + ((a * T) / (b + T)); return (b * alpha) / (a - alpha); } static void sht3x_start_measurement (void) { if (!sht3x_measurement_timeout && !sht3x_calibration_running && sht3x_begin_measurment()) sht3x_measurement_timeout = get_timeout(25); // datasheet says 12.5msecs } static bool sht3x_fetch_measurement (void) { if (!sht3x_measurement_timeout) { sht3x_start_measurement(); return false; } if (time_before(millis(), sht3x_measurement_timeout)) return false; sht3x_measurement_timeout = 0; byte data[6]; Wire.requestFrom(SHT3X_I2C_ADDR, sizeof(data)); if (Wire.available() < sizeof(data)) { if (verbose) SERIAL_PRINTF("%s: available=%u\r\n", __func__, Wire.available()); return false; } for (byte i = 0; i < sizeof(data); i++) data[i] = Wire.read(); if (!sht3x_crc(data[0], data[1], data[2]) || !sht3x_crc(data[3], data[4], data[5])) { if (verbose) SERIAL_PRINTF("%s: bad CRC\r\n", __func__); return false; } double d; d = data[0] << 8; d += data[1]; sht3x_temperature = (d * (175.0 / 65535.0)) - 45.0; // tenths of degrees C d = ((unsigned)(data[3])) << 8; d += ((unsigned)(data[4])); sht3x_relhumidity = d * (100.0 / 65535.0); // percent sht3x_dewpoint = calc_dewpoint(sht3x_temperature, sht3x_relhumidity); sht3x_start_measurement(); // for next time return true; } #define PWM_FREQUENCY 4 // Same as what Celestron uses #define PWM_RESOLUTION 8 // 8-bits is enough here static void dew_set_pwm_percent (int channel, uint percent) { dew_pwm_percent[channel] = percent; byte duty_cycle = percent * ((1 << PWM_RESOLUTION) - 1) / 100; pwm_set_duty_cycle(channel, duty_cycle); if (dew_aggression[channel] == 0) { // Manual mode? if (percent != dew_manual_pwm[channel]) { dew_manual_pwm[channel] = percent; char name[] = "dew0.manual.pwm"; name[3] += channel; nvram_save_int(name, percent); } } } static void dew_pwm_setup (void) { for (byte channel = 0; channel < DEW_NUMCHANNELS; ++channel) { if (dew_pwm_pins[channel]) { pinMode(dew_pwm_pins[channel], OUTPUT); pwm_enable(channel); dew_set_pwm_percent(channel, 0); } } } //////////////////////////// begin ADS1115 ADC Support //////////////////////////////////////////////// #define ADS1115_I2C_ADDR 0x48 #define ADS1115_CONV_REG 0x00 #define ADS1115_CFG_REG 0x01 //#define ADS1115_CFG_VAL 0xc383 // 128 SPS, 8msec response (default) //#define ADS1115_CFG_VAL 0xc3c3 // 475 SPS, 2msec response #define ADS1115_CFG_VAL 0xc3e3 // 860 SPS, 1msec response static uint8_t Wire_write16 (byte addr, uint8_t reg, uint16_t val) { Wire.beginTransmission(addr); Wire.write(reg); Wire.write(val >> 8); Wire.write(val & 0xff); return Wire.endTransmission(); } static uint16_t Wire_read16 (byte addr, uint8_t reg) { Wire.beginTransmission(addr); Wire.write(reg); Wire.endTransmission(false); Wire.requestFrom(ADS1115_I2C_ADDR, 2); if (Wire.available()) { uint16_t a = Wire.read(); return (a << 8) | Wire.read(); } return 0; } static bool ads1115_detected = false; static void ads1115_setup (void) { Wire.setClock(800000ul); Wire.beginTransmission(ADS1115_I2C_ADDR); ads1115_detected = (Wire.endTransmission() == 0); SERIAL_PRINTF("ADS1115 %sfound!\r\n", ads1115_detected ? "" : "not "); } static int16_t ads1115_read_ADC (uint16_t channel) // 0,1,2,3 { static int16_t pinvals[4] = {0,}; // for maintaning a 2-sample average for each ADC Wire_write16(ADS1115_I2C_ADDR, ADS1115_CFG_REG, ADS1115_CFG_VAL | (channel << 12)); while (!(Wire_read16(ADS1115_I2C_ADDR, ADS1115_CFG_REG) & 0x8000)); int16_t v = Wire_read16(ADS1115_I2C_ADDR, ADS1115_CONV_REG); if (pinvals[channel]) pinvals[channel] = (pinvals[channel] + v) / 2; // keep a running average of 2 samples else pinvals[channel] = v; return pinvals[channel]; } static int16_t ads1115_vref = 26112; // 3.3V static int16_t ads1115_read_thermistor (byte channel) // 0,1 { static byte counter = 7; if ((++counter % 8) == 0) // periodically re-sample the "3.3V" value on channel-0 ads1115_vref = ads1115_read_ADC(0); int16_t pinval = ads1115_read_ADC(1 + channel); if (pinval > ads1115_vref) pinval = ads1115_vref; return pinval; } static void ads1115_read_dew_input_voltage (void) { int16_t pinval = ads1115_read_ADC(3); if (pinval >= 0x1000 && pinval <= 0x7000) { double v = (4.096 / (double)0x7fff) * (double)pinval; dew_input_voltage = v * 5; // external 1:5 voltage divider keeps input levels safe for ADS1115 if (dew_debug) SERIAL_PRINTF("%s: pinval=%u/%u v=%f V=%f\r\n", __func__, pinval, ads1115_vref, v, dew_input_voltage); } } //////////////////////////// end ADS1115 ADC Support //////////////////////////////////////////////// /* * Get ADC Voltage Reference. Note that the actual value doesn't make any difference though, * but doing the call to esp_adc_cal_characterize() does seem to improve readings slightly, * and also seems to reduce occurances of obviously "bad" readings. */ #include "esp_adc_cal.h" static void get_esp32_adc_vref (void) { esp_adc_cal_characteristics_t adc_chars; esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars); } static double pinval_to_degreesC (int16_t pinval, int16_t pinmax) { const double R1 = 10000.0; // voltage divider resistor value const double Beta = 3950.0; // Beta value const double Koffset = 273.15; // Degrees Kelvin for zero degrees Celsius const double Ro = 10000.0; // Resistance of Thermistor at 25 degree Celsius double Rt = R1 * pinval / (pinmax - pinval); double Tk = 1.0 / ( (1.0 / (25.0 + Koffset)) + (log(Rt / Ro) / Beta)); // degrees Kelvin return Tk - Koffset; // degrees Celsius } static void dew_read_thermistor (byte channel) { static int32_t sums [DEW_NUMCHANNELS] = {0,}; static byte samples [DEW_NUMCHANNELS] = {0,}; int16_t pinval, pinavg, pinmax = ads1115_detected ? ads1115_vref : 4095; if (ads1115_detected) { pinval = ads1115_read_thermistor(channel); } else if (dew_thm_pins[channel]) { pinval = analogRead(dew_thm_pins[channel]); pinval = ADC_LUT[pinval]; } else { goto bad_reading; } if (pinval < (pinmax / 16) || pinval >= (pinmax - (pinmax / 16))) { if (dew_debug && samples[channel]) SERIAL_PRINTF("Th%u: Bad reading=%d\r\n", channel, pinval); goto bad_reading; } if (samples[channel] < 4) samples[channel] += 1; else sums[channel] -= sums[channel] / samples[channel]; sums[channel] += pinval; pinavg = sums[channel] / samples[channel]; dew_temperature[channel] = pinval_to_degreesC(pinavg, pinmax); if (dew_temperature[channel] <= 45.0 && dew_temperature[channel] >= -40.0) { thermistor_detected[channel] = true; return; } if (dew_debug) SERIAL_PRINTF("Th%u: Bad reading=%d %lf\r\n", channel, pinval, dew_temperature[channel]); bad_reading: thermistor_detected[channel] = false; sums[channel] = samples[channel] = 0; dew_set_pwm_percent(channel, dew_aggression[channel] ? 0 : dew_manual_pwm[channel]); } static void dew_tenths (const char *prefix, double T, const char *suffix, bool pm) { int t, degrees, tenths; t = T * 10.0; tenths = t; degrees = t / 10; tenths = abs(tenths - (degrees * 10)); SERIAL_PRINTF(pm ? "%s%+d.%u%s" : "%s%d.%u%s", prefix, degrees, tenths, suffix); } static void dew_oled_show_aggr (byte channel) { #if OLED_ENABLED char *out = dew_oled_rows[2 + channel] + 12; byte aggr = dew_aggression_to_degreesC[dew_aggression[channel]]; if (aggr > 9) { *out++ = '+'; *out++ = '1'; *out++ = '0' + (aggr - 10); } else { *out++ = ' '; *out++ = '+'; *out++ = '0' + aggr; } *out++ = 'C'; #endif /* OLED_DETECTED */ } static void dew_oled_show_intensity (byte channel) { #if OLED_ENABLED if (dew_aggression[channel]) { sprintf(dew_oled_rows[2 + channel] + 12, "+XXC %3u%%", dew_pwm_percent[channel]); dew_oled_show_aggr(channel); } else { sprintf(dew_oled_rows[2 + channel] + 12, "Man: %3u%%", dew_pwm_percent[channel]); } #endif /* OLED_DETECTED */ } static void oled_dew_update (byte channel) { #if OLED_ENABLED int t, degrees, tenths; t = dew_temperature[channel] * 10.0; tenths = t; degrees = t / 10; tenths = abs(tenths - (degrees * 10)); if (thermistor_detected[channel]) sprintf(dew_oled_rows[2 + channel], "Ch%u: %3d.%uC ", channel, degrees, tenths); else sprintf(dew_oled_rows[2 + channel], "Ch%u: ", channel); dew_oled_show_intensity(channel); #endif /* OLED_DETECTED */ } static void dew_manage_channel (byte channel) { double target, delta; uint pwm; dew_read_thermistor(channel); if (!dew_aggression[channel]) { pwm = dew_manual_pwm[channel]; } else { pwm = 0; if (thermistor_detected[channel]) { if (!sht3x_calibration_running) { target = sht3x_dewpoint + dew_aggression_to_degreesC[dew_aggression[channel]]; delta = target - dew_temperature[channel]; if (delta > 0) pwm = (delta <= 1.5) ? delta * 50 + 20 : 100; } } } dew_set_pwm_percent(channel, pwm); // This also does: dew_pwm_percent[channel] = pwm; if (dew_debug) { char pm = (delta <= 0) ? '+' : '-'; SERIAL_PRINTF("Ch%u: ", channel); dew_tenths("Ambient=", sht3x_temperature, "C", false); dew_tenths(" RH=", sht3x_relhumidity, "%", false); dew_tenths(" DewPt=", sht3x_dewpoint, "C", false); dew_tenths(" Target=", target, "C", false); if (thermistor_detected[channel]) { dew_tenths(" Temp=", dew_temperature[channel], "C", false); dew_tenths(" delta=", -delta, "C", true); } else { SERIAL_PRINTF(" Temp=??C delta=??C"); } SERIAL_PRINTF(" PWM=%s.%u%%\r\n", dew_aggression[channel] ? "auto" : "manual", pwm); } oled_dew_update(channel); } static void dew_loop (void) { static long nexttime = 0; if (nexttime && time_before(millis(), nexttime)) return; nexttime = get_timeout(3000); if (sht3x_calibration_running) { if (OLED_ENABLED) { sprintf(dew_oled_rows[0], "Calibrating.."); long secs = time_delta(millis(), sht3x_calibration_timeout) / 1000; sprintf(dew_oled_rows[1], "%s %lu", sht3x_calibration_heating_done ? "cooldown" : "heating", secs); } if (sht3x_calibration_timeout) { if (time_after(millis(), sht3x_calibration_timeout)) { if (sht3x_calibration_heating_done) { sht3x_calibration_running = false; sht3x_calibration_timeout = 0; SERIAL_PRINTF("%s: cooldown complete\r\n", __func__); } else { if (!sht3x_heating_off()) { delay(5); sht3x_softreset(); delay(5); } sht3x_calibration_heating_done = true; sht3x_calibration_timeout = get_timeout(SHT3X_COOLING_SECS * 1000); SERIAL_PRINTF("%s: heating_off\r\n", __func__); } } } } if (sht3x_detected && !sht3x_fetch_measurement()) { nexttime = get_timeout(100); return; } if (ads1115_detected) { Wire.setClock(800000ul); ads1115_read_dew_input_voltage(); } if (OLED_ENABLED) { int t, degrees, tenths; t = sht3x_dewpoint * 10.0; tenths = t; degrees = t / 10; tenths = abs(tenths - (degrees * 10)); sprintf(dew_oled_rows[0], "Dewpoint: %3d.%uC", degrees, tenths); t = sht3x_temperature * 10.0; tenths = t; degrees = t / 10; tenths = abs(tenths - (degrees * 10)); sprintf(dew_oled_rows[1], "Air: %3d.%uC RH: %3u%%", degrees, tenths, (int)(sht3x_relhumidity + 0.5)); } static byte channel = DEW_NUMCHANNELS - 1; channel = (channel + 1) % DEW_NUMCHANNELS; dew_manage_channel(channel); // Manages a single channel per call, cycling through them over time } static void dew_setup (void) { ads1115_setup(); dew_enabled |= ads1115_detected; sht3x_detected = false; if (auxrelay_detected) { // Switch to alternate Dew Control pins, to avoid conflicts with auxrelay pins: #ifdef AIO_H dew_thm_pins[1] = 0; // Manual-only for Channel-1; no alternate ADC1 pin available on AIO. #else dew_thm_pins [0] = DEW_RELAY_THM0_PIN; dew_thm_pins [1] = DEW_RELAY_THM1_PIN; dew_pwm_pins [0] = DEW_RELAY_PWM0_PIN; dew_pwm_pins [1] = DEW_RELAY_PWM1_PIN; // Pin conflicts are still possible with Dew Channel-1 though. Try and sort out the mess: #if !ETHERNET_ENABLED #define ethernet_detected false #endif #if EMULATE_FOCUS #define DEW1_PIN_CONFLICT (ethernet_detected || stepper_driver != STEPPER_DRIVER_NONE) #else #define DEW1_PIN_CONFLICT (ethernet_detected) #endif if (DEW1_PIN_CONFLICT) dew_pwm_pins[1] = dew_thm_pins[1] = 0; // No pin available for pwm1: disable Channel-1 completely. #endif /* !AIO_H */ } get_esp32_adc_vref(); if (sht3x_softreset() || sht3x_softreset() || sht3x_softreset()) { sht3x_detected = dew_enabled = true; SERIAL_PRINTLN("SHT3X sensor detected, enabling Dew Heater Control"); SERIAL_PRINTF("dew0.amps=%4.2lf dew1.amps=%4.2lf\r\n", dew_max_amperage[0], dew_max_amperage[1]); } else if (!dew_enabled) { SERIAL_PRINTLN("SHT3X sensor NOT detected, disabling Dew Heater Control."); } else { SERIAL_PRINTLN("SHT3X Dew sensor not detected; Dew control will be manual only."); sht3x_temperature = 18.0; sht3x_relhumidity = 50.0; sht3x_dewpoint = calc_dewpoint(sht3x_temperature, sht3x_relhumidity); } if (dew_enabled) { dew_pwm_setup(); if (!dew_pwm_pins[1]) SERIAL_PRINTLN("Dew Channel-1 disabled (pin conflicts)."); else if (!dew_thm_pins[1]) SERIAL_PRINTLN("Dew Channel-1 manual control only, no thermistor (pin conflicts)."); } } static double dew_nvram_restore_amperage (byte channel) { double amps = 1.7; // default value char name[16]; sprintf(name, "dew%u.amps", channel); const char *val = nvram_get_val(name); if (val && *val) { double a = atof(val); if (a > 0 && a <= 5.0) amps = a; } return amps; } #endif /* EMULATE_DEW */ // *** End DEW support **************************************************************************************************** // *** Begin OLED support **************************************************************************************************** #if OLED_ENABLED #include static struct oled_type_s { const char *name; const DevType *oled; } oled_types[] = { {"Adafruit128x64", &Adafruit128x64}, {"Adafruit128x32", &Adafruit128x32}, {"SH1106_128x64", &SH1106_128x64}, {NULL, NULL} }; static int oled_timeout_secs = 120; // (default) 60-seconds inactivity timeout for turning off the OLED // Set oled type in NVRAM. Eg. set oled.type SH1106_128x64 // static const DevType *oled_type = oled_types[0].oled; static void oled_get_type (void) { const char *oled_name = nvram_get_val("oled.type"); byte oled_index = 0; // Default if (oled_name && oled_name[0]) { for (byte i = 0; oled_types[i].name; ++i) { if (0 == strcasecmp(oled_name, oled_types[i].name)) { oled_index = i; break; } } } oled_type = oled_types[oled_index].oled; SERIAL_PRINTF("oled.type = %s\r\n", oled_types[oled_index].name); if (oled_index != 0) oled_setup(); } #define OLED_I2C_ADDR 0x3c static SSD1306AsciiWire oled; #define OLED_ROW_CHARS 22 // only 21 chars are usable.. last byte is only about 2 columns wide #define OLED_NMODES (((byte)EMULATE_DEW) + (2*(byte)EMULATE_GPS) + (byte)EMULATE_FOCUS + ((byte)ETHERNET_ENABLED) + 3) // Network, OTA, and QRcode are the other modes static bool (*oled_updatefn[OLED_NMODES])(byte); // oled_arg_t typedef enum {oled_firstcall, oled_normal, oled_button_pressed, oled_button_timer_expired} oled_arg_t; static char oled_data[4][OLED_ROW_CHARS + 1]; // Four rows of OLED data static bool oled_data_updated[4] = {false,false,false,false}; static bool oled_turned_off = false; static long oled_offtime = 0; static long oled_splash_timer = 0; static long oled_button_timer = 0; // for longpress timings in various updatefn()'s static byte oled_mode = 0; static const uint8_t HomeBrew5x14_font[] = { // Two groups of 5-bytes-across for each character. Each byte is vertical pixels. 0x0, 0x0, 5, 14, 0x20, 0x61, // Final entry (code 0x80) is degrees symbol 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xff, 0x30, 0xff, 0x30, 0x03, 0x3f, 0x03, 0x3f, 0x03, 0x30, 0xcc, 0xff, 0xcc, 0x0c, 0x0c, 0x0c, 0x3f, 0x0c, 0x03, 0x0f, 0x0f, 0xc0, 0x30, 0x0c, 0x0c, 0x03, 0x00, 0x3c, 0x3c, 0x3c, 0xc3, 0x33, 0x0c, 0x00, 0x0f, 0x30, 0x33, 0x0c, 0x33, 0x00, 0x33, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0c, 0x03, 0x00, 0x00, 0x03, 0x0c, 0x30, 0x00, 0x00, 0x03, 0x0c, 0xf0, 0x00, 0x00, 0x30, 0x0c, 0x03, 0x00, 0xc0, 0xcc, 0xf0, 0xcc, 0xc0, 0x00, 0x0c, 0x03, 0x0c, 0x00, 0xc0, 0xc0, 0xfc, 0xc0, 0xc0, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x0f, 0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x3c, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x30, 0x0c, 0x0c, 0x03, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc3, 0x33, 0xfc, 0x0f, 0x33, 0x30, 0x30, 0x0f, 0x00, 0x0c, 0xff, 0x00, 0x00, 0x00, 0x30, 0x3f, 0x30, 0x00, 0x0c, 0x03, 0x03, 0xc3, 0x3c, 0x30, 0x3c, 0x33, 0x30, 0x30, 0x03, 0x03, 0x33, 0xcf, 0x03, 0x0c, 0x30, 0x30, 0x30, 0x0f, 0xc0, 0x30, 0x0c, 0xff, 0x00, 0x03, 0x03, 0x03, 0x3f, 0x03, 0x3f, 0x33, 0x33, 0x33, 0xc3, 0x0c, 0x30, 0x30, 0x30, 0x0f, 0xf0, 0xcc, 0xc3, 0xc3, 0x00, 0x0f, 0x30, 0x30, 0x30, 0x0f, 0x03, 0x03, 0xc3, 0x33, 0x0f, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3c, 0xc3, 0xc3, 0xc3, 0x3c, 0x0f, 0x30, 0x30, 0x30, 0x0f, 0x3c, 0xc3, 0xc3, 0xc3, 0xfc, 0x00, 0x30, 0x30, 0x0c, 0x03, 0x00, 0x3c, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x3c, 0x3c, 0x00, 0x00, 0x00, 0x33, 0x0f, 0x00, 0x00, 0x00, 0xc0, 0x30, 0x0c, 0x03, 0x00, 0x00, 0x03, 0x0c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x0c, 0x30, 0xc0, 0x00, 0x30, 0x0c, 0x03, 0x00, 0x00, 0x0c, 0x03, 0x03, 0xc3, 0x3c, 0x00, 0x00, 0x33, 0x00, 0x00, 0x0c, 0xc3, 0xc3, 0x03, 0xfc, 0x0f, 0x30, 0x3f, 0x30, 0x0f, 0xfc, 0x03, 0x03, 0x03, 0xfc, 0x3f, 0x03, 0x03, 0x03, 0x3f, 0xff, 0xc3, 0xc3, 0xc3, 0x3c, 0x3f, 0x30, 0x30, 0x30, 0x0f, 0xfc, 0x03, 0x03, 0x03, 0x0c, 0x0f, 0x30, 0x30, 0x30, 0x0c, 0xff, 0x03, 0x03, 0x0c, 0xf0, 0x3f, 0x30, 0x30, 0x0c, 0x03, 0xff, 0xc3, 0xc3, 0xc3, 0x03, 0x3f, 0x30, 0x30, 0x30, 0x30, 0xff, 0xc3, 0xc3, 0x03, 0x03, 0x3f, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x03, 0x03, 0x0c, 0x0f, 0x30, 0x30, 0x33, 0x0f, 0xff, 0xc0, 0xc0, 0xc0, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x03, 0xff, 0x03, 0x00, 0x00, 0x30, 0x3f, 0x30, 0x00, 0x00, 0x00, 0x03, 0xff, 0x03, 0x0c, 0x30, 0x30, 0x0f, 0x00, 0xff, 0xc0, 0x30, 0x0c, 0x03, 0x3f, 0x00, 0x03, 0x0c, 0x30, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x30, 0x30, 0x30, 0x30, 0xff, 0x0c, 0x30, 0x0c, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x30, 0xc0, 0x00, 0xff, 0x3f, 0x00, 0x00, 0x03, 0x3f, 0xfc, 0x03, 0x03, 0x03, 0xfc, 0x0f, 0x30, 0x30, 0x30, 0x0f, 0xff, 0xc3, 0xc3, 0xc3, 0x3c, 0x3f, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x03, 0x03, 0xfc, 0x0f, 0x30, 0x33, 0x0c, 0x33, 0xff, 0xc3, 0xc3, 0xc3, 0x3c, 0x3f, 0x00, 0x03, 0x0c, 0x30, 0x3c, 0xc3, 0xc3, 0xc3, 0x03, 0x30, 0x30, 0x30, 0x30, 0x0f, 0x03, 0x03, 0xff, 0x03, 0x03, 0x00, 0x00, 0x3f, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x0f, 0x30, 0x30, 0x30, 0x0f, 0xff, 0x00, 0x00, 0x00, 0xff, 0x03, 0x0c, 0x30, 0x0c, 0x03, 0xff, 0x00, 0xc0, 0x00, 0xff, 0x3f, 0x0c, 0x03, 0x0c, 0x3f, 0x0f, 0x30, 0xc0, 0x30, 0x0f, 0x3c, 0x03, 0x00, 0x03, 0x3c, 0x0f, 0x30, 0xc0, 0x30, 0x0f, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x03, 0x03, 0xc3, 0x33, 0x0f, 0x3c, 0x33, 0x30, 0x30, 0x30, 0x00, 0x00, 0xff, 0x03, 0x03, 0x00, 0x00, 0x3f, 0x30, 0x30, 0x0c, 0x30, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0c, 0x03, 0x03, 0xff, 0x00, 0x00, 0x30, 0x30, 0x3f, 0x00, 0x00, 0x30, 0x0c, 0x03, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x03, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0xc0, 0x0c, 0x33, 0x33, 0x33, 0x3f, 0xff, 0xc0, 0x30, 0x30, 0xc0, 0x3f, 0x30, 0x30, 0x30, 0x0f, 0xc0, 0x30, 0x30, 0x30, 0x00, 0x0f, 0x30, 0x30, 0x30, 0x0c, 0xc0, 0x30, 0x30, 0xc0, 0xff, 0x0f, 0x30, 0x30, 0x30, 0x3f, 0xc0, 0x30, 0x30, 0x30, 0xc0, 0x0f, 0x33, 0x33, 0x33, 0x03, 0xc0, 0xfc, 0xc3, 0x03, 0x0c, 0x00, 0x3f, 0x00, 0x00, 0x00, 0xc0, 0x30, 0x30, 0x30, 0xf0, 0x00, 0x03, 0x33, 0x33, 0x0f, 0xff, 0xc0, 0x30, 0x30, 0xc0, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x30, 0xf3, 0x00, 0x00, 0x00, 0x30, 0x3f, 0x30, 0x00, 0x00, 0x00, 0x30, 0xf3, 0x00, 0x0c, 0x30, 0x30, 0x0f, 0x00, 0x00, 0xff, 0x00, 0xc0, 0x30, 0x00, 0x3f, 0x03, 0x0c, 0x30, 0x00, 0x03, 0xff, 0x00, 0x00, 0x00, 0x30, 0x3f, 0x30, 0x00, 0xf0, 0x30, 0xc0, 0x30, 0xc0, 0x3f, 0x00, 0x03, 0x00, 0x3f, 0xf0, 0xc0, 0x30, 0x30, 0xc0, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x30, 0x30, 0x30, 0xc0, 0x0f, 0x30, 0x30, 0x30, 0x0f, 0xf0, 0x30, 0x30, 0x30, 0xc0, 0x3f, 0x03, 0x03, 0x03, 0x00, 0xc0, 0x30, 0x30, 0xc0, 0xf0, 0x00, 0x03, 0x03, 0x03, 0x3f, 0xf0, 0xc0, 0x30, 0x30, 0xc0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x30, 0x30, 0x30, 0x00, 0x30, 0x33, 0x33, 0x33, 0x0c, 0x30, 0xff, 0x30, 0x00, 0x00, 0x00, 0x0f, 0x30, 0x30, 0x0c, 0xf0, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x30, 0x30, 0x0c, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0xf0, 0x03, 0x0c, 0x30, 0x0c, 0x03, 0xf0, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x30, 0x0f, 0x30, 0x0f, 0x30, 0xc0, 0x00, 0xc0, 0x30, 0x30, 0x0c, 0x03, 0x0c, 0x30, 0xf0, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x33, 0x33, 0x33, 0x0f, 0x30, 0x30, 0x30, 0xf0, 0x30, 0x30, 0x3c, 0x33, 0x30, 0x30, 0x00, 0xc0, 0x3c, 0x03, 0x00, 0x00, 0x00, 0x0f, 0x30, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x03, 0x3c, 0xc0, 0x00, 0x00, 0x30, 0x0f, 0x00, 0x00, 0xc0, 0xc0, 0xcc, 0xf0, 0xc0, 0x00, 0x00, 0x0c, 0x03, 0x00, 0xc0, 0xf0, 0xcc, 0xc0, 0xc0, 0x00, 0x03, 0x0c, 0x00, 0x00, 0x00, 0x3c, 0xc3, 0xc3, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, }; static bool oled_write_row (int row) { if (!oled_data_updated[row]) return false; oled_data_updated[row] = false; oled.setRow(row * ((oled_type->lcdHeight == 32) ? 1 : 2)); oled.setCol(0); oled.print(oled_data[row]); return true; } static void oled_set_row (int row, const char *s) { char tmp[OLED_ROW_CHARS + 1]; byte n = strlen(s); if (n > OLED_ROW_CHARS) n = OLED_ROW_CHARS; strncpy(tmp, s, n); while (n < OLED_ROW_CHARS) tmp[n++] = ' '; tmp[n] = 0; if (strcmp(tmp, oled_data[row])) { strcpy(oled_data[row], tmp); oled_data_updated[row] = true; } } static void oled_clear (byte from_row) { switch (from_row) { case 0: oled_set_row(0, ""); case 1: oled_set_row(1, ""); case 2: oled_set_row(2, ""); default: oled_set_row(3, ""); } } static void oled_force_clear (void) { memset(oled_data, ' ', sizeof(oled_data)); oled_clear(0); } #if EMULATE_GPS #if EMULATE_FOCUS static bool focus_updatefn (byte arg) // oled_arg_t { static byte which_screen; static bool got_action, draw_screen; static ulong p; static long final_delay, next_poll = 0; char tmp[OLED_ROW_CHARS + 1]; if (stepper_driver == STEPPER_DRIVER_NONE) return false; // skip this screen if (arg == oled_firstcall) { next_poll = 0; got_action = false; which_screen = 0; final_delay = 0; draw_screen = true; } else if (next_poll && time_before(millis(), next_poll)) { return true; } next_poll = get_timeout(500); // Updating OLED REALLY interferes with the stepper motor! if (which_screen == 0) { if (draw_screen) { draw_screen = false; oled_set_row(0, "Focus Motor:"); oled_set_row(2, "Push+Hold button"); oled_set_row(3, " to Begin Calibration."); } if (arg == oled_normal) { if (got_action) { got_action = false; which_screen = 1; focus_set_calibration(0, 0); draw_screen = true; next_poll = 0; } } else if (arg == oled_button_pressed && !oled_button_timer) { oled_button_timer = get_timeout(700); } else if (arg == oled_button_timer_expired) { got_action = true; oled_set_row(2, ""); oled_set_row(3, "Okay. Let Go Now."); } goto show_position; } if (draw_screen) { draw_screen = false; if (which_screen == 1) { focus_limit_min = FOCUS_LIMIT_MIN; oled_set_row(0, "Focus Min-Limit: "); oled_set_row(2, "Move motor to Min.."); } else if (which_screen == 2) { focus_limit_max = FOCUS_LIMIT_MAX; oled_set_row(0, "Focus Max-Limit: "); oled_set_row(2, "Move motor to Max"); } oled_set_row(3, " Push+Hold to Set."); } if (arg == oled_normal) { if (got_action) { got_action = false; draw_screen = true; next_poll = 0; if (++which_screen == 3) { focus_set_calibration(focus_limit_min, focus_limit_max); which_screen = 0; } } } else if (arg == oled_button_pressed && !oled_button_timer) { oled_button_timer = get_timeout(1000); } else if (arg == oled_button_timer_expired) { got_action = true; if (which_screen == 1) focus_limit_min = p; else if (which_screen == 2) focus_limit_max = p; oled_set_row(2, ""); oled_set_row(3, "Okay. Let Go Now."); } show_position: p = focus_currentPosition(); snprintf(tmp, OLED_ROW_CHARS, "%5lu<< %5lu >>%-5lu", focus_limit_min, p, focus_limit_max); oled_set_row(1, tmp); return true; } #endif /* EMULATE_FOCUS */ static bool oled_location_updatefn (byte arg) // oled_arg_t { static bool got_action, saving; static long final_delay; if (!gps_detected) return false; // skip this screen if (arg == oled_firstcall) { byte fix_type = gps_get_fix_type(); saving = (fix_type == true_fix || fix_type == recent_fix); if (!saving && !gps_has_saved_location()) return false; // skip this screen got_action = false; final_delay = 0; oled_set_row(0, saving ? "Save GPS Location?" : "Restore GPS Location?"); } if (arg == oled_button_pressed && !oled_button_timer) oled_button_timer = get_timeout(1200); else if (arg == oled_button_timer_expired) got_action = true; oled_set_row(1, "=================="); if (!got_action) { oled_set_row(2, "Press and hold"); oled_set_row(3, saving ? " to Save:" : " to Restore:"); } else if (!final_delay) { oled_clear(3); final_delay = get_timeout(1500); if (saving) { oled_set_row(2, "SAVED."); gps_save_location(); } else { oled_set_row(2, "RESTORED."); gps_restore_location(); } } else if (time_after(millis(), final_delay)) { oled_mode -= 2; // go back to main GPS status display return false; } return true; } #define OLED_DEGREES 0x80 static byte oled_degreesym = OLED_DEGREES; static void gps_dddmmss (byte fix_type, char *tmp, const char *prefix, const char colon, const char compass[2], double f) { if (fix_type == no_fix) { snprintf(tmp, OLED_ROW_CHARS, "%s: ---%c -- -- -- -", prefix, oled_degreesym); } else { byte C = compass[0], ddd, mm, ss, ff; unsigned int SS; if (f < 0) { C = compass[1]; f = -f; } // Format decimal degrees ddd.ddddd into DDD°MM'SS.ss" C, similar to Celestron hand-controller displays. ddd = f; // degrees f = (f - ddd) * 3600; // remainder (fraction of a degree) in seconds SS = f; // seconds 0..3600 ff = (f - SS) * 100; // remainder (fraction of a second) in decimal mm = SS / 60; // minutes 0..59 ss = SS % 60; // seconds 0..59 snprintf(tmp, OLED_ROW_CHARS, "%s%c %3u%c %02u'%02u.%02u\" %c", prefix, colon, ddd, oled_degreesym, mm, ss, ff, C); } } static bool oled_gps_updatefn (byte arg) // oled_arg_t { static const char NS[2] = {'N','S'}, EW[2] = {'E','W'}; if (!gps_detected) return false; char tmp[OLED_ROW_CHARS + 1]; tmp[OLED_ROW_CHARS] = 0; snprintf(tmp, OLED_ROW_CHARS, "GPS Sats: %02u/%02u", gps.satellites.value(), gps.satellitesInView()); oled_set_row(0, tmp); double lat, lng; byte fix_type = gps_get_lat_lng(false, &lat, &lng); char colon = ':'; if (fix_type == recent_fix) colon = '-'; else if (fix_type == saved_fix) colon = '?'; gps_dddmmss(fix_type, tmp, "Lat", colon, NS, lat); oled_set_row(1, tmp); gps_dddmmss(fix_type, tmp, "Lon", colon, EW, lng); oled_set_row(2, tmp); int n; if (gps.date.is_valid()) n = snprintf(tmp, OLED_ROW_CHARS, "%02d/%02d/%02d UTC ", gps.date.day(), gps.date.month(), gps.date.year() % 100); #if NTP_ENABLED else if (ntp_time_is_valid) { ntp_fast_update_secs = 60; n = snprintf(tmp, OLED_ROW_CHARS, "%02d/%02d/%02d NTP ", ntp_time.day, ntp_time.month, ntp_time.year % 100); } #endif /* NTP_ENABLED */ else n = snprintf(tmp, OLED_ROW_CHARS, "%s", "--/--/-- UTC "); byte hms[3]; if (gps.time.getHMS(hms)) snprintf(tmp + n, OLED_ROW_CHARS - n, "%02d:%02d:%02d", hms[0], hms[1], hms[2]); else snprintf(tmp + n, OLED_ROW_CHARS - n, "%s", "--:--:--"); oled_set_row(3, tmp); return true; } #endif #if EMULATE_DEW static bool oled_dewcontrol_updatefn (byte arg) // oled_arg_t { if (!dew_enabled) return false; // skip this screen if (arg == oled_button_pressed && !oled_button_timer) { oled_button_timer = get_timeout(200); oled_set_row(3, "Ch0: Cycling values.."); } else if (arg == oled_button_timer_expired) { oled_button_timer = get_timeout(1000); const byte channel = 0; // Only channel-0 supported for this right now oled_dew_next_mode(channel); dew_oled_show_intensity(channel); if (dew_debug) SERIAL_PRINTF("dew_aggression[0]=%u\r\n", dew_aggression[0]); } else if (arg == oled_normal || arg == oled_firstcall) { oled_set_row(3, dew_oled_rows[3]); } for (byte row = 0; row < 3; ++row) oled_set_row(row, dew_oled_rows[row]); return true; } static void oled_dew_next_mode (byte channel) { byte aggr = dew_aggression [channel]; byte pwm = dew_pwm_percent[channel]; if (aggr && (!sht3x_detected || !thermistor_detected[channel])) { aggr = 0; pwm = 100; // this immediately rolls around to 0 below } if (aggr) { if (++aggr > 10) { aggr = 0; pwm = 0; } } else { pwm = pwm - (pwm % 20) + 20; if (pwm > 100) { pwm = 0; if (sht3x_detected && thermistor_detected[channel]) aggr = 1; } } nvram_save_delay = 2500; dew_set_aggression (channel, aggr); dew_set_pwm_percent(channel, pwm); nvram_save_delay = NVRAM_SAVE_DELAY; } #endif /* EMULATE_DEW */ static bool oled_connectstatus_updatefn (byte arg) // oled_arg_t { char tmp[OLED_ROW_CHARS + 1]; tmp[OLED_ROW_CHARS] = 0; const char *connection = get_connection_type(); if (musb_switch_timer) snprintf(tmp, OLED_ROW_CHARS, "%-5s Reset Pending..", hbg3_version); else if (connection[0] == 'N') snprintf(tmp, OLED_ROW_CHARS, "%-5s %s", hbg3_version, musb_selected ? "MUSB Selected" : HBG3_NAME " Idle"); else snprintf(tmp, OLED_ROW_CHARS, "%-5s %s %s", hbg3_version, connection, (connection[0] != 'B' || bt_active) ? (bt_handshake ? "Handshake" : "Connected") : "Session"); oled_set_row(0, tmp); if (connection[0] == 'B') { snprintf(tmp, OLED_ROW_CHARS, "Mode: %s", (bt_protocol == bt_protocol_aux) ? "AUX protocol" : "USB handset"); oled_set_row(1, tmp); snprintf(tmp, OLED_ROW_CHARS, "SSID: %s", hbg3_ssid); oled_set_row(2, tmp); oled_clear(3); } else if (connection[0] == 'U') { snprintf(tmp, OLED_ROW_CHARS, "Mode: USB %lu", (musb_uart->baudRate() / 10) * 10); // show 115201 as 115200. oled_set_row(1, tmp); snprintf(tmp, OLED_ROW_CHARS, "Wireless is %s", musb_rfkill ? "off" : "on"); oled_set_row(2, tmp); snprintf(tmp, OLED_ROW_CHARS, "%u %u", musb_rxcount, musb_txcount); oled_set_row(3, tmp); } else if (esp32_wifi_off) { oled_set_row(1, (musb_selected && musb_rfkill) ? "Wireless is off" : "WiFi is off"); #if ETHERNET_ENABLED if (ethernet_detected) { snprintf(tmp, OLED_ROW_CHARS, "Eth: DHCP %s", ethernet_client_mode ? "Client" : "Server"); oled_set_row(2, tmp); snprintf(tmp, OLED_ROW_CHARS, " IP: %s", Ethernet.localIP().toString().c_str()); oled_set_row(3, tmp); } else #endif /* ETHERNET_ENABLED */ oled_clear(2); } else { const char *mode = (wifi_client_mode && !wifi_client_mode_override) ? "AccessPoint" : "DirectConnect"; if (WIFIRELAY_ENABLED && wifi_relay_mode) mode = "Relay"; snprintf(tmp, OLED_ROW_CHARS, "WiFi: %s%s", mode, wifi_client_mode_override ? "*" : ""); oled_set_row(1, tmp); snprintf(tmp, OLED_ROW_CHARS, "SSID: %s", current_ssid); oled_set_row(2, tmp); snprintf(tmp, OLED_ROW_CHARS, " IP: %s", current_ipaddr); oled_set_row(3, tmp); } if (arg == oled_button_pressed) { oled_set_row(0, "Hold 5 secs to RESET"); if (!oled_button_timer) oled_button_timer = get_timeout(4500); } else if (arg == oled_button_timer_expired) { esp32_reset_pending = get_timeout(50); } return true; } #if OTA_ENABLED static bool oled_ota_updatefn (byte arg) // oled_arg_t { static bool fwupdate_possible = false; if (arg == oled_firstcall) { fwupdate_possible = false; oled_set_row(0, "OTA Firmware Update"); oled_set_row(1, "==================="); if (!ota_partition_size_okay) { oled_set_row(2, "Wrong Partition Size"); oled_clear(3); } else if (!wifi_client_mode) { // Unfortunately, the OTA library doesn't work over Ethernet. oled_set_row(2, "Wrong WiFi mode:"); oled_set_row(3, " Needs Access Point"); } else { oled_set_row(3, " to trigger update."); fwupdate_possible = true; } } else if (arg == oled_button_pressed && !oled_button_timer && fwupdate_possible) { oled_button_timer = get_timeout(2500); } else if (arg == oled_button_timer_expired) { ota_fetch_update(); } if (oled_button_timer) oled_set_row(2, "Hold for 3 seconds"); else if (fwupdate_possible) oled_set_row(2, "Push+Hold button"); return true; } #endif /* OTA_ENABLED */ static const byte QRcode_image[] = { 0xff,0xc0,0xc0,0xc0,0xff,0x00,0xff,0xc3,0xc3,0xc3,0x3c,0x00,0xfc,0x03,0x03,0x03,0x0c,0x00,0x03,0x03,0x33,0xcf,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xc0, 0x30,0x30,0xc0,0x00,0xf0,0x00,0x00,0x00,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0x07,0x07,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0x07,0x07,0xff,0xff,0xff,0xe7,0xe7,0xe7,0xe7,0xe7,0xff,0xff,0xff,0xff,0xff, 0xff,0xff,0x1f,0x1f,0xff,0xff,0xff,0xff,0xff,0xe7,0xe7,0xff,0xff,0xff,0x07,0x07,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0x07,0x07,0xff,0xff, 0x3f,0x00,0x00,0x00,0x3f,0x00,0x3f,0x30,0x30,0x30,0x0f,0x00,0x0f,0x30,0x30,0x33,0x0f,0x00,0x0c,0x30,0x30,0x30,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3f,0x30, 0x30,0x30,0x0f,0x00,0x00,0x33,0x33,0x33,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0x80,0x80,0x03,0x03,0x03,0x8f,0x8f,0x80,0x80,0x7c, 0x7c,0x7c,0xf0,0xf0,0xfc,0xfc,0x80,0x80,0x80,0x0c,0x0c,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xff,0xff,0xff,0x00,0x00,0xff,0xff, 0xff,0x0c,0x30,0x0c,0xff,0x00,0x00,0x30,0x30,0x30,0xc0,0x00,0xf0,0xc0,0x30,0x30,0xc0,0x00,0x00,0xff,0x00,0xc0,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x00, 0x00,0x00,0x00,0x00,0xc0,0x30,0x30,0x30,0xc0,0x00,0xf0,0xc0,0x30,0x30,0xc0,0x00,0xc0,0x30,0x30,0xc0,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0x38,0x38,0x39,0x39,0x39,0x39,0x39,0x39,0x39,0xf9,0xf9,0xf9,0xf9,0xf9,0x38,0x38,0xff,0xff,0xff,0x39,0x39,0xc6,0xc6,0xc6,0xf9,0xf9,0xc7,0xc7,0xf8, 0xf8,0xf8,0xff,0xff,0xf9,0xf9,0x3f,0x3f,0x3f,0x38,0x38,0x3f,0x3f,0x3f,0xf8,0xf8,0xf9,0xf9,0x39,0x39,0x39,0x39,0x39,0x39,0x39,0xf9,0xf9,0xf9,0x38,0x38,0xff,0xff, 0x3f,0x00,0x00,0x00,0x3f,0x00,0x0c,0x33,0x33,0x33,0x3f,0x00,0x3f,0x00,0x00,0x00,0x00,0x00,0x00,0x3f,0x03,0x0c,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3f,0x30, 0x30,0x30,0x30,0x00,0x0f,0x30,0x30,0x30,0x0f,0x00,0x3f,0x00,0x00,0x00,0x00,0x00,0x0f,0x30,0x30,0x30,0x3f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0x87,0x87,0x9f,0x9f,0x9f,0x1f,0x1f,0xe0,0xe0,0xf8,0xf8,0xf8,0x18,0x18,0x67,0x67,0xe0,0xe0,0xe0,0x1f,0x1f,0x78,0x78,0x78,0xff,0xff,0x1f,0x1f,0x9f, 0x9f,0x9f,0x80,0x80,0x9f,0x9f,0x9f,0x9f,0x9f,0xe0,0xe0,0x78,0x78,0x78,0x7f,0x7f,0x18,0x18,0x7f,0x7f,0x7f,0x9f,0x9f,0x1f,0x1f,0x78,0x78,0x78,0x7f,0x7f,0xff,0xff, 0x30,0x30,0x30,0x30,0x30,0x00,0x30,0x30,0x30,0x30,0x30,0x00,0x30,0x30,0x30,0x30,0x30,0x00,0x30,0x30,0x30,0x30,0x30,0x00,0x30,0x30,0x30,0x30,0x30,0x00,0x30,0x30, 0x30,0x30,0x30,0x00,0x30,0x30,0x30,0x30,0x30,0x00,0x30,0x30,0x30,0x30,0x30,0x00,0x30,0x30,0x30,0x30,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0x3f,0x3f,0xc3,0xc3,0xc3,0xc0,0xc0,0xc3,0xc3,0x33,0x33,0x33,0xf0,0xf0,0xcc,0xcc,0x0f,0x0f,0x0f,0x30,0x30,0x30,0x30,0x30,0x33,0x33,0xf0,0xf0,0x0f, 0x0f,0x0f,0x33,0x33,0xc3,0xc3,0x33,0x33,0x33,0xff,0xff,0xfc,0xfc,0xfc,0xf0,0xf0,0x30,0x30,0x00,0x00,0x00,0xff,0xff,0xcc,0xcc,0xcc,0xcc,0xcc,0x30,0x30,0xff,0xff, 0x03,0x03,0x03,0x03,0x03,0x00,0x03,0x03,0x03,0x03,0x03,0x00,0x03,0x03,0x03,0x03,0x03,0x00,0x03,0x03,0x03,0x03,0x03,0x00,0x03,0x03,0x03,0x03,0x03,0x00,0x03,0x03, 0x03,0x03,0x03,0x00,0x03,0x03,0x03,0x03,0x03,0x00,0x03,0x03,0x03,0x03,0x03,0x00,0x03,0x03,0x03,0x03,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0x3e,0x3e,0x3f,0x3f,0x3f,0x39,0x39,0x39,0x39,0x38,0x38,0x38,0x3f,0x3f,0x39,0x39,0xf8,0xf8,0xf8,0xc0,0xc0,0xfe,0xfe,0xfe,0xf8,0xf8,0x39,0x39,0x06, 0x06,0x06,0xc6,0xc6,0xff,0xff,0xfe,0xfe,0xfe,0x01,0x01,0xf9,0xf9,0xf9,0x39,0x39,0xf8,0xf8,0x00,0x00,0x00,0xf9,0xf9,0x01,0x01,0x39,0x39,0x39,0x00,0x00,0xff,0xff, 0x3c,0xc3,0xc3,0xc3,0x03,0x00,0xc0,0x30,0x30,0x30,0x00,0x00,0x00,0x30,0x30,0x30,0xc0,0x00,0xf0,0xc0,0x30,0x30,0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x00, 0x00,0x00,0xff,0x00,0xff,0xc3,0xc3,0xc3,0x3c,0x00,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x3c,0x3c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0x1f,0x1f,0x00,0x00,0x00,0xe0,0xe0,0x63,0x63,0x00, 0x00,0x00,0x03,0x03,0x9f,0x9f,0xfc,0xfc,0xfc,0x80,0x80,0x63,0x63,0x63,0x03,0x03,0xe3,0xe3,0x00,0x00,0x00,0x83,0x83,0x1f,0x1f,0x00,0x00,0x00,0x9f,0x9f,0xff,0xff, 0x30,0x30,0x30,0x30,0x0f,0x00,0x0f,0x30,0x30,0x30,0x0c,0x00,0x0c,0x33,0x33,0x33,0x3f,0x00,0x3f,0x00,0x00,0x00,0x3f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0x30, 0x30,0x30,0x0f,0x00,0x3f,0x00,0x03,0x0c,0x30,0x00,0x3f,0x30,0x30,0x30,0x30,0x00,0x00,0x0f,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xff,0xff,0xff,0xc0,0xc0,0xcf,0xcf,0xcf,0xce,0xce,0xce,0xce,0xce,0xce,0xce,0xcf,0xcf,0xc0,0xc0,0xff,0xff,0xff,0xc0,0xc0,0xc0,0xc0,0xc0,0xcf,0xcf,0xfe,0xfe,0xce, 0xce,0xce,0xfe,0xfe,0xcf,0xcf,0xff,0xff,0xff,0xff,0xff,0xf0,0xf0,0xf0,0xf0,0xf0,0xcf,0xcf,0xc0,0xc0,0xc0,0xcf,0xcf,0xc0,0xc0,0xce,0xce,0xce,0xcf,0xcf,0xff,0xff }; static void oled_cmd (uint8_t command) { Wire.setClock(800000ul); Wire.beginTransmission(OLED_I2C_ADDR); Wire.write(0x00); // Select "Command" mode Wire.write(command); // The actual command byte Wire.endTransmission(); } static void oled_send_image (const byte *image, unsigned int image_size) { oled_force_clear(); // so that next use after this works oled_cmd(0x20); // Set MemoryMode to next byte: oled_cmd(0x00); // use Horizontal addressing mode for image. oled_cmd(0x21); // Set Column Address Low/High: oled_cmd(0x00); // Low: 0 oled_cmd(0x7F); // High: 127 oled_cmd(0x22); // Set Page (row) Address Low/High: oled_cmd(0x00); // Low: 0 oled_cmd(0x07); // High: 7 const byte burst_size = 32; Wire.setClock(800000ul); for (byte burst = 0; burst < (image_size / burst_size); burst++) { Wire.beginTransmission(OLED_I2C_ADDR); Wire.write(0x40); // Select "Data" mode for (unsigned int i = 0; i < burst_size; ++i) Wire.write(*image++); Wire.endTransmission(); } oled_cmd(0x20); // Set MemoryMode to next byte: oled_cmd(0x02); // use Page addressing mode for text. // Prevent oled_loop() from overwriting the image: oled_data_updated[0] = false; oled_data_updated[1] = false; oled_data_updated[2] = false; oled_data_updated[3] = false; } static bool oled_QRcode_updatefn (byte arg) // oled_arg_t { static bool need_redraw = false, did_reset = false; if (arg == oled_firstcall) { need_redraw = true; did_reset = false; } else if (arg == oled_button_pressed) { if (!oled_button_timer) { oled_button_timer = get_timeout(9 * 1000 + 500); oled_set_row(0, HBG3_NAME " by Mark Lord"); oled_set_row(1, "===================="); oled_set_row(2, "Hold for 10 seconds"); oled_set_row(3, " for Factory Reset."); } need_redraw = true; // In case button is not held long enough to begin the reset return true; } else if (arg == oled_button_timer_expired && !did_reset) { did_reset = true; SERIAL_PRINTLN("Factory Reset"); oled_set_row(2, ""); oled_set_row(3, "Doing FACTORY RESET"); nvram_load_defaults(); nvram_save(); need_redraw = false; esp32_reset_pending = get_timeout(2000); } if (need_redraw) { need_redraw = false; if (oled_type == &Adafruit128x64) { oled_send_image(QRcode_image, sizeof(QRcode_image)); } else { oled_set_row(1, HBG3_NAME " by Mark Lord"); oled_set_row(2, "https://rtr.ca/hbg3/"); if (oled_splash_timer) { oled_write_row(1); oled_write_row(2); } } } return true; } static void oled_turn_off (void) { oled_offtime = 0; oled_turned_off = true; oled_clear(0); oled.ssd1306WriteCmd(SSD1306_DISPLAYOFF); } static void oled_turn_on (void) { oled_offtime = 0; oled_turned_off = false; oled.ssd1306WriteCmd(SSD1306_DISPLAYON); } static void oled_init_modes (void) { byte modes = 0; #if EMULATE_DEW oled_updatefn[modes++] = oled_dewcontrol_updatefn; #endif oled_updatefn[modes++] = oled_connectstatus_updatefn; #if ETHERNET_ENABLED oled_updatefn[modes++] = oled_ethernet_updatefn; #endif #if EMULATE_GPS oled_updatefn[modes++] = oled_gps_updatefn; oled_updatefn[modes++] = oled_location_updatefn; #endif #if EMULATE_FOCUS oled_updatefn[modes++] = focus_updatefn; #endif #if OTA_ENABLED oled_updatefn[modes++] = oled_ota_updatefn; #endif oled_updatefn[modes++] = oled_QRcode_updatefn; if (modes != OLED_NMODES) SERIAL_PRINTF("ERROR/BUG: %s: modes=%u/%u\r\n", modes, OLED_NMODES); } static void oled_setup (void) { oled_force_clear(); oled_init_modes(); oled_detected = i2c_detect_device(OLED_I2C_ADDR); if (oled_detected) { if (OLED_BUTTON_PIN != -1) pinMode(OLED_BUTTON_PIN, INPUT_PULLUP); oled.begin(oled_type, OLED_I2C_ADDR); oled.clear(); oled.setFont((oled_type->lcdHeight == 32) ? System5x7 : HomeBrew5x14_font); // Show splash screen briefly at start-up: oled_splash_timer = get_timeout(1300); oled_QRcode_updatefn(oled_firstcall); } } static void oled_loop (void) { static oled_arg_t arg = oled_firstcall; static bool modepin_nochange = false; static byte oled_row_num = 0; static long button_low, button_debounce = 0, next_update = 0; static int button = HIGH; if (!oled_detected) return; if (oled_splash_timer) { if (time_before(millis(), oled_splash_timer)) return; oled_splash_timer = 0; oled.clear(); } bool autoblank = oled_timeout_secs && !button_debounce; int this_modepin = (OLED_BUTTON_PIN != -1) ? digitalRead(OLED_BUTTON_PIN) : HIGH; if (button != this_modepin) { button = this_modepin; button_debounce = get_timeout(100); autoblank = false; } else if (button_debounce && time_after(millis(), button_debounce)) { if (button == LOW) { if (!button_low) { button_low = get_timeout(800); } else if (time_after(millis(), button_low)) { if (oled_button_timer && time_after(millis(), oled_button_timer)) arg = oled_button_timer_expired; else arg = oled_button_pressed; } } else { // (button == HIGH) { button_low = 0; button_debounce = 0; if (oled_turned_off) { oled_turn_on(); modepin_nochange = true; } else if (arg == oled_button_pressed || arg == oled_button_timer_expired) { arg = oled_normal; oled_button_timer = 0; modepin_nochange = true; } if (modepin_nochange) { modepin_nochange = false; } else { arg = oled_firstcall; oled_mode = (oled_mode + 1) % OLED_NMODES; next_update = 0; oled_row_num = 0; oled_button_timer = 0; } } } if (next_update && time_before(millis(), next_update)) return; if (!autoblank) { oled_offtime = 0; } else if (!oled_offtime) { oled_offtime = get_timeout(oled_timeout_secs * 1000); } else if (time_after(millis(), oled_offtime)) { oled_turn_off(); } if (!oled_turned_off) { // Update the in-RAM copy of the display: oled_data[]: while (!oled_updatefn[oled_mode](arg)) oled_mode = (oled_mode + 1) % OLED_NMODES; if (arg == oled_firstcall) arg = oled_normal; // Update the physical OLED display, but only one row at a time, so as not to unduly hog the CPU: byte first_row = oled_row_num; bool wrote; do { wrote = oled_write_row(oled_row_num); oled_row_num = (oled_row_num + 1) % 4; } while (!wrote && oled_row_num != first_row); } next_update = get_timeout(35); // Could be faster, perhaps? } static void oled_print_displays (void) { char tmp[OLED_ROW_CHARS + 1]; int orig_mode = oled_mode; oled_degreesym = '^'; SERIAL_PRINTLN("+-----------------------+"); oled_button_timer = 0; for (oled_mode = 0; oled_mode < OLED_NMODES; ++oled_mode) { bool (*updatefn)(byte) = oled_updatefn[oled_mode]; if (updatefn != oled_QRcode_updatefn && updatefn(oled_firstcall)) { for (byte r = 0; r < 4; ++r) { memcpy(tmp, oled_data[r], OLED_ROW_CHARS); tmp[OLED_ROW_CHARS - 1] = '\0'; // Final char on each row is only 2-columns wide, not visible. SERIAL_PRINTF("| %21s |\r\n", tmp); } SERIAL_PRINTLN("+-----------------------+"); } } oled_degreesym = OLED_DEGREES; oled_button_timer = 0; oled_mode = orig_mode; oled_updatefn[oled_mode](oled_firstcall); } #endif /* OLED_ENABLED */ // *** End OLED support **************************************************************************************************** static inline byte packet_reset (struct rxbuf_s *rxbuf, byte ret) { if (rxbuf->bus) { rxbuf->bus->txq->last_tx_len = 0; } else { if (0) { // CFM will need something like this, someday if (ret == pkt_fail) tx_enq(rxbuf, &auxbus, forward_yes, !REGULAR_PKT, rxbuf->data, rxbuf->len); } rxbuf->len = 0; } return ret; } static void auxtest_received_msg (struct bus_s *bus, byte *data) { if (bus != &auxbus) return; auxtest_timer = 0; if (!TVERBOSE(data)) print_packet(bus->rxbuf->name, "auxtest", data, data[1] + 3); if (auxtest_remaining > 0) { auxtest_remaining--; set_blue_led(LED_ON); blue_led_timer = get_timeout(500); } } static inline bool dev_is_emulated (byte dst) { return (dst == DEV_ESP32) #if EMULATE_GPS || (dst == DEV_GPS && gps_detected && !gps_disabled) #endif #if EMULATE_FOCUS || (dst == DEV_FOCUS && stepper_driver != STEPPER_DRIVER_NONE) #endif #if EMULATE_DEW || ((dst == DEV_DEW || dst == DEV_DEWBB) && dew_enabled) #endif || (EMULATING_SSAG && dst == DEV_SSAG) || (EMULATING_SSAA && dst == DEV_SSAA); } /* Test auxbus by sending a message and (elsewhere) seeing the response */ static void auxtest_send_msg (void) { byte msg[] = {0x3b, 0x03, DEV_ESP32, DEV_AZM, DEV_GET_VERSION, 0x00}; update_checksum(msg); tx_enq(NULL, &auxbus, wifi_relay_mode ? forward_yes : forward_auxtest, REGULAR_PKT, msg, sizeof(msg)); } static byte tx_enq_from_rxbuf (struct rxbuf_s *rxbuf, byte ret) { bool regular_pkt = (ret == pkt_complete); byte dst = regular_pkt ? rxbuf->data[3] : 0xff; bool emulated = dev_is_emulated(dst); if (emulated || rxbuf != auxbus.rxbuf) tx_enq(rxbuf, &auxbus, (rxbuf != auxrelay.rxbuf) ? forward_yes : forward_no, regular_pkt, rxbuf->data, rxbuf->len); if (!emulated && rxbuf != auxrelay.rxbuf) tx_enq(rxbuf, &auxrelay, (rxbuf != auxbus.rxbuf ) ? forward_yes : forward_no, regular_pkt, rxbuf->data, rxbuf->len); return packet_reset(rxbuf, ret); } static void handle_uart_set_baud (struct rxbuf_s *rxbuf) { // CFM likes to shift baud rates on the fly: if (rxbuf->data[4] == UART_SET_BAUD) { struct txq_s *txq = auxbus.txq; if (rxbuf->data[1] == 7 && rxbuf->data[3] == DEV_UART) { txq->pending_baud = ((ulong)(rxbuf->data[5]) << 24) | ((ulong)(rxbuf->data[6]) << 16) | ((ulong)(rxbuf->data[7]) << 8) | rxbuf->data[8]; SERIAL_PRINTF("auxbus BAUD pending %lu\r\n", txq->pending_baud); } else if (rxbuf->data[1] == 3 && rxbuf->data[2] == DEV_UART) { if (txq->pending_baud) { txq->uart->updateBaudRate(txq->pending_baud); txq->pending_baud = 0; SERIAL_PRINTF("auxbus BAUD now %lu\r\n", txq->uart->baudRate()); } } } } static inline void bus_autodetect_ssforward (struct bus_s *bus, byte src, byte dst) { if (auxrelay_detected) { static bus_s *controller_bus = NULL; if (dst == DEV_SSAA) controller_bus = bus; else if (src == DEV_SSAA) auxrelay_ssforward = (controller_bus && bus != controller_bus); } } static inline bool suppress_lowbattery_warnings (struct rxbuf_s *rxbuf) { // Regular bursts of battery warnings from the Evolution mount: if (suppress_lowbattery && rxbuf->data[1] == 0x04 && rxbuf->data[5] == 0) { // Send a response, to make it stop complaining for a period: byte msg[] = {0x3b, 0x03, rxbuf->data[3], DEV_AZM, MC_SEND_WARNING, 0x00}; update_checksum(msg); tx_enq(NULL, rxbuf->bus, forward_drop, REGULAR_PKT, msg, sizeof(msg)); rxbuf->len = 0; // eat it packet_reset(rxbuf, pkt_fail); return true; } return false; } static inline bool block_cfm_connections (struct rxbuf_s *rxbuf, byte src, byte dst) { #if !BLOCK_CFM_CONNECTIONS /* * CFM support is still a ways off. It switches to some completely different protocol * when sending firmware binaries to the hand-controllers. Dunno about other devices yet. * * One way to handle CFM would be to enter a STRICT send/receive mode when CFM is detected, * with no attempt at buffering (much) or protocol decoding. Should work, but still risky. */ #error "Allowing CFM to attempt firmware updates can completely brick the mount and accessories!!" #else if (!auxbus_passive_mode && (src == DEV_CFM || (src == DEV_SW && dst == DEV_UART))) { auxbus_raw_break(); SERIAL_PRINTLN("Caught Celestron Firmware Manager (CFM); killing the connection!"); (void)packet_reset(rxbuf, pkt_fail); while (true) delay(1000); // Freeze all further operation to prevent CFM from doing anything return true; // Never reached } #endif /* BLOCK_CFM_CONNECTIONS */ return false; // all clear: not CFM } /* Need to accurately decode/follow 0x3c Starsense AutoAlign packets to know when they end. * But the actual data transfers happens back in bus_rx() for this, not here. * This function merely handles the book-keeping. */ static byte decode_ssaa_0x3c (struct rxbuf_s *rxbuf, byte b) { byte ret = pkt_ssinprogress; rxbuf->csum += b; switch (++rxbuf->sscount) { //case 1: // 0x3c already handled in packet_decoder() // rxbuf->csum = 0; // break; case 2: case 3: case 4: case 5: if (b != 0x3b) { rxbuf->sslen32 = (rxbuf->sslen32 << 8) | b; } else { if (verbose || TRACING_DEV(DEV_SSAA)) { auxbus_raw_break(); SERIAL_PRINTF("Starsense: pkt[%u]=0x%02x FAIL\r\n", rxbuf->sscount, b); } ret = pkt_fail; break; } if (rxbuf->sscount != 5) break; if ((rxbuf->sslen32 % 8) || rxbuf->sslen32 > (100 * 8)) { // Max 100 stars, 8-bytes each if (verbose || TRACING_DEV(DEV_SSAA)) { auxbus_raw_break(); SERIAL_PRINTF("Starsense: sslen32=%lu/%lu FAIL\r\n", rxbuf->sslen32, (100 * 8)); } ret = pkt_fail; break; } rxbuf->sslen = rxbuf->sslen32 + 6; // everything, including 0x3c, sslen32, csum rxbuf->sspadding = 0; if (verbose || TRACING_DEV(DEV_SSAA)) { auxbus_raw_break(); SERIAL_PRINTF("Starsense: %u/%u (%lu stars)\r\n", rxbuf->sscount, rxbuf->sslen, rxbuf->sslen32 / 8); } break; default: if (rxbuf->sspadding) { rxbuf->sspadding--; rxbuf->csum -= b; // padding is excluded from csum calculation if (b != 0x02) { // Sanity check.. doesn't actually have to be 0x02 if (verbose || TRACING_DEV(DEV_SSAA)) { auxbus_raw_break(); SERIAL_PRINTF("Starsense: %u/%u bad padding 0x%02x FAIL\r\n", rxbuf->sscount, rxbuf->sslen, b); } ret = pkt_fail; } break; } if (b == 0x3b) { rxbuf->sspadding = 2; rxbuf->sslen += 2; if (verbose || TRACING_DEV(DEV_SSAA)) { auxbus_raw_break(); SERIAL_PRINTF("Starsense: %u/%u (+2 padding)\r\n", rxbuf->sscount, rxbuf->sslen); } break; } if (rxbuf->sscount == rxbuf->sslen) { if (verbose || TRACING_DEV(DEV_SSAA)) { auxbus_raw_break(); SERIAL_PRINTF("Starsense: %u/%u csum=0x%02x%s\r\n", rxbuf->sscount, rxbuf->sslen, rxbuf->csum, rxbuf->csum ? " FAIL" : ""); } ret = rxbuf->csum ? pkt_fail : pkt_sscomplete; } } if (ret != pkt_ssinprogress) rxbuf->sscount = rxbuf->sslen = 0; return packet_reset(rxbuf, ret); } static bool ssag_discard_mc_acks = false; #if SSSWI_HACKING static uint32_t reverse32 (uint32_t n) { uint32_t r = 0; if (0) { for (byte b = 0; b < 32; ++b) { if ((n & (1<>= 1; } } return r; } static bool have_ichallenge = false; static uint32_t ichallenge = 0; static uint32_t nchallenge = 0; static bool spoof_challenge_response (struct rxbuf_s *rxbuf) { byte *data = rxbuf->data; byte src = data[2], dst = data[3], op = data[4]; static uint32_t challenge; if (op == SSSWI_GET_CHALLENGE) { if (src == DEV_SSHC) { if (have_ichallenge) challenge = ichallenge; byte msg[] = {0x3b, 0x07, DEV_SSSWI, DEV_SSHC, SSSWI_GET_CHALLENGE, 0x00, 0x00, 0x00, 0x00, 0x00}; msg[5] = challenge; msg[6] = challenge >> 8; msg[7] = challenge >> 16; msg[8] = challenge >> 24; update_checksum(msg); if (verbose) print_packet("spoof", NULL, msg, sizeof(msg)); tx_enq(NULL, &auxrelay, forward_no, REGULAR_PKT, msg, sizeof(msg)); } return true; // Drop original packet } if (op == SSSWI_RESPONSE) { if (src == DEV_SSHC) { if (verbose) print_packet(rxbuf->name, NULL, rxbuf->data, rxbuf->len); uint32_t r = *(uint32_t *)(data + 5); if (have_ichallenge) { have_ichallenge = false; SERIAL_PRINTF("CR: 0x%08x 0x%08x 0x%08x 0x%08x\r\n", challenge, r, reverse32(challenge), reverse32(r)); } byte msg[] = {0x3b, 0x04, DEV_SSSWI, DEV_SSHC, SSSWI_RESPONSE, 0x01, 0x00}; // RESPONSE_OK update_checksum(msg); tx_enq(NULL, &auxrelay, forward_no, REGULAR_PKT, msg, sizeof(msg)); } return true; // Drop original packet } return false; // Continue processing this packet } #endif /* SSSWI_HACKING */ static byte packet_decoder (struct rxbuf_s *rxbuf, byte b) { restart: rxbuf->data[rxbuf->len++] = b; if (rxbuf->sscount) return decode_ssaa_0x3c(rxbuf, b); if (rxbuf->len == 1) { rxbuf->csum = 0; if (b == 0x3b) return pkt_inprogress; if (b == 0x3c && rxbuf->bus) { rxbuf->sscount = 1; rxbuf->sslen = 5; return pkt_inprogress; } return packet_reset(rxbuf, pkt_fail); } if ((b == 0x3b || b == 0x3c) && rxbuf->len <= 4) { // Okay, because no devices use 0x3b/0x3c as DEV_id // SSAG on relay observed to send 0x3b 0x06, then repeat them with the rest of the packet.. print_packet(rxbuf->name, "Collsion", rxbuf->data, rxbuf->len); packet_reset(rxbuf, pkt_fail); rxbuf->len = 0; goto restart; } rxbuf->csum += b; if (rxbuf->len == 2) { //if (b < 3 || b > (sizeof(rxbuf->data) - 3)) { if (b < 3 || b > 16) { // Mmm.. limits 0x3b packets to 19-bytes. Not currently an issue though. if (verbose) { auxbus_raw_break(); SERIAL_PRINTF("%s: bad len %02x\r\n", rxbuf->name, b); } return packet_reset(rxbuf, pkt_fail); } return pkt_inprogress; } if (rxbuf->len != (rxbuf->data[1] + 3)) return pkt_inprogress; if (rxbuf->csum != 0) { if (verbose) { const char *msg = BAD_CHECKSUM; if (b == 0 && rxbuf->data[1] == 3 && (rxbuf->data[4] == DEV_GET_VERSION || rxbuf->data[4] == DEV_GET_MODEL || (rxbuf->data[4] == DEW_GET_NUM_PORTS && rxbuf->data[3] == DEV_DEW)) ) msg = TIMED_OUT; print_packet(rxbuf->name, msg, rxbuf->data, rxbuf->len); // auxbus_rx } return packet_reset(rxbuf, pkt_fail); } // Handle a fully completed packet: struct bus_s *bus = rxbuf->bus; if (bus && rxbuf->len == bus->txq->last_tx_len) { if (0 == memcmp(rxbuf->data, bus->txq->last_tx, rxbuf->len)) { rxbuf->len = 0; // Suppress echo-back of most recently transmitted command: return packet_reset(rxbuf, pkt_fail); } } byte src = rxbuf->data[2]; byte dst = rxbuf->data[3]; if (!routing_check_dev(src, rxbuf)) { // Drop echo-backs for devices we know are NOT on this link // This is where routing gets used even if no auxrelay_detected: keeping local traffic off of WiFi/BT/MUSB/ETH rxbuf->len = 0; // eat it return packet_reset(rxbuf, pkt_fail); } rxbuf->requestor = src; // Used to select packets for forwarding over WiFi/BT/MUSB/ETH routing_add_dev(src, rxbuf); #if SSSWI_HACKING if (spoof_challenge_response(rxbuf)) { rxbuf->len = 0; return packet_reset(rxbuf, pkt_fail); } #endif /* SSSWI_HACKING */ byte op = rxbuf->data[4]; if (src == DEV_AZM && op == MC_SEND_WARNING && suppress_lowbattery_warnings(rxbuf)) return pkt_fail; if (TVERBOSE(rxbuf->data)) print_packet(rxbuf->name, NULL, rxbuf->data, rxbuf->len); if (ssag_discard_mc_acks) { // SSAG doesn't need to see ACKs back from the motor controllers, so discard those: if (dst == DEV_SSAG && (src == DEV_ALT || src == DEV_AZM) && rxbuf->data[1] == 0x03) { rxbuf->len = 0; // eat it return packet_reset(rxbuf, pkt_fail); } } // If a SSAA capture sequence has completed, we can revert back to normal timeouts again: if (src == DEV_SSAA && rxbuf->data[4] == SSAA_CAPTURE_GET_RESULT) p2000_timeout = P2000_NORMAL_TIMEOUT; // Safe to revert to normal timeouts now. bus_autodetect_ssforward(bus, src, dst); if (block_cfm_connections(rxbuf, src, dst)) return pkt_fail; if (op == UART_SET_BAUD) // CFM likes to shift baud rates on the fly: handle_uart_set_baud(rxbuf); if (src == DEV_FOCUS) fm_detected = true; #if NCHUCK_ENABLED else if (dst == DEV_AZM || dst == DEV_ALT) nchuck_handle_mc_request(rxbuf->data); #endif /* NCHUCK_ENABLED */ return tx_enq_from_rxbuf(rxbuf, pkt_complete); } static void bt_setup (void) { init_rxbuf(&bt_rxbuf, "bt_rx"); if (simple_mode || (musb_selected && musb_rfkill)) { bt_enabled = false; } else { bt_enabled = true; bt.begin(hbg3_ssid); } if (bt_debug) SERIAL_PRINTF("bt_enabled=%u\r\n", bt_enabled); } static inline void bt_restart (void) { if (bt_enabled) { bt_enabled = false; bt.end(); if (bt_debug) SERIAL_PRINTLN("bt_enabled=0"); } bt_setup(); } static inline void _bt_set_state (byte state, const char *name) { bt_auxstate = state; if (bt_debug) SERIAL_PRINTLN(name); } #define BT_SET_STATE(s) do { _bt_set_state(s, #s); } while (0) static inline bool bt_check_connection (void) { bool connected = bt_enabled ? bt.connected() : false; if (connected != bt_connected) { SERIAL_PRINTF("BT: %sCONNECTED mode %s\r\n", connected ? "" : "DIS", (bt_protocol == bt_protocol_aux) ? "AUX" : "HC"); bt_connected = connected; bt_active = 0; bt_handshake = 0; bt_ready = false; p2000_timeout = P2000_NORMAL_TIMEOUT; BT_SET_STATE(BT_IDLE); if (connected) bt_handshake = get_timeout(2000); else bt_restart(); } return connected; } static inline void bt_tx (bool from_ssaa, byte *data, uint16_t count) { if (bt_connected) { bt.write(data, count); //bt.flush(); // breaks bt operation if (TVERBOSE2(from_ssaa, data)) print_packet(__func__, NULL, data, count); } } /* * Bluetooth in v7.16 used to work most of the time with CPWI. * But ever since the code was updated to use the bt.connected() function, * it has been failing for (only) CPWI. Older code used bt.available() instead. * * MS-Windows though seems to behave better when we do use bt.connected(), * but CPWI now fails unless we emulate the hand-controller handshake with it. * * So we have to partially emulate the initial handshake with a Celestron hand-controller * to keep CPWI happy, without breaking anything else that uses AUX-protocol Bluetooth. * * The code for this is a bit of a mess. Ideally, it should eg. send a byte * and return to the main loop() thread for the delay periods. But something in * the ESP32 bluetooth stack requires an inline delay() instead, or the data is not * sent/received in a timely fashion. Dunno what/why. But it means the handshake code * has to use lengthy inline delay() calls to work with CPWI. Ugh. Triple UGH!!! */ #define BT_HS_TIMEOUT 220 static inline void bt_tx_byte (byte b) { bt_tx(false, &b, 1); if (bt_debug && !verbose) SERIAL_PRINTF("bt_tx %02x\r\n", b); } static void bt_aux_rx (void) { static int len; byte b = bt.read(); if (bt_handshake && bt_debug) SERIAL_PRINTF("bt_RX %02x\r\n", b); if (!bt_handshake || b == 0x3b) { bt_handshake = 0; bt_ready = true; (void)packet_decoder(&bt_rxbuf, b); } else if (bt_auxstate == BT_IDLE) { if (b == 0x8a) { BT_SET_STATE(BT_BOOTLOADER); bt_handshake = get_timeout(1250); // CPWI now ignores us for at least this long. } } else if ((bt_auxstate == BT_BOOTLOADER || bt_auxstate == BT_WAITBAUD) && b <= 16) { len = b - 1; // Normally, "b" is 0x0a here, for a 10-byte "set baud 19200" command. BT_SET_STATE(BT_SETBAUD); bt_handshake = get_timeout(BT_HS_TIMEOUT); // Normally the rest of the bytes arrive here without delay. } else if (bt_auxstate == BT_SETBAUD) { bt_handshake = get_timeout(BT_HS_TIMEOUT); if (--len == 0) BT_SET_STATE(BT_HSDONE); } } static inline void bt_aux_loop (void) { if (bt.available()) { do { bt_aux_rx(); } while (bt.available()); bt_active = get_timeout(p2000_timeout); return; } if (!bt_check_connection()) return; if (bt_active && time_after(millis(), bt_active)) bt_active = 0; if (bt_handshake && time_after(millis(), bt_handshake)) { bt_handshake = get_timeout(BT_HS_TIMEOUT); if (bt_auxstate == BT_BOOTLOADER) { byte failsafe = 0; do { bt_tx_byte(0xfa); delay(200); // MUST be an inline delay() or Bluetooth doesn't work. } while (!bt.available() && ++failsafe < 5); if (bt.available()) BT_SET_STATE(BT_WAITBAUD); } else if (bt_auxstate == BT_HSDONE) { byte failsafe = 0; do { bt_tx_byte(0x80); delay(200); // MUST be an inline delay() or Bluetooth doesn't work. } while (!bt.available() && ++failsafe < 5); } else if (bt_auxstate == BT_SETBAUD) { BT_SET_STATE(BT_IDLE); // timed-out } } } static void w2000_tx (bool from_ssaa, byte *data, uint16_t count) { if (!esp32_wifi_off && w2000.connected()) { w2000.write(data, count); //w2000.flush(); // breaks wifi operation with CPWI if (TVERBOSE2(from_ssaa, data)) print_packet("w2000_tx", NULL, data, count); } } static void emulate_send_reply (const char *devname, struct rxbuf_s *rxbuf, byte *data, uint16_t len) { bool from_ssaa = (data[0] == 0x3c); bool regular_pkt = (data[0] == 0x3b); if (regular_pkt) { len++; // make room for checksum byte data[1] = len - 3; // excludes 0x3b, itself, and checksum update_checksum(data); } if (regular_pkt && data[3] == DEV_ESP32 && TVERBOSE(data)) print_packet(devname, NULL, data, data[1] + 3); if (!rxbuf) { tx_enq(NULL, &auxbus, from_ssaa ? forward_discard : forward_yes, regular_pkt, data, len); } else { if (!rxbuf->bus && aux_only_tracing && TVERBOSE2(from_ssaa, data)) { // This ensures we always see emulated device transactions in full: aux_only_tracing = false; print_packet(rxbuf->name, NULL, data, len); aux_only_tracing = true; } if (rxbuf->bus) tx_enq(NULL, rxbuf->bus, from_ssaa ? forward_discard : forward_yes, regular_pkt, data, len); else if (rxbuf == &w2000_rxbuf) w2000_tx(from_ssaa, data, len); else if (rxbuf == &bt_rxbuf) bt_tx(from_ssaa, data, len); #if ETHERNET_ENABLED else if (ethernet_detected && rxbuf == &e2000_rxbuf) e2000_tx(from_ssaa, data, len); #endif /* ETHERNET_ENABLED */ else if (rxbuf == &musb_rxbuf) musb_tx(from_ssaa, data, len); else SERIAL_PRINTF("%s: unknown rxbuf\r\n", __func__); } } static void negate_24bit_position (byte *p) { /* * 24-bit signed integer, representing a fraction of a "full rotation". * 000000 to 7fffff --> 0 deg to 179.999978542327881 deg // (0x7fffff * 360 / 0x1000000) * ffffff to 800000 --> -0 deg to -179.999978542327881 deg */ uint32_t pos = get_24bit_value(p); pos = 0x1000000 - pos; // better than decoding it to degrees and back again p[2] = (byte)(pos & 0xff); pos >>= 8; p[1] = (byte)(pos & 0xff); pos >>= 8; p[0] = (byte)(pos & 0xff); } static void negate_16bit_position (byte *p) { ulong pos = p[0]; pos = (pos << 8) | p[1]; pos = 0x10000 - pos; p[1] = (byte)(pos & 0xff); p[0] = (byte)(pos >> 8); } static byte cordwrap_original_op = 0; static byte cordwrap_original_dev = 0; static long evo_wifi_change_pending = 0; /* Try and achieve compatibility with older mounts such as the Nexstar GPS NXGPS */ static uint16_t hack_response_pkt (byte *data, uint16_t len) { bool rewritten = false; byte src = data[2], dst = data[3], op = data[4]; uint16_t op_len = OP(op,data[1]); if (op == DEV_GET_MODEL && (src == DEV_AZM || src == DEV_ALT)) { if (data[1] == 3) { // older firmware on Nexstar-GPS returns no data. data[1] += 1; data[5] = 0x01; len += 1; rewritten = true; } if (data[1] == 4) { // 1-byte response from newer Nexstar-GPS firmware: {0x3b,0x04,0x10,0x20,0x05,0x01,0xc6}: // Latest CPWI is happy with it now, but older versions were not. // And SkyPortal/SkySafari still expect a 2-byte response. data[1] += 1; data[6] = 0x00; // append one extra byte to convert 1-byte model to 2-byte model. len += 1; rewritten = true; } if (data[1] == 5) // Save mount_model for other uses. Eg. Evo Wifi on/off. mount_model = (((uint16_t)data[5]) << 8) | data[6]; goto done; } if (src == DEV_EVWIFI && op_len == OP(SET_EVO_WIFI, 3)) { evo_wifi_change_pending = 0; goto done; } // StarSense-For-Skywatcher box replies from DEV_SSSWI instead of DEV_AZM: 3b 07 b8 0e fe 01 00 17 90 8d if (src == DEV_SSSWI && dst == DEV_SW && op_len == OP(DEV_GET_VERSION,3)) { data[2] = DEV_AZM; rewritten = true; goto done; } // Handle cordwrap_overrides for CPWI etc. if (src == DEV_AZM) { if (cordwrap_original_dev) { switch (op) { // Cannot use op_len, as response len varies by mount model!! Eg. CPC vs Evo. case MC_ENABLE_CORDWRAP: case MC_DISABLE_CORDWRAP: case MC_SET_CORDWRAP_POS: case MC_POLL_CORDWRAP: case MC_GET_CORDWRAP_POS: data[2] = src = cordwrap_original_dev; rewritten = true; } cordwrap_original_dev = 0; } if (cordwrap_original_op) { if (op == MC_DISABLE_CORDWRAP || op == MC_ENABLE_CORDWRAP) { data[4] = cordwrap_original_op; rewritten = true; } cordwrap_original_op = 0; } } if (src == DEV_ALT && mount_reversed_alt) { switch (op_len) { case OP(MC_GET_POS,6): negate_24bit_position(data + 5); rewritten = true; break; case OP(MC_SET_POS_GUIDERATE,3): data[4] = MC_SET_NEG_GUIDERATE; rewritten = true; break; case OP(MC_SET_NEG_GUIDERATE,3): data[4] = MC_SET_POS_GUIDERATE; rewritten = true; break; case OP(MC_SET_POS_BACKLASH,3): data[4] = MC_SET_NEG_BACKLASH; rewritten = true; break; case OP(MC_SET_NEG_BACKLASH,3): data[4] = MC_SET_POS_BACKLASH; rewritten = true; break; case OP(MC_MOVE_POS,3): data[4] = MC_MOVE_NEG; rewritten = true; break; case OP(MC_MOVE_NEG,3): data[4] = MC_MOVE_POS; rewritten = true; break; case OP(MC_GET_POS_BACKLASH,4): data[4] = MC_GET_NEG_BACKLASH; rewritten = true; break; case OP(MC_GET_NEG_BACKLASH,4): data[4] = MC_GET_POS_BACKLASH; rewritten = true; break; case OP(MC_GET_APPROACH,4): data[5] = !data[5]; rewritten = true; break; } } done: if (rewritten) { update_checksum(data); if (TVERBOSE(data)) print_packet("rewritten", NULL, data, len); } return len; } /* Hook to allow altering auxbus commands just before they are sent on the bus */ static inline uint16_t hack_command_pkt (byte *data, uint16_t len) { bool rewritten = false; byte dst = data[3], op = data[4]; uint16_t op_len = OP(op,data[1]); // CPWI bug: sends some cordwrap commands to wrong MC: if (dst == DEV_ALT) { switch (op_len) { case OP(MC_ENABLE_CORDWRAP,3): case OP(MC_DISABLE_CORDWRAP,3): case OP(MC_SET_CORDWRAP_POS,6): case OP(MC_POLL_CORDWRAP,3): case OP(MC_POLL_CORDWRAP,4): case OP(MC_GET_CORDWRAP_POS,3): cordwrap_original_dev = dst; data[3] = dst = DEV_AZM; rewritten = true; } } if (dst == DEV_AZM) { // CPWI bug: includes an extra (0x01) byte in MC_GOTO_DONE for focuser at least. if (op_len == OP(MC_GOTO_DONE,4)) { data[1] = 0x03; len -= 1; rewritten = true; } // CPWI bug: includes an extra (0x01) byte in MC_POLL_CORDWRAP if (op_len == OP(MC_POLL_CORDWRAP,4)) { data[1] = 0x03; len -= 1; rewritten = true; } // Handle cordwrap_override for CPWI etc. if (cordwrap_override) { // MC_SET_CORDWRAP_POS also enables CORDWRAP.. if (cordwrap_override == 1 && (op_len == OP(MC_ENABLE_CORDWRAP,3) || op_len == OP(MC_SET_CORDWRAP_POS,6))) { cordwrap_original_op = op; data[4] = MC_DISABLE_CORDWRAP; data[1] = 3; len = data[1] + 3; rewritten = true; } else if (cordwrap_override == 2 && op_len == OP(MC_DISABLE_CORDWRAP,3)) { cordwrap_original_op = MC_DISABLE_CORDWRAP; data[4] = MC_ENABLE_CORDWRAP; rewritten = true; } } } if (dst == DEV_ALT && mount_reversed_alt) { switch (op_len) { case OP(MC_GOTO_SLOW,5): case OP(MC_GOTO_FAST,5): negate_16bit_position(data + 5); rewritten = true; break; case OP(MC_GOTO_SLOW,6): case OP(MC_GOTO_FAST,6): negate_24bit_position(data + 5); rewritten = true; break; case OP(MC_SET_POS,6): negate_24bit_position(data + 5); rewritten = true; break; case OP(MC_SET_POS_GUIDERATE,5): case OP(MC_SET_POS_GUIDERATE,6): data[4] = MC_SET_NEG_GUIDERATE; rewritten = true; break; case OP(MC_SET_NEG_GUIDERATE,5): case OP(MC_SET_NEG_GUIDERATE,6): data[4] = MC_SET_POS_GUIDERATE; rewritten = true; break; case OP(MC_SET_POS_BACKLASH,4): data[4] = MC_SET_NEG_BACKLASH; rewritten = true; break; case OP(MC_SET_NEG_BACKLASH,4): data[4] = MC_SET_POS_BACKLASH; rewritten = true; break; case OP(MC_MOVE_NEG,4): data[4] = MC_MOVE_POS; rewritten = true; break; case OP(MC_MOVE_POS,4): data[4] = MC_MOVE_NEG; rewritten = true; break; case OP(MC_GET_POS_BACKLASH,3): data[4] = MC_GET_NEG_BACKLASH; rewritten = true; break; case OP(MC_GET_NEG_BACKLASH,3): data[4] = MC_GET_POS_BACKLASH; rewritten = true; break; case OP(MC_SET_APPROACH,4): data[5] = !data[5]; rewritten = true; break; } } done: if (rewritten) { update_checksum(data); if (TVERBOSE(data)) print_packet("rewritten", NULL, data, len); } return len; } static void forward_from_bus_to_outside (struct rxbuf_s *rxbuf, byte status, byte *data, uint16_t len) { byte src = DEV_FF, dst = DEV_FF; bool regular_pkt = (status == pkt_complete); bool from_ssaa = (status == pkt_ssinprogress || status == pkt_sscomplete); if (status == pkt_fail) return; // Don't forward failed packets if (!regular_pkt) { if (verbose || (from_ssaa && TRACING_DEV(DEV_SSAA))) print_packet(rxbuf->name, NULL, data, len); } else if (status == pkt_complete) { if (rxbuf == auxbus.rxbuf) { len = hack_response_pkt(data, len); if (len == 0) return; } src = data[2]; dst = data[3]; if (dst == DEV_ESP32) return; // Message goes no further // Handle cordwrap_override for local source, eg. hand-controller. if (cordwrap_override && dst == DEV_AZM) { uint16_t op_len = OP(rxbuf->data[4],rxbuf->data[1]); if (cordwrap_override == 1 && (op_len == OP(MC_ENABLE_CORDWRAP,3) || op_len == OP(MC_SET_CORDWRAP_POS,6))) { SERIAL_PRINTF("%s: forcing cordwrap off\r\n", __func__); byte msg[] = {0x3b, 0x03, DEV_DUMMY, DEV_AZM, MC_DISABLE_CORDWRAP, 0x00}; update_checksum(msg); tx_enq(NULL, &auxbus, forward_no, regular_pkt, msg, sizeof(msg)); } else if (cordwrap_override == 2 && op_len == OP(MC_DISABLE_CORDWRAP,3)) { SERIAL_PRINTF("%s: forcing cordwrap on\r\n", __func__); byte msg[] = {0x3b, 0x03, DEV_DUMMY, DEV_AZM, MC_ENABLE_CORDWRAP, 0x00}; update_checksum(msg); tx_enq(NULL, &auxbus, forward_no, regular_pkt, msg, sizeof(msg)); } } } // This is what enables the StarSense "controller" to be on a different bus than the StarSense camera. if (auxrelay_detected && (status == pkt_fail || status == pkt_inprogress || (from_ssaa && auxrelay_ssforward))) { if (verbose || (from_ssaa && auxrelay_ssforward && TRACING_DEV(DEV_SSAA))) SERIAL_PRINTLN("Relaying camera data."); struct bus_s *bus = (rxbuf == auxbus.rxbuf) ? &auxrelay : &auxbus; tx_enq(NULL, bus, forward_discard, regular_pkt, data, len); } // Echo packet to w2000 (wifi), unless it came from w2000: if (!regular_pkt || src != w2000_rxbuf.requestor) w2000_tx(from_ssaa, data, len); // Echo packet to bt, unless it came from bt: if (bt_ready && (!regular_pkt || src != bt_rxbuf.requestor)) bt_tx(from_ssaa, data, len); #if ETHERNET_ENABLED if (ethernet_detected) { // Echo packet to e2000 (ethernet), unless it came from e2000: if (!regular_pkt || src != e2000_rxbuf.requestor) e2000_tx(from_ssaa, data, len); } #endif /* ETHERNET_ENABLED */ if (musb_selected) { // Echo packet to Mount-USB, unless it came from there: if (!regular_pkt || src != musb_rxbuf.requestor) musb_tx(from_ssaa, data, len); } } static inline uint16_t discard_rx (HardwareSerial *uart, uint16_t discard_len) { while (discard_len && uart->available()) { --discard_len; uart->read(); } return discard_len; } static inline void txq_set_is_alive (struct txq_s *txq, bool is_alive, const char *who) { if (is_alive != txq->is_alive) { txq->is_alive = is_alive; SERIAL_PRINTF("%s: %s is %s\r\n", who, txq->name, is_alive ? "alive" : "dead"); } } // Buffering Starsense Camera packet data is surprisingly complex and difficult: static void bus_rx (struct bus_s *bus) { HardwareSerial *uart = bus->txq->uart; struct rxbuf_s *rxbuf = bus->rxbuf; while (uart->available()) { byte b = uart->read(); if (rxbuf->discard_len) { rxbuf->discard_len--; continue; } if (auxbus_raw_tracing) { if (auxbus_raw_count && (time_after(millis(), auxbus_raw_timeout) || (b == 0x3b || auxbus_raw_count >= 16))) auxbus_raw_break(); if (auxbus_raw_count++ == 0) SERIAL_PRINT("raw:"); auxbus_raw_timeout = get_timeout(200); SERIAL_PRINTF(" %02x", b); } byte status = packet_decoder(bus->rxbuf, b); if (status == pkt_inprogress) { if (rxbuf->len == 1 && bus->count) { // New 0x3b packet, so flush anything left over from before. //forward_from_bus_to_outside(rxbuf, pkt_fail, bus->data, bus->count); bus->count = 0; bus->delay = 0; } } else if (rxbuf->len) { memcpy(bus->data + bus->count, rxbuf->data, rxbuf->len); bus->count += rxbuf->len; rxbuf->len = 0; if (status == pkt_complete || status == pkt_sscomplete || bus->count >= (sizeof(bus->data) - AUXBUS_PKT_MAX)) { txq_set_is_alive(bus->txq, true, __func__); if (status != pkt_fail) forward_from_bus_to_outside(rxbuf, status, bus->data, bus->count); bus->count = 0; bus->delay = 0; return; // Give others a chance } else if (!bus->delay) { bus->delay = get_timeout(20); // Try and wait for whole packets or full buffers } } } if (bus->count && (!bus->delay || time_after(millis(), bus->delay))) { forward_from_bus_to_outside(rxbuf, rxbuf->sscount ? pkt_ssinprogress : pkt_inprogress, bus->data, bus->count); bus->count = 0; bus->delay = 0; } } static void esp32_handle_response (struct bus_s *bus, byte *data) { byte src = data[2]; if ((src == DEV_AZM || src == DEV_SSSWI) && data[4] == DEV_GET_VERSION) { auxtest_received_msg(bus, data); } else if (src == DEV_FOCUS) { fm_detected = true; // redundant: process_packet() already sets this #if NCHUCK_ENABLED nchuck_handle_fm_response(data); #endif } } static inline struct pkt_s *bus_get_next_txq_entry (struct bus_s *bus) { struct txq_s *txq = bus->txq; struct pkt_s *p = &txq->pkts[txq->tail]; if (p->len == 0) return NULL; /* Nothing to send; txq is empty. */ byte *pdata = p->data; if (p->indirect_data) pdata = *(byte **)pdata; if (auxbus_passive_mode) goto discard_txq_entry; if (p->regular_pkt && !p->indirect_data) { // Emulated device requests pass through txq to be handled here, guaranteeing execution in correct sequence byte dst = pdata[3]; if (dst == DEV_ESP32) { esp32_handle_response(bus, pdata); goto discard_txq_entry; } #if EMULATE_GPS if (dst == DEV_GPS && gps_detected && !gps_disabled) { gps_handle_request(p->from_rxbuf, pdata); goto discard_txq_entry; } #endif #if EMULATE_FOCUS if (stepper_driver != STEPPER_DRIVER_NONE && dst == DEV_FOCUS) { if (focus_handle_request(p->from_rxbuf, pdata)) return NULL; // not ready yet goto discard_txq_entry; } #endif #if EMULATE_DEW if (dew_enabled && (dst == DEV_DEW || dst == DEV_DEWBB)) { dew_handle_request(p->from_rxbuf, pdata); goto discard_txq_entry; } #endif #if EMULATE_SSAG if (EMULATING_SSAG && dst == DEV_SSAG) { ssag_handle_request(p->from_rxbuf, pdata); goto discard_txq_entry; } #endif if (dst == DEV_SSAA) { #if EMULATE_SSAA if (EMULATING_SSAA) { ssaa_handle_request(p->from_rxbuf, pdata); goto discard_txq_entry; } if (pdata[4] == SSAA_CAPTURE_BEGIN) p2000_timeout = P2000_SSAA_CAPTURE_TIMEOUT; // Need longer timeouts until the capture is completed. #endif } } if (!p->from_rxbuf || p->from_rxbuf != bus->rxbuf) // always true? return p; print_packet(__func__, "discard", pdata, p->len); // Never happens? discard_txq_entry: txq_free_pkt(txq, p); return NULL; /* Nothing to send at the moment */ } static inline void txq_assert_busyout (struct txq_s *txq) { if (txq->busyout_pin == txq->busyin_pin) pinMode(txq->busyout_pin, OUTPUT); // Assert BUSYOUT digitalWrite(txq->busyout_pin, LOW); // Assert BUSYOUT } static inline void txq_deassert_busyout (struct txq_s *txq) { if (txq->busyout_pin == txq->busyin_pin) pinMode(txq->busyout_pin, INPUT); // Tri-state BUSY else digitalWrite(txq->busyout_pin, HIGH); // Deassert BUSYOUT } static inline bool claim_bus_for_tx (struct bus_s *bus) { struct txq_s *txq = bus->txq; if (bus->delay && time_before(millis(), bus->delay)) { txq->busy_timestamp = 0; return false; /* In the middle of receiving something */ } if (digitalRead(txq->busyin_pin) == LOW) { if (!txq->busy_timestamp) { if (verbose) SERIAL_PRINTF("%s: Busy\r\n", txq->name); txq->busy_timestamp = get_timeout(0); } else if (time_after(millis(), txq->busy_timestamp + (15 * 1000))) { // Handle situation where mount is connected but not powered on, dragging BUSYIN low txq->busy_timestamp = 0; //txq_set_is_alive(txq, false, __func__); // good idea, but causes issues and not really needed } return false; /* bus is busy */ } // "Claim" the bus txq_assert_busyout(txq); if (txq->busy_timestamp) { ulong delta = time_delta(millis(), txq->busy_timestamp); if (delta > 1500) SERIAL_PRINTF("%s: %sBusy for %lu.%03lu seconds.\r\n", txq->name, verbose ? "Idle, was " : "", delta / 1000, delta % 1000); else if (verbose) SERIAL_PRINTF("%s: Idle\r\n", txq->name); } txq->busy_timestamp = 0; return true; // bus claimed for tx } static inline void bus_tx (struct txq_s *txq, byte *data, uint16_t len) { txq->uart->write(data, len); txq->uart->flush(true); // flush tx only txq_deassert_busyout(txq); } static inline void bus_forward (struct bus_s *bus, byte forward, byte *pdata, uint16_t len) { if (forward == forward_auxtest) { forward = forward_no; if (verbose || !nchuck_debug) print_packet(bus->txq->name, TVERBOSE(pdata) ? NULL : "auxtest", pdata, len); } else if (forward != forward_drop && TVERBOSE(pdata)) { print_packet(bus->txq->name, NULL, pdata, len); // auxbus_tx } struct rxbuf_s *rxbuf = bus->rxbuf; if (forward == forward_discard) { bus->txq->last_tx_len = 0; if (verbose) SERIAL_PRINTF("%s: discarding %u+%u bytes\r\n", rxbuf->name, rxbuf->discard_len, len); rxbuf->discard_len += len; } else { // Keep track of last transmission, so we can supress echo-back later, after a delay. // This is NECESSARY to keep CPWI happy with older mounts that have a DELAYED echo-back. bus->txq->last_tx_len = len; memcpy(bus->txq->last_tx, pdata, len); } rxbuf->discard_len = discard_rx(bus->txq->uart, rxbuf->discard_len); if (forward == forward_yes) forward_from_bus_to_outside(bus->rxbuf, (forward == forward_discard) ? pkt_sscomplete : pkt_complete, pdata, len); } static void bus_loop (struct bus_s *bus) { struct pkt_s *p; long min_busy_time; bus_rx(bus); // Handle incoming data from bus p = bus_get_next_txq_entry(bus); // Get a packet to send, handling internal destinations at the same time if (!p || !p->len) // Re-test p->len in case packet got deleted by bus_rx() return; // Nothing to send yield(); byte *pdata = p->data; if (p->indirect_data) pdata = *(byte **)pdata; struct txq_s *txq = bus->txq; if (!txq->is_alive && digitalRead(txq->busyin_pin) == HIGH) txq_set_is_alive(txq, true, __func__); if (txq->is_alive) { if (!claim_bus_for_tx(bus)) return; // bus in-use */ min_busy_time = micros() + 100; // Allow BUSY to assert for 100usec before tx if (bus == &auxbus && p->regular_pkt) p->len = hack_command_pkt(pdata, p->len); while (time_before(micros(), min_busy_time)); // Wait until BUSY has been asserted long enough bus_tx(txq, pdata, p->len); // All good; now send the packet } else { print_packet(__func__, "discard", pdata, p->len); // Never happens? } bus_forward(bus, p->forward, pdata, p->len); // Forward packet out to other destinations txq_free_pkt(txq, p); } static const char RESERVED[] = "RESERVED"; // No need to keep these in order; they now get sorted by name when displayed with "get all". static char *nvram_vars[] = { (char *)"softap.passkey", (char *)"softap.ssid", (char *)"wlan.dhcp.enabled", (char *)"wlan.passkey", (char *)"wlan.ssid", (char *)"wlan.static.dns", (char *)"wlan.static.gateway", (char *)"wlan.static.ip", (char *)"wlan.static.netmask", (char *)"focus.presetC", (char *)"focus.presetZ", (char *)"focus.only", (char *)"focus.backlash.pos", (char *)"focus.backlash.neg", (char *)"focus.microsteps", (char *)"nchuck.reverse.alt", (char *)"mount.reversed.alt", (char *)"emulate.ssaa", (char *)"emulate.ssag", (char *)"dew0.amps", (char *)"dew1.amps", (char *)"gps.disable", (char *)"cordwrap.override", (char *)"dew0.aggression", (char *)"dew1.aggression", (char *)"focus.limits", (char *)"suppress.lowbattery", (char *)"gps.location.lat", (char *)"gps.location.lng", (char *)"dew0.manual.pwm", (char *)"dew1.manual.pwm", (char *) RESERVED, // was gps.idle.timeout (char *)"oled.type", (char *)"ota.version.path", (char *)"auxbus.stopbits", (char *)"musb.rfenable", (char *)"gps.location.force", // was ota.timestamp until v8.5 (char *)"ota.update.server", (char *)"ota.update.path", (char *)"focus.factor", (char *)"ssag.discard.mc.acks", (char *)"dew.force.enabled", (char *)"oled.timeout.secs", NULL }; static const char nvram_vals_defaults[][NVRAM_VALS_MAX_LEN + 1] = { "", // softap.passkey "", // softap.ssid "1", // wlan.dhcp.enabled "", // wlan.passkey "", // wlan.ssid "8.8.8.8", // wlan.static.dns "0.0.0.0", // wlan.static.gateway "0.0.0.0", // wlan.static.ip "0.0.0.0", // wlan.static.netmask "", // focus.presetC "", // focus.presetZ "", // focus.only "", // focus.backlash.pos "", // focus.backlash.neg "", // focus.microsteps "", // nchuck.reverse_alt "", // mount.reversed.alt "", // emulate.ssaa "", // emulate.ssag "", // dew0.amps "", // dew1.amps "", // gps.disable "", // cordwrap.override "", // dew0.aggression "", // dew1.aggression "", // focus.limits "", // suppress.lowbattery "", // gps.location.lat "", // rgps.location.lng "", // dew0.manual.pwm "", // dew1.manual.pwm "", // RESERVED was gps.idle.timeout "", // oled.type "", // ota.version.path "", // auxbus.stopbits "", // musb.rfenable "", // gps.location.force // was ota.timestamp until v8.5 "", // ota.update.server "", // ota.update.path "", // focus.factor "", // ssag.discard.mc.acks "", // dew.force.enabled "", // oled.timeout.secs "" // NULL }; #include #define NVRAM_COUNT (sizeof(nvram_vars) / sizeof(nvram_vars[0])) #define NVRAM_COUNT2 (sizeof(nvram_vals_defaults) / sizeof(nvram_vals_defaults[0])) static_assert(NVRAM_COUNT == NVRAM_COUNT2, "nvram_vars[] not same size as nvram_vals_defaults[]"); static char nvram_vals [NVRAM_COUNT][NVRAM_VALS_MAX_LEN + 1]; static int nvram_find_var (const char *name) { unsigned int namelen = strlen(name); for (int i = 0; nvram_vars[i]; ++i) { const char *vname = nvram_vars[i]; if (vname && *vname) { byte len = strlen(vname); if (len <= namelen) { if (name[len] == '\0' || name[len] == ' ') { if (0 == memcmp(name, vname, len)) return i; } } } } return -1; } static const char *nvram_get_val (const char *name) { int x = nvram_find_var(name); return (x == -1) ? "" : nvram_vals[x]; } static int nvram_get_int (const char *name) { return strtol(nvram_get_val(name), NULL, 0); } static void nvram_get_nonblank_val (const char *name, char *dst) { const char *tmp = nvram_get_val(name); if (*tmp) strcpy(dst, tmp); } static void nvram_clear_reserved_slots (void) { // Ensure "reservedNN" slots are empty, keeping them clean for future (re-)uses. for (byte v = 0; v < (NVRAM_COUNT - 1); ++v) { const char *vname = nvram_vars[v]; if (!vname || !vname[0] || 0 == strncmp(vname, RESERVED, strlen(RESERVED))) { if (nvram_vals[v][0]) { SERIAL_PRINTF("nvram: clearing slot %u: %s\r\n", v, nvram_vals[v]); nvram_vals[v][0] = 0; nvram_delayed_save = get_timeout(nvram_save_delay); } } } } static void nvram_save_int (const char *name, int val) { int x = nvram_find_var(name); if (x != -1) { char *val_p = nvram_vals[x]; if (atoi(val_p) != val) { sprintf(val_p, "%d", val); nvram_delayed_save = get_timeout(nvram_save_delay); } } } static void nvram_save_float (const char *name, double fval) { int x = nvram_find_var(name); if (x != -1) { char *val = nvram_vals[x], tmp[NVRAM_VALS_MAX_LEN + 1]; sprintf(tmp, "%lf", fval); if (strcmp(val, tmp)) { strcpy(val, tmp); nvram_delayed_save = get_timeout(nvram_save_delay); } } } static double nvram_get_float (const char *name) { double fval = BAD_FLOAT_VAL; int x = nvram_find_var(name); if (x != -1) { const char *val = nvram_vals[x]; if (index(val, '.')) sscanf(val, "%lf", &fval); else nvram_erase_val(name); } return fval; } static void nvram_erase_val (const char *name) { int x = nvram_find_var(name); if (x != -1) { char *val_p = nvram_vals[x]; *val_p = '\0'; nvram_delayed_save = get_timeout(nvram_save_delay); } } #if NCHUCK_ENABLED static void nchuck_save_preset (struct button_s *b) { int x = nvram_find_var(b->name); if (x != -1) { long val = (b->approach == MC_MOVE_NEG) ? -b->position : b->position; sprintf(nvram_vals[x], "%ld", val); nvram_delayed_save = get_timeout(1000); } } #endif /* NCHUCK_ENABLED */ static bool nvram_initialized = false; static inline void default_ssid (char *dst) { snprintf(dst, NVRAM_VALS_MAX_LEN, "%s%02X%02X%02X", DEFAULT_SSID, wifi_mac_addr[3], wifi_mac_addr[4], wifi_mac_addr[5]); } static void nvram_load_defaults (void) { SERIAL_PRINTLN(__func__); memcpy(nvram_vals, nvram_vals_defaults, sizeof(nvram_vals)); // Set up a unique default SSID by appending last 6-digits of MAC: default_ssid(nvram_vals[nvram_find_var("softap.ssid")]); nvram_initialized = true; } #define NVRAM_PATH "/HBGen3.txt" #define VARS_SIGNATURE "HBGen3-v1.0" static struct nvram_signature_s { char sig[12]; uint16_t vsize; } nvram_signature; static bool nvram_get_bool (const char *name) { const char *val = nvram_get_val(name); if (val && *val) { if (0 == strcmp(val, "1") || 0 == strcasecmp(val, "true")) return true; if (0 == strcmp(val, "0") || 0 == strcasecmp(val, "false")) return false; *(char *)val = '\0'; // Fix bad value nvram_delayed_save = get_timeout(nvram_save_delay); } return (0 == strcmp(name, "wlan.dhcp.enabled")); // the only bool that defaults to "1" } static void nvram_restore (void) { if (nvram_initialized) return; memcpy(nvram_vals, nvram_vals_defaults, sizeof(nvram_vals)); // In case restored vals[] is smaller than new size of vals[] if (FRAM_USE_FOR_NVRAM && fram_detected) { fram_read(FRAM_NVRAM_OFFSET, &nvram_signature, sizeof(nvram_signature)); if (strcmp(nvram_signature.sig, VARS_SIGNATURE) || nvram_signature.vsize > sizeof(nvram_vals)) { SERIAL_PRINTF("read(FRAM): signature mismatch: Sig='%s' vsize=%lu\r\n", nvram_signature.sig, nvram_signature.vsize); } else { fram_read(FRAM_NVRAM_OFFSET + sizeof(nvram_signature), nvram_vals, nvram_signature.vsize); nvram_initialized = true; } } else { bool ret = SPIFFS.begin(false, "/spiffs", 2); // Max 2 open files at a time if (!ret) { SERIAL_PRINTLN("Formatting SPIFFS -- please wait on BLUE LED"); ret = SPIFFS.begin(true, "/spiffs", 2); // Max 2 open files at a time } if (!ret) { SERIAL_PRINTLN("SPIFFS.begin failed"); } else { #if OTA_ENABLED ota_partition_size_okay = (SPIFFS.totalBytes() < 300000); #endif /* OTA_ENABLED */ //SERIAL_PRINTF("SPIFFS size: %lu\r\n", SPIFFS.totalBytes()); // 113201 File vf = SPIFFS.open(NVRAM_PATH, FILE_READ); if (!vf) { SERIAL_PRINTLN("open(" NVRAM_PATH "[R]) failed"); } else { size_t ret = vf.read((uint8_t *)&nvram_signature, sizeof(nvram_signature)); if (ret != sizeof(nvram_signature)) SERIAL_PRINTF("read(" NVRAM_PATH ") failed: %d/%d\r\n", ret, sizeof(nvram_vals)); else if (strcmp(nvram_signature.sig, VARS_SIGNATURE) || nvram_signature.vsize > sizeof(nvram_vals)) SERIAL_PRINTF("read(" NVRAM_PATH "): signature mismatch: vsize=%u/%u\r\n", nvram_signature.vsize, sizeof(nvram_vals)); else if ((ret = vf.read((uint8_t *)nvram_vals, nvram_signature.vsize)) != nvram_signature.vsize) SERIAL_PRINTF("read(" NVRAM_PATH ") failed: %d/%d\r\n", ret, nvram_signature.vsize); else nvram_initialized = true; } vf.close(); } } /* FRAM_USE_FOR_NVRAM */ if (!nvram_initialized) { nvram_load_defaults(); nvram_save(); } nvram_clear_reserved_slots(); #if OLED_ENABLED oled_get_type(); { const char *t = nvram_get_val("oled.timeout.secs"); if (t && t[0]) { int tmp = nvram_get_int("oled.timeout.secs"); if (tmp >= 0) oled_timeout_secs = tmp; } } #endif const char *ssid = nvram_get_val("softap.ssid"); if (ssid && ssid[0]) strcpy(hbg3_ssid, ssid); else default_ssid(hbg3_ssid); musb_rfkill = !nvram_get_bool("musb.rfenable"); suppress_lowbattery = nvram_get_bool("suppress.lowbattery"); byte tmp = nvram_get_int("auxbus.stopbits"); if (tmp == 1 || tmp == 2) auxbus_stopbits = tmp; #if EMULATE_FOCUS focus_backlash_pos = nvram_get_int("focus.backlash.pos"); focus_backlash_neg = nvram_get_int("focus.backlash.neg"); focus_factor = nvram_get_float("focus.factor"); if (focus_factor == 0.0 || focus_factor == BAD_FLOAT_VAL) focus_factor = 1.0; ulong min, max; char *flimits = (char *)nvram_get_val("focus.limits"); if (2 != sscanf(flimits, "%lu %lu", &min, &max)) min = max = 0; focus_set_calibration(min, max); #endif /* EMULATE_FOCUS */ #if EMULATE_SSAA emulate_ssaa = nvram_get_bool("emulate.ssaa"); if (emulate_ssaa) SERIAL_PRINTF("emulate.ssaa=%u\r\n", emulate_ssaa); #endif #if EMULATE_SSAG emulate_ssag = nvram_get_bool("emulate.ssag"); if (emulate_ssag) SERIAL_PRINTF("emulate.ssag=%u\r\n", emulate_ssag); #endif #if NCHUCK_ENABLED nchuck_reverse_alt = nvram_get_bool("nchuck.reverse.alt"); #endif #if EMULATE_GPS gps_disabled = nvram_get_bool("gps.disable"); if (gps_disabled) SERIAL_PRINTLN("gps.disable=1"); gps_saved_lat = nvram_get_float("gps.location.lat"); gps_saved_lng = nvram_get_float("gps.location.lng"); gps_location_force = nvram_get_bool("gps.location.force"); if (gps_location_force) gps_detected = true; #endif const char *cw = nvram_get_val("cordwrap.override"); if (cw) { if (*cw == '0') cordwrap_override = 1; // cordwrap enabled else if (*cw == '1') cordwrap_override = 2; // cordwrap disabled else cordwrap_override = 0; // no cordwrap override } #if EMULATE_DEW dew_enabled |= nvram_get_bool("dew.force.enabled"); dew_max_amperage[0] = dew_nvram_restore_amperage(0); dew_max_amperage[1] = dew_nvram_restore_amperage(1); tmp = nvram_get_int("dew0.aggression"); if (tmp >= 0 && tmp <= 10) dew_aggression[0] = tmp; tmp = nvram_get_int("dew1.aggression"); if (tmp >= 0 && tmp <= 10) dew_aggression[1] = tmp; tmp = nvram_get_int("dew0.manual.pwm"); if (tmp >= 0 && tmp <= 100) dew_manual_pwm[0] = tmp; tmp = nvram_get_int("dew1.manual.pwm"); if (tmp >= 0 && tmp <= 100) dew_manual_pwm[1] = tmp; #endif #if OTA_ENABLED nvram_get_nonblank_val("ota.update.server", ota_update_server); nvram_get_nonblank_val("ota.update.path", ota_update_path); nvram_get_nonblank_val("ota.version.path", ota_version_path); #endif ssag_discard_mc_acks = nvram_get_bool("ssag.discard.mc.acks"); mount_reversed_alt = nvram_get_bool("mount.reversed.alt"); // SkyPortal app looks for these UDP broadcasts. "AMW007" seems to be the critical bit. // Insert our MAC address and ADAPTER_VERSION string into the advertisement broadcast string: sprintf(advertisement, "{\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\",\n\"version\":\"" ADAPTER_VERSION "%s\"\n}", wifi_mac_addr[0], wifi_mac_addr[1], wifi_mac_addr[2], wifi_mac_addr[3], wifi_mac_addr[4], wifi_mac_addr[5], hbg3_version); if (verbose) SERIAL_PRINTF("UDP advertisement=%s\r\n", advertisement); } static void nvram_save (void) { if (!nvram_initialized) return; nvram_delayed_save = 0; strcpy(nvram_signature.sig, VARS_SIGNATURE); nvram_signature.vsize = sizeof(nvram_vals); if (FRAM_USE_FOR_NVRAM && fram_detected) { SERIAL_PRINTF("Sig='%s' vsize=%lu\r\n", nvram_signature.sig, nvram_signature.vsize); fram_write(FRAM_NVRAM_OFFSET, &nvram_signature, sizeof(nvram_signature)); fram_write(FRAM_NVRAM_OFFSET + sizeof(nvram_signature), nvram_vals, sizeof(nvram_vals)); } else { File vf = SPIFFS.open(NVRAM_PATH, FILE_WRITE); if (!vf) { SERIAL_PRINTLN("open(" NVRAM_PATH "[W]) failed"); return; } vf.write((byte *)&nvram_signature, sizeof(nvram_signature)); vf.write((byte *)nvram_vals, sizeof(nvram_vals)); vf.close(); } } static bool IP_fromString(IPAddress ip, const char *s) { uint parts[4]; if (4 == sscanf(s, "%d.%d.%d.%d", &parts[0], &parts[1], &parts[2], &parts[3])) { if ((parts[0] | parts[1] | parts[2] | parts[3]) <= 255) { ip = IPAddress(parts[0], parts[1], parts[2], parts[3]); return true; } } return false; } static void wifi_begin_server_mode (void) { SERIAL_PRINTLN(__func__); const char *passkey = nvram_get_val("softap.passkey"); wifi_relay_mode = false; SERIAL_PRINTF("esp32_wifi ON, softAP, ssid=%s, passkey=%s\r\n", hbg3_ssid, passkey); WiFi.mode(WIFI_AP); WiFi.softAP(hbg3_ssid, passkey); delay(100); // This delay here is critical: wait for SYSTEM_EVENT_AP_START before setting IP config WiFi.softAPConfig(server_static_ip, server_static_ip, server_static_netmask); #if OLED_ENABLED set_wifi_info(hbg3_ssid, server_static_ip); #endif } static inline bool autodetect_wifi_relay_mode (const char *ssid) { #if WIFIRELAY_ENABLED /* Hack: auto-detect WiFi-Relay mode: */ static const char Default [] = DEFAULT_SSID; static const char Celestron[] = "Celestron-"; return (0 == strncmp(ssid, Default, strlen(Default)) || 0 == strncmp(ssid, Celestron, strlen(Celestron))); #else return false; #endif } static void non_wifi_loop (void); static bool wifi_begin_client_mode (void) { SERIAL_PRINTLN("wifi_begin_client_mode:"); const char *ssid = nvram_get_val("wlan.ssid"); const char *passkey = nvram_get_val("wlan.passkey"); bool use_dhcp = nvram_get_bool("wlan.dhcp.enabled"); if (!ssid[0]) { wifi_client_mode_override = true; SERIAL_PRINTLN("wifi_client_mode_override: wlan.ssid not set yet"); return false; /* cannot be a client unless we know which AP to connect to! */ } WiFi.mode(WIFI_STA); if (!use_dhcp) { const char *ip = nvram_get_val("wlan.static.ip"); const char *nmask = nvram_get_val("wlan.static.netmask"); const char *gw = nvram_get_val("wlan.static.gateway"); const char *dns = nvram_get_val("wlan.static.dns"); IPAddress IP, NMASK, GW, DNS; if (!*ip || !*nmask || !*gw || !*dns) SERIAL_PRINTLN("static IP config failed; using DHCP"); else if (IP_fromString(IP,ip) && IP_fromString(NMASK,nmask) && IP_fromString(GW,gw) && IP_fromString(DNS,dns)) WiFi.config(IP, DNS, GW, NMASK); else SERIAL_PRINTLN("static IP config failed2; using DHCP"); } #if OLED_ENABLED set_wifi_info(ssid, WiFi.localIP()); #endif wifi_relay_mode = autodetect_wifi_relay_mode(ssid); long timeout = get_timeout(5 * 60 * 1000); // 5-minute failsafe timeout WiFi.setMinSecurity((passkey && *passkey) ? WIFI_AUTH_WPA_PSK : WIFI_AUTH_OPEN); non_wifi_loop(); WiFi.begin(ssid, (passkey && *passkey) ? passkey : NULL); WiFi.setAutoReconnect(true); while (WiFi.status() != WL_CONNECTED) { non_wifi_loop(); // Keep other things working while we're stuck here waiting for WiFi if (time_after(millis(), timeout)) { SERIAL_PRINTLN("WiFi client mode aborted"); wifi_client_mode_override = true; return false; } } SERIAL_PRINTF("\r\nWiFi connected to %s, IP address: ", ssid); SERIAL_PRINTLN(WiFi.localIP().toString().c_str()); #if OLED_ENABLED set_wifi_info(ssid, WiFi.localIP()); #endif return true; } static bool is_wifi_client_mode_requested (void) { static byte old_want_client = 0xa5; byte want_client = false; want_client = (READ_WIFI_MODE_PIN() == LOW); if (want_client != old_want_client) { wifi_client_mode_override = false; old_want_client = want_client; SERIAL_PRINTF("%09lu wifi_mode_switch: %s_mode\r\n", millis(), want_client ? "client" : "server"); } if (wifi_client_mode_override) want_client = false; return want_client; } static void set_esp32_wifi_off (bool want_off) { if (esp32_wifi_off == want_off) return; /* all okay as-is */ esp32_wifi_off = want_off; #if NTP_ENABLED ntp_time_is_valid = 0; #endif if (want_off) { wifi_relay_mode = false; SERIAL_PRINTLN("esp32_wifi OFF"); if (wifi_client_mode) WiFi.disconnect(true); WiFi.mode(WIFI_OFF); } else { wifi_client_mode = is_wifi_client_mode_requested(); SERIAL_PRINTF("esp32_wifi ON, %s mode\r\n", wifi_client_mode ? "Client" : "Server"); if (wifi_client_mode) wifi_client_mode = wifi_begin_client_mode(); if (!wifi_client_mode) wifi_begin_server_mode(); W2000.begin(); // Start listening for application connections on wifi W3000.begin(); // Start listening for management commands } } static void set_esp32_wifi_onoff (void) { set_esp32_wifi_off(false); // default is WiFi "on" if (wifi_client_mode != is_wifi_client_mode_requested()) { set_esp32_wifi_off(true); // turn off WiFi set_esp32_wifi_off(false); // turn on WiFi using the requested mode } } static bool evo_wifi_want_off = false; static bool evo_wifi_is_off = false; static long evo_wifi_attempts = 0; static void send_evo_wifi_onoff (void) { evo_wifi_change_pending = 0; if (evo_wifi_attempts == 0) return; evo_wifi_attempts--; if (verbose) SERIAL_PRINTF("evo_wifi %s\r\n", evo_wifi_is_off ? "OFF" : "ON"); byte msg[] = {0x3b, 0x04, DEV_ESP32, DEV_EVWIFI, SET_EVO_WIFI, !evo_wifi_is_off, 0x00}; update_checksum(msg); tx_enq(NULL, &auxbus, forward_no, REGULAR_PKT, msg, sizeof(msg)); evo_wifi_change_pending = get_timeout(1000); } static void manage_evo_wifi (bool want_off) { if ((!auxbus.txq->is_alive) || (mount_model && mount_model != EVOLUTION_MODEL)) return; static long evo_wifi_delay_timer = 0; if (want_off != evo_wifi_want_off) { evo_wifi_want_off = want_off; evo_wifi_delay_timer = get_timeout(5000); } else if (evo_wifi_delay_timer && time_after(millis(), evo_wifi_delay_timer)) { evo_wifi_delay_timer = 0; evo_wifi_attempts = 3; if (!auxbus_passive_mode && evo_wifi_is_off != evo_wifi_want_off) { evo_wifi_is_off = evo_wifi_want_off; send_evo_wifi_onoff(); } } else if (evo_wifi_change_pending && time_after(millis(), evo_wifi_change_pending)) { send_evo_wifi_onoff(); } } static void manage_esp32_wifi (bool want_off) { static long esp32_wifi_off_timer = 0; if (!want_off) { esp32_wifi_off_timer = 0; set_esp32_wifi_onoff(); } else if (!esp32_wifi_off_timer) { esp32_wifi_off_timer = get_timeout(5000); } else if (time_after(millis(), esp32_wifi_off_timer)) { esp32_wifi_off_timer = 0; set_esp32_wifi_off(true); } } static void manage_wifi_onoff (void) { if (simple_mode) { manage_esp32_wifi(true); } else { // Turn WiFi off when using Bluetooth or Mount-USB; turn it back on if activity there lapses. bool others_active = (bt_connected || musb_selected); #if ETHERNET_ENABLED others_active |= ethernet_detected && e2000.connected(); #endif manage_esp32_wifi(others_active && musb_rfkill); others_active |= (!esp32_wifi_off && w2000.connected()); manage_evo_wifi(others_active); } } // **** Begin OTA Firmware Update ************************************************************************************** #if OTA_ENABLED #include #include #define otaclient w2000 // Re-use w2000 for this (saves memory) #if OLED_ENABLED static void oled_refresh_all (void) { oled_write_row(0); oled_write_row(1); oled_write_row(2); oled_write_row(3); } static void oled_show_progress (byte percent, size_t progress, size_t total) { char tmp[OLED_ROW_CHARS + 1]; if (total == 0) { oled_set_row(0, "OTA Firmware Update"); oled_set_row(1, "==================="); } sprintf(tmp, "Progress: %3u%%", percent); oled_set_row(2, tmp); snprintf(tmp, OLED_ROW_CHARS, "%7lu/%lu", progress, total); tmp[OLED_ROW_CHARS] = 0; oled_set_row(3, tmp); oled_refresh_all(); } #endif /* OLED_ENABLED */ static void ota_show_progress (size_t progress, size_t total) { static byte old_percent; byte percent; percent = total ? (progress * 100 / total) : ~(old_percent = ~0); if (percent != old_percent) { old_percent = percent; SERIAL_PRINTF("OTA update: %3u%%\r\n", percent); } #if OLED_ENABLED if (oled_detected) oled_show_progress(percent, progress, total); #endif monitor_musb_switch(); } // Update firmware from existing stream: this type is triggered by pressing buttons. static bool firmware_update_from_stream (uint32_t clength) { if (!Update.begin(clength)) { SERIAL_PRINTLN("Not enough space to begin OTA"); } else { SERIAL_PRINTF("Begin OTA update, length is %d\r\n", clength); Update.onProgress([](size_t progress, size_t total) { ota_show_progress(progress, total); }); ulong wrote = Update.writeStream(otaclient); SERIAL_ENDLINE(); if (wrote != clength) { // Update.writeStream() does the download/update SERIAL_PRINTF("Write failed! %lu/%lu %s\r\n", wrote, clength, Update.errorString()); // errorString() returns char* Update.abort(); return false; } SERIAL_PRINTF("Wrote %d bytes successfully\r\n", clength); if (Update.end()) { SERIAL_PRINTLN("OTA done"); if (Update.isFinished()) { SERIAL_PRINTLN("Update successfully completed"); return true; // Success } else { SERIAL_PRINTLN("Update not finished!"); } } else { SERIAL_PRINTF("Error Occurred. Error %s\r\n", Update.errorString()); // errorString() returns char* } } return false; } static void ota_exit (const char *msg) { #if OLED_ENABLED if (oled_detected) { oled_set_row(3, msg); oled_write_row(3); } #endif // A full reset is necessary regardless of the outcome, to re-enable AUX buses, WiFi, etc.. SERIAL_PRINTLN(msg); delay(5000); w3000.stop(); // Close w3000 connection, if any #if ETHERNET_ENABLED e3000.stop(); // Close e3000 connection, if any #endif delay(100); ESP.restart(); } static ulong ota_clength = 0; #define OTA_TIMEOUT_MSECS (12 * 1000) /* 12 second timeout for all OTA update remote fetches */ static void ota_wait_for_data (void) { long timeout = get_timeout(OTA_TIMEOUT_MSECS); while (!otaclient.available()) { if (time_after(millis(), timeout)) ota_exit("Host timed-out"); delay(1); } } static void ota_fetch_file (const char *server, const char *part1, const char *part2) { otaclient.stop(); const char *qmark = part2[0] ? "?" : ""; SERIAL_PRINTF("Connecting to http://%s%s%s%s%s\r\n", server, (part1[0] == '/') ? "" : "/", part1, qmark, part2); ota_clength = 0; if (!otaclient.connect(server, 80, OTA_TIMEOUT_MSECS)) ota_exit("Connection failed"); otaclient.printf( "GET %s%s%s HTTP/1.1\r\n" "Host: %s\r\n" "Cache-Control: no-cache\r\n" "Connection: close\r\n\r\n", part1, qmark, part2, server ); while (1) { ota_wait_for_data(); String line = otaclient.readStringUntil('\n'); line.trim(); SERIAL_PRINTF("server: %s\r\n", line.c_str()); if (!line.length()) // End of headers? return; // Yes, let caller read the actual data content if (line.startsWith("HTTP/1.1") && line.indexOf(" 200 ") < 0) ota_exit("Bad server status"); static const char Content_Length[] = "Content-Length:"; const char *s = line.c_str(); if (s && 0 == strncmp(s, Content_Length, strlen(Content_Length))) ota_clength = atoi(s + strlen(Content_Length)); } } static void bus_free (struct bus_s *bus) { bus->txq->uart->end(); free(bus->txq); free(bus->rxbuf); } static void ota_fetch_update (void) { char version[20] = {0,}; // Prevent AUX buses from interfering, and free up extra RAM in case it's needed: if (auxrelay_detected) bus_free(&auxrelay); bus_free(&auxbus); ota_show_progress(0, 0); // Initialize the display ota_fetch_file(ota_update_server, ota_version_path, ota_update_path); // Note: server responds with a (hex) bytecount line as well as the actual data.. while (1) { ota_wait_for_data(); String line = otaclient.readStringUntil('\n'); line.trim(); const char *data = line.c_str(); SERIAL_PRINTF("server: %s\r\n", data); if (data[0] == 'v') { strncpy(version, data, sizeof(version) - 1); break; } if (!data[0]) ota_exit("File not found."); } SERIAL_PRINTF("OTA: running=" VERSION " " VERSION_DATE ", available=%s\r\n", version); if (!ota_ignore_version && 0 == strcmp(version, VERSION " " VERSION_DATE)) ota_exit("Already Up-to-date"); #if OLED_ENABLED char tmp[OLED_ROW_CHARS + 1]; char *space = index(version, ' '); if (space) *space = '\0'; // Strip date from version for display purposes sprintf(tmp, "Updating to %s", version); oled_set_row(0, tmp); #endif /* OLED_ENABLED */ // Now do the actual firmware download/update: ota_fetch_file(ota_update_server, ota_update_path, ""); const char *msg; if (ota_clength <= 0) msg = "No data from server"; else if (!firmware_update_from_stream(ota_clength)) msg = "OTA Update failed"; else msg = "OTA Update success"; ota_exit(msg); } #endif /* OTA_ENABLED */ // **** End OTA Firmware Update ************************************************************************************** static const char help_serial_commands[] = "; -- Comment, ignore/print this line\r\n" "# -- Comment, ignore/print this line\r\n" "a -- Toggle AUX-bus only mode for traces\r\n" "b -- Toggle Bluetooth handshake debug on/off\r\n" "baud -- Change baud rate on USB/Serial port\r\n" "cls -- Clear screen\r\n" #if SSSWI_HACKING "ich xxxxxxxx -- Specify 32-bit challenge for next SSSWI exchange\r\n" #endif "devs -- List devnames usable with trace command\r\n" #if EMULATE_DEW "d -- Toggle Dew-Controller debug on/off\r\n" #endif "D -- Toggle discard of MC ACKs to SSAG on/off\r\n" #if EMULATE_FOCUS "f -- Toggle Focus-Motor debug on/off\r\n" #endif #if EMULATE_GPS "fakegps -- Enable GPS emulation with lat/lng: xx.xxxxxx -xx.xxxxxx\r\n" #endif "free -- Show amount of free RAM (heap_size)\r\n" #if EMULATE_GPS "g -- Toggle GPS debug on/off\r\n" "G -- Toggle display of GPS NMEA sentences on/off\r\n" #endif #if OLED_ENABLED "oled -- Dump contents of the various OLED screens\r\n" #endif "m -- Toggle MUSB debug on/off\r\n" "M -- Toggle MUSB timestamps on/off\r\n" "P -- Toggle passive_mode on/off\r\n" #if NCHUCK_ENABLED "n -- Toggle Nunchuck debug on/off\r\n" #endif "r -- Toggle raw tracing of all AUX bus reception\r\n" "send -- Send AUX command to a device. Eg. send ALT 0xfe\r\n" #if EMULATE_SSAA "S -- Request huge image data packet from SSAA (for testing)\r\n" #endif "trace dev1 dev2 -- Enable/disable tracing of one or two specific AUX devs\r\n" "t -- Send AUX bus test message\r\n" "test_auxbus -- low-level test for AUX bus functionality\r\n" "v -- Toggle verbose on/off\r\n" "$$$ -- Enter ASCII pass-through mode to a Celestron WiFi adapter's config interface\r\n" "ssaa_recentre N -- Reset SSAA Camera centre point to 640,480 for Profile slot N: 0-2\r\n" ; static int get_hex_digit (char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } static int get_hex_byte_val (char **cmd_p) { char *cmd = *cmd_p; int x1, x2; while (*cmd == ' ') ++cmd; if (cmd[0] == '0' && cmd[1] == 'x') cmd += 2; x1 = get_hex_digit(*cmd); if (x1 == -1) goto Huh; x2 = get_hex_digit(*++cmd); if (x2 == -1) goto Huh; if (*++cmd && *cmd != ' ') goto Huh; while (*cmd == ' ') ++cmd; *cmd_p = cmd; return (x1 << 4) | x2; Huh: SERIAL_PRINTF("Huh? \"%s\"\r\n", cmd); return -1; } static bool match_word (const char *word, char **cmd_p, bool noargs) { char *cmd = *cmd_p; while (*cmd == ' ') ++cmd; if (0 == strncasecmp(word, cmd, strlen(word))) { cmd += strlen(word); while (*cmd == ' ') ++cmd; *cmd_p = cmd; if (!*cmd || !noargs) return true; } return false; } static int get_dev_name (char **cmd_p) { char *cmd = *cmd_p; while (*cmd == ' ') ++cmd; if (!*cmd) return -1; for (byte i = 0; i < N_DEVNAMES; ++i) { const char *devname = devnames[i].name; if (match_word(devname, &cmd, false)) { *cmd_p = cmd; return devnames[i].dev; } } return get_hex_byte_val(cmd_p); } // Can trace up to two devices at once static int get_trace_devs (char **cmd_p, byte *dev2_p) { char *orig_cmd = *cmd_p; int dev1 = get_dev_name(cmd_p); if (dev1 != -1) { char *cmd = *cmd_p; while (*cmd == ' ') ++cmd; if (!*cmd) return dev1; *cmd_p = cmd; int dev2 = get_dev_name(cmd_p); cmd = *cmd_p; while (*cmd == ' ') ++cmd; if (dev2 != -1 && !*cmd) { *dev2_p = dev2; } else { *cmd_p = orig_cmd; SERIAL_PRINTF("Huh? \"%s\"\r\n", *cmd_p); dev1 = -1; } } return dev1; } static void handle_send_cmd (char *cmd) { byte msg[AUXBUS_PKT_MAX], len = 0; int val; msg[len++] = 0x3b; msg[len++] = 0; // len, to be filled in after msg[len++] = DEV_ESP32; val = get_dev_name(&cmd); if (val == -1) return; msg[len++] = val; while (*cmd == ' ') ++cmd; // Look for a possible opcode name as first parameter: if (cmd[0] && (cmd[0] < '0' || cmd[0] > '9') && cmd[1] && cmd[2] && cmd[2] != ' ') { char savedc, *t = cmd; while (*t && *t != ' ') ++t; // Find end-of-word savedc = *t; // Temporarily save char after end-of-word *t = '\0'; // Zero-terminate the word byte op = find_op_by_name(cmd); *t = savedc; // Restore original char if (op != 0xff) { // Was opcode name found? msg[len++] = op; // Stuff opcode into msg cmd = t; // Skip ahead to next word while (*cmd == ' ') ++cmd; // Skip to next word, needed by loop below } } // Now collect the remaining parameters as additional message data: while (*cmd && len < (AUXBUS_PKT_MAX - 1)) { val = get_hex_byte_val(&cmd); if (val == -1) return; msg[len++] = val; } if (len < 5) SERIAL_PRINTLN("Huh? missing opcode"); else auxbus_send_msg(msg, ++len); } // These are used to restore settings to what they were before a p3000 connection changed them: static int p3000_orig_verbose = -1; static int p3000_orig_trace_dev1 = -1; static int p3000_orig_trace_dev2 = -1; static bool handle_serial_commands (char *cmd) { static const char ssaa_recentre[] = "ssaa_recentre "; byte b = *cmd; if (b == ';' || b == '#') { auxbus_raw_timeout = auxbus_raw_count = 0; } else if (match_word("cls", &cmd, true)) { SERIAL_PRINT("\033c"); } else if (match_word("send", &cmd, false)) { handle_send_cmd(cmd); #if OLED_ENABLED } else if (match_word("oled", &cmd, false)) { oled_print_displays(); #endif } else if (match_word("free", &cmd, true)) { SERIAL_PRINTF("free_heap=%lu, min_free=%lu, largest_free=%lu\r\n", esp_get_free_heap_size(), heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT), heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT)); } else if (match_word("devs", &cmd, true)) { SERIAL_PRINTF("%4s %-6s %s\r\n", "Addr", "Name ", "Description"); SERIAL_PRINTF("%4s %-6s %s\r\n", "====", "======", "===================="); for (byte i = 0; i < N_DEVNAMES; ++i) SERIAL_PRINTF("0x%02x %-6s %s\r\n", devnames[i].dev, devnames[i].name, devnames[i].desc); } else if (match_word("test_auxbus", &cmd, true)) { auxtest_remaining = 0; // cancel the automatic tests bus_test(&auxbus); if (auxrelay.txq) bus_test(&auxrelay); } else if (match_word("baud", &cmd, false)) { char *endp = NULL; ulong baud = strtoul(cmd, &endp, 10); if (!*cmd || *endp) { SERIAL_PRINTF("Huh? \"%s\"\r\n", cmd); } else { SERIAL_PRINTF("Setting new baud: %lu\r\n", baud); SerialDebug->updateBaudRate(baud); } } else if (match_word("trace", &cmd, false)) { int dev1 = -1; byte dev2 = 0xff; if (*cmd) dev1 = get_trace_devs(&cmd, &dev2); if ((unsigned)dev1 >= DEV_FF) { trace_dev1 = DEV_FF; trace_dev2 = DEV_FF; } else { if (p3000_debug & p3000_orig_trace_dev1 == -1) { p3000_orig_trace_dev1 = trace_dev1; p3000_orig_trace_dev2 = trace_dev2; } trace_dev1 = dev1; trace_dev2 = dev2; if (trace_dev2 == trace_dev1) trace_dev2 = DEV_FF; } if (trace_dev1 == DEV_FF) { SERIAL_PRINTLN("Tracing off"); } else { SERIAL_PRINTF("Tracing %s (0x%02x: %s)", dev_to_name(trace_dev1, false), trace_dev1, dev_to_name(trace_dev1, true)); if (trace_dev2 != DEV_FF) SERIAL_PRINTF(", and %s (0x%02x: %s)", dev_to_name(trace_dev2, false), trace_dev2, dev_to_name(trace_dev2, true)); SERIAL_PRINTLN(); } #if SSSWI_HACKING } else if (match_word("ich", &cmd, false)) { char *endp = NULL; ulong c = strtoul(cmd, &endp, 16); if (!*cmd || *endp) { SERIAL_PRINTF("%s: expected 32-bit challenge\r\n"); } else { ichallenge = c; have_ichallenge = true; } #endif /* SSSWI_HACKING */ } else if (match_word("ssaa_recentre", &cmd, false)) { char *endp = NULL; ulong profile = strtoul(cmd, &endp, 10); if (profile > 2 || (!*cmd || *endp)) { SERIAL_PRINTF("%s: expected profile number 0-2\r\n", ssaa_recentre); } else { SERIAL_PRINTF("%s: Setting centre point for profile %u to 640,480\r\n", ssaa_recentre, profile); AUXBUS_SEND_MSG(DEV_ESP32, DEV_SSAA, SSAA_SET_PROFILE, (byte)640,(640>>8), 0x00,0x00,(byte)480, (480>>8), 0x00,0x00, (byte)profile); } } else if (cmd[1] != 0) { return false; } else if (b == 'a') { aux_only_tracing = !aux_only_tracing; SERIAL_PRINTF("aux_only_tracing=%u\r\n", aux_only_tracing); #if EMULATE_DEW } else if (b == 'd') { dew_debug = !dew_debug; SERIAL_PRINTF("dew_debug=%u\r\n", dew_debug); #endif } else if (b == 'D') { ssag_discard_mc_acks = !ssag_discard_mc_acks; SERIAL_PRINTF("ssag_discard_mc_acks=%u\r\n", ssag_discard_mc_acks); #if EMULATE_FOCUS } else if (b == 'f') { focus_debug = !focus_debug; SERIAL_PRINTF("focus_debug=%u\r\n", focus_debug); #endif #if EMULATE_GPS } else if (b == 'g') { gps_debug = !gps_debug; SERIAL_PRINTF("gps_debug=%s\r\n", gps_debug ? "on" : "off"); } else if (b == 'G') { GPS_debug = !GPS_debug; SERIAL_PRINTF("GPS_debug(NMEA)=%s\r\n", GPS_debug ? "on" : "off"); #endif /* EMULATE_GPS */ } else if (b == 'm') { musb_debug = !musb_debug; SERIAL_PRINTF("musb_debug=%u\r\n", musb_debug); } else if (b == 'M') { musb_timestamps = !musb_timestamps; musb_debug = musb_timestamps; SERIAL_PRINTF("musb_timestamps=%u\r\n", musb_debug); #if NCHUCK_ENABLED } else if (b == 'n') { nchuck_debug = !nchuck_debug; SERIAL_PRINTF("nchuck_debug=%u\r\n", nchuck_debug); #endif } else if (b == 'P') { auxbus_passive_mode = !auxbus_passive_mode; auxtest_remaining = 0; SERIAL_PRINTF("auxbus_passive_mode=%u\r\n", auxbus_passive_mode); } else if (b == 'r') { auxtest_remaining = 0; // cancel the automatic tests auxbus_raw_tracing = !auxbus_raw_tracing; auxbus_raw_break(); SERIAL_PRINTF("raw_tracing=%u\r\n", auxbus_raw_tracing); #if EMULATE_SSAA // For testing } else if (b == 'S') { // Request LARGE image data packet from StarSense Simulator, for testing logic. SERIAL_PRINTLN("Requesting image plate from SSAA"); AUXBUS_SEND_MSG(DEV_DUMMY, DEV_SSAA, SSAA_CAPTURE_BEGIN, 0x03, 0xe8, 0x00); AUXBUS_SEND_MSG(DEV_DUMMY, DEV_SSAA, SSAA_CAPTURE_GET_PLATE); #endif } else if (b == 't') { SERIAL_PRINTLN("Sending auxtest"); auxtest_remaining = 0; // cancel the automatic tests auxtest_send_msg(); } else if (b == 'b') { bt_debug = !bt_debug; SERIAL_PRINTF("bt_debug=%u\r\n", bt_debug); } else if (b == 'v') { if (p3000_debug & p3000_orig_verbose == -1) p3000_orig_verbose = verbose; verbose = !verbose; SERIAL_PRINTF("verbose=%u\r\n", verbose); } else { return false; } return true; } /* * AUX devices normally ignore anything without a 0x3b/0x3c header. * Because TX/RX are tied together, we get echo-back of everything. * * Sending "$$$\r\n" connects ASCII stuff to WiFi port 3000. * Any ASCII sent from then on goes into that port, * and it responds to commands in ASCII again. * * The first response to "$$$" is "Command Mode Start". * The session then continues until we send "exit", * to which it responds "Command Mode Stop" and closes the port. * * Note that regular 0x3b/0x3c packets could be interspered with all of this, * because everything else on the bus ignores non 0x3b/0x3c packets. */ static void send_ascii_passthru (char *cmd) { // This didn't work with tx_enq() when tried, probably due to echo-back suppression. // So doing it the hard way here: struct txq_s *txq = auxbus.txq; long timeout = get_timeout(1000); // Starsense packets can take a while to clear the bus while (digitalRead(txq->busyin_pin) == LOW) { delay(1); if (time_after(millis(), timeout)) break; } txq_assert_busyout(txq); txq->uart->println(cmd); txq->uart->flush(true); // flush tx only txq_deassert_busyout(txq); } static void serial_loop (void) { static long ascii_passthru_timeout = 0; static char cmd[64]; static byte cx = 0; if (ascii_passthru_timeout && time_after(millis(), ascii_passthru_timeout)) { ascii_passthru_timeout = 0; aux_ascii_passthru = false; SERIAL_PRINTLN("ASCII PASSTHRU OFF"); } while (SERIAL_AVAILABLE()) { char c = SERIAL_READ(); if (c == 0x7f || c == '\b') { // DEL or backspace if (cx != 0) { cx--; SERIAL_PRINT("\b \b"); } continue; } if (c != '\n') SERIAL_PRINT(c); // echo-back if (cx == 0 && c == ' ' || c == '\t') continue; if (c != '\r' && c != '\n') { if (cx < (sizeof(cmd) - 2)) cmd[cx++] = c; continue; } if (cx) { cmd[cx] = '\0'; cx = 0; if (c != '\n') { SERIAL_PRINT('\n'); SERIAL_FLUSH(true); } if (aux_ascii_passthru) { send_ascii_passthru(cmd); char *cmdp = cmd; if (match_word("exit", &cmdp, true)) ascii_passthru_timeout = get_timeout(200); // delay off, to allow for "exit" responses to arrive } else if (0 == strcmp(cmd, "$$$")) { ascii_passthru_timeout = 0; aux_ascii_passthru = true; SERIAL_PRINTLN("ASCII PASSTHRU ON"); send_ascii_passthru(cmd); } else { if (handle_p3000_commands(cmd, SERIAL_WRITE) || handle_serial_commands(cmd)) { SERIAL_FLUSH(true); return; } SERIAL_PRINTLN("Huh?"); } } } } static void w3000_write (const char *s) { if (verbose && SerialDebug) SerialDebug->print(s); w3000.write(s); } static void w3000_flush (void) { w3000.flush(); } static void set_p3000_debug (bool on_off, bool debug_ethernet) { if (p3000_debug != on_off || p3000_debug_ethernet != debug_ethernet) { const char *s = on_off ? "on" : "off"; const char *d = debug_ethernet ? "Ethernet" : "WiFi"; SERIAL_PRINTF("p3000_debug=%s (%s)\r\n", s, d); // Once for the current debug destination p3000_debug = on_off; if (!p3000_debug) { // Restore verbose and trace_devs to their pre-p3000 values: if (p3000_orig_verbose != -1) { verbose = p3000_orig_verbose; p3000_orig_verbose = -1; } if (p3000_orig_trace_dev1 != -1) { trace_dev1 = p3000_orig_trace_dev1; trace_dev2 = p3000_orig_trace_dev2; p3000_orig_trace_dev1 = -1; p3000_orig_trace_dev2 = -1; } } #if ETHERNET_ENABLED p3000_debug_ethernet = ethernet_detected && debug_ethernet; #endif SERIAL_PRINTF("p3000_debug=%s (%s)\r\n", s, d); // Again for the new debug destination } } static const char *get_connection_type (void) { const char *connection; if (bt_connected) connection = "BT"; else if (musb_connected) connection = "USB"; else if (w2000.connected()) connection = "WiFi"; #if ETHERNET_ENABLED else if (ethernet_detected && e2000.connected()) connection = "Eth"; #endif else if (w3000.connected()) connection = "WiF3"; #if ETHERNET_ENABLED else if (ethernet_detected && e3000.connected()) connection = "Eth3"; #endif else connection = "Not"; return connection; } static void nvram_get_all (void (*writep)(const char *)) { // Show all variables/values, sorted by name: bool wrote[NVRAM_COUNT] = {false,}; for (int k = 0; nvram_vars[k]; ++k) { int best = -1; for (int i = 0; nvram_vars[i]; ++i) { if (!wrote[i]) { const char *vname = nvram_vars[i]; if (!vname || !vname[0] || 0 == strncmp(vname, RESERVED, strlen(RESERVED))) wrote[i] = true; else if (best == -1 || strcmp(nvram_vars[best], nvram_vars[i]) > 0) best = i; } } if (best == -1) return; wrote[best] = true; writep(nvram_vars[best]); if (*(nvram_vals[best])) { writep(": "); writep(nvram_vals[best]); writep("\r\n"); } else { writep(":\r\n"); } } } static void status_command (void (*writep)(const char *)) { char tmp[128]; const char *connection = get_connection_type(); sprintf(tmp, "%s %s, ", HBG3_NAME, hbg3_version); if (musb_switch_timer) { strcat(tmp, "Reset Pending"); } else if (connection[0] == 'N') { strcat(tmp, musb_selected ? "MUSB Selected" : HBG3_NAME " Idle"); } else { strcat(tmp, connection); strcat(tmp, "-"); strcat(tmp, (connection[0] != 'B' || bt_active) ? (bt_handshake ? "Handshake" : "Connected") : "Session"); } strcat(tmp, "; "); writep(tmp); if (connection[0] == 'B') { sprintf(tmp, "Mode: Bluetooth %s, SSID: %s; ", (bt_protocol == bt_protocol_aux) ? "AUX" : "USB", hbg3_ssid); writep(tmp); } if (connection[0] == 'U') { sprintf(tmp, "Mode: USB %lu; ", (musb_uart->baudRate() / 10) * 10); // show 115201 as 115200. writep(tmp); } if (esp32_wifi_off) { writep((musb_selected && musb_rfkill) ? "Wireless is off\r\n" : "WiFi is off\r\n"); } else { const char *mode = (wifi_client_mode && !wifi_client_mode_override) ? "AccessPoint" : "DirectConnect"; if (WIFIRELAY_ENABLED && wifi_relay_mode) mode = "Relay"; sprintf(tmp, "WiFi=%s%s, SSID=%s, IP=%s\r\n", mode, wifi_client_mode_override ? "*" : "", current_ssid, current_ipaddr); writep(tmp); } #if TXQ_MONITOR_SIZE sprintf(tmp, "%s: pkts_in_use=%u pkts_max=%u/%u\r\n", auxbus.txq->name, auxbus.txq->pkts_in_use, auxbus.txq->pkts_max, AUXBUS_TXQ_SIZE); writep(tmp); if (auxrelay_detected) { sprintf(tmp, "%s: pkts_in_use=%u pkts_max=%u/%u\r\n", auxrelay.txq->name, auxrelay.txq->pkts_in_use, auxrelay.txq->pkts_max, AUXBUS_TXQ_SIZE); writep(tmp); } #endif /* TXQ_MONITOR_SIZE */ #if NTP_ENABLED SERIAL_PRINTF("gps_has_saved_location=%u fix_type=%u gps_get_lat_lng=%u\r\n", gps_has_saved_location(), gps_get_fix_type(), gps_get_lat_lng(false, NULL, NULL)); if (ntp_time_is_valid) SERIAL_PRINTF("NTP Time: %02u/%02u/%04u UTC %02u:%02u:%02u\r\n", ntp_time.day, ntp_time.month, ntp_time.year, ntp_time.hh, ntp_time.mm, ntp_time.ss); else SERIAL_PRINTLN("NTP Time: not available yet"); #else SERIAL_PRINTLN("NTP Time: not included"); #endif /* NTP_ENABLED */ } static char help_p3000_commands[] = "\r\n" HBG3_NAME " " VERSION " by Mark Lord: https://rtr.ca/hbg3/\r\n" "Command list:\r\n" "help -- print command list\r\n" "exit -- close this connection\r\n" "debug -- toggle HBG3 debug monitor mode on this connection\r\n" "reset -- reset/restart the ESP32\r\n" "version -- print version info\r\n" "load defaults -- reset NVRAM to factory defaults\r\n" "ota_update [path] -- fetch/install remote firmware update\r\n" "save -- save any NVRAM changes from the 'set' command\r\n" "status -- show some basic system information, including IP address\r\n" "get all -- list all NVRAM variables and values\r\n" "get var -- show value of a specific variable\r\n" "set var val -- change value of 'var' to 'val'\r\n" "get wlan.info -- show current wifi connection info\r\n" "get wlan.network.status -- show current wifi state\r\n" ; /* * handle_p3000_commands() is used for both p3000 and serial port commands, and by e3000 on the AIO. */ static bool handle_p3000_commands (char *cmd, void (*writep)(const char *)) { static bool wlan_ssid_passkey_updated = false; if (!p3000_debug && writep != SERIAL_WRITE) SERIAL_PRINTF("p3000: %s\r\n", cmd); if (!*cmd) { /* empty command: ignore it */ } else if (match_word("help", &cmd, false)) { *index(help_p3000_commands, 'v') = hbg3_version[0]; // hack for 'v'/'r writep(help_p3000_commands); if (p3000_debug || writep == SERIAL_WRITE) { writep("\r\nDebug commands:\r\n"); writep(help_serial_commands); } } else if (match_word("version", &cmd, true)) { writep(ADAPTER_VERSION); writep(hbg3_version); } else if (match_word("debug", &cmd, true)) { set_p3000_debug(!p3000_debug, (writep != w3000_write)); } else if (match_word("reset", &cmd, true)) { esp32_reset_pending = get_timeout(200); } else if (match_word("load defaults", &cmd, true)) { nvram_load_defaults(); writep("Loaded defaults"); wlan_ssid_passkey_updated = false; #if OTA_ENABLED } else if (match_word("ota_update", &cmd, false)) { if (*cmd) { ota_ignore_version = true; strcpy(ota_update_path, cmd); } if (ota_partition_size_okay) ota_fetch_update(); else writep("Wrong Parition Size for OTA"); #endif /* OTA_ENABLED */ } else if (match_word("save", &cmd, true)) { nvram_save(); writep("Success"); if (wlan_ssid_passkey_updated) { wlan_ssid_passkey_updated = false; wifi_client_mode_override = false; // enable WiFi to connect with new parameters } #if EMULATE_GPS } else if (match_word("fakegps", &cmd, false)) { fakegps_command(cmd); #endif /* EMULATE_GPS */ } else if (match_word("status", &cmd, true)) { // Show similar information to what OLED shows. status_command(writep); return true; } else if (match_word("get all", &cmd, true)) { nvram_get_all(writep); return true; } else if ((cmd[0] == 'g' || cmd[0] == 's') && cmd[1] == 'e' && cmd[2] == 't' && cmd[3] == ' ' && cmd[4]) { char *c = cmd + 4; if (cmd[0] == 'g') { if (0 == strcmp(c, "wlan.network.status")) { writep(wifi_client_mode ? "2" : "0"); writep("\r\n"); return true; } if (0 == strcmp(c, "wlan.mac")) { char mac[20]; sprintf(mac, "%02X:%02X:%02X:%02X:%02X:%02X\r\n", wifi_mac_addr[0], wifi_mac_addr[1], wifi_mac_addr[2], wifi_mac_addr[3], wifi_mac_addr[4], wifi_mac_addr[5]); writep(mac); return true; } if (0 == strcmp(c, "wlan.info")) { char tmp[128]; const char *ssid = nvram_get_val("wlan.ssid"); uint8_t bssid[6] = {0,0,0,0,0,0}, *b = bssid; if (wifi_client_mode && WiFi.isConnected()) b = WiFi.BSSID(); sprintf(tmp, "state: %s\r\nSSID: %s\r\nBSSID: %02x:%02x:%02x:%02x:%02x:%02x\r\nchannel: 0\r\ndatarate: 0\r\n", wifi_client_mode ? "up" : "down", ssid, b[0], b[1], b[2], b[3], b[4], b[5]); writep(tmp); return true; } } int x = nvram_find_var(c); if (x >= 0) { if (cmd[0] == 'g') { // "get" writep(nvram_vals[x]); } else { // "set" c += strlen(nvram_vars[x]); if (*c) ++c; const char *var = nvram_vars[x]; byte quoted = (*c == '"'); c += quoted; if (strlen(c) < NVRAM_VALS_MAX_LEN) { char *v = nvram_vals[x]; memset(v, 0, sizeof(nvram_vals[x])); while (*c && (!quoted || *c != '"')) *v++ = *c++; *v = '\0'; writep("Set OK"); if (0 == strcmp(var, "wlan.ssid") || 0 == strcmp(var, "wlan.passkey")) wlan_ssid_passkey_updated = true; #if EMULATE_FOCUS else if (0 == strcmp(var, "focus.microsteps")) stepdir_set_microsteps(atoi(c)); #endif /* EMULATE_FOCUS */ } } } } else { return false; // command not recognized } writep("\r\n"); return true; } static bool p3000_decoder (char b, void (*writep)(const char *), void (*flushp)(void)) { static char cmd[64]; static byte cx = 0; if (writep == NULL || flushp == NULL) { cx = 0; return true; } if (cx == 0) memset(cmd, 0, sizeof(cmd)); if ((cx < sizeof(cmd) - 1) && b != '\r' && b != '\n') cmd[cx++] = b; if (b != '\n') return false; cx = 0; char *cmdp = cmd; if (match_word("exit", &cmdp, true)) return true; // close connection if (SSSWI_HACKING && !p3000_debug) set_p3000_debug(true, false); if (!handle_p3000_commands(cmd, writep) && !(p3000_debug && handle_serial_commands(cmd))) writep("Unknown command\r\n"); else if (esp32_reset_pending) return true; writep("> "); flushp(); return false; } static inline void wifi_got_disconnected (bool *connected, const char *name) { *connected = false; p2000_timeout = P2000_NORMAL_TIMEOUT; init_rxbuf(&w2000_rxbuf, "w2000_rx"); SERIAL_PRINTF("%09lu %s Disconnected\r\n", millis(), name); } // Common code for w2000_loop() and w3000_loop(): static bool wifi_port_is_connected (WiFiClient client, bool *connected, void (*poll_for_connection)(), const char *name) { if (esp32_wifi_off) { if (*connected) wifi_got_disconnected(connected, name); } else if (client.connected()) { if (!*connected) { *connected = true; SERIAL_PRINTF("%09lu %s Connected\r\n", millis(), name); } } else { if (*connected) wifi_got_disconnected(connected, name); poll_for_connection(); } return *connected; } static void poll_for_new_w3000_connection (void) { w3000 = W3000.available(); if (w3000 && w3000.connected()) { (void)p3000_decoder(0, NULL, NULL); // reset the decoder w3000.write("> "); } } static void w3000_loop (void) { static bool connected = false; if (!wifi_port_is_connected(w3000, &connected, poll_for_new_w3000_connection, "w3000")) { if (!p3000_debug_ethernet) set_p3000_debug(false, false); } else { while (w3000.available()) { char b = w3000.read(); if (p3000_decoder(b, w3000_write, w3000_flush)) { w3000.stop(); } } } } static void poll_for_new_w2000_connection (void) { static long next_bcast = 0, next_msg = 0; if (WIFIRELAY_ENABLED && wifi_relay_mode) w2000.connect("1.2.3.4", 2000); else w2000 = W2000.available(); if (w2000.connected()) { p2000_timeout = P2000_NORMAL_TIMEOUT; init_rxbuf(&w2000_rxbuf, "w2000_rx"); /* Enable KEEPALIVE messages */ int flags = 1; w2000.setSocketOption(SO_KEEPALIVE, (char *)&flags, sizeof(flags)); // boolean, turns on keepalives flags = 3000; w2000.setOption(TCP_KEEPALIVE, &flags); // set keepalives to 3000msecs interval next_bcast = next_msg = 0; } else if (bt_connected) { next_bcast = next_msg = 0; } else { // SkyPortal app looks for these advertisement broadcasts: if (!next_bcast || time_after(millis(), next_bcast)) { next_bcast = get_timeout(1000); if (0) { // This message is annoying and no longer needed. if (!verbose) { next_msg = 0; } else if (!next_msg || time_after(millis(), next_msg)) { next_msg = get_timeout(10 * 1000); // Print message once every 10-seconds if (!auxbus_raw_tracing) SERIAL_PRINTLN("UDP Broadcast"); } } static AsyncUDP udp_bcast; udp_bcast.broadcastTo((uint8_t *)advertisement, strlen(advertisement), (uint16_t)55555); } } } static void w2000_loop (void) { static long w2000_timeout = 0; static bool connected = false; bool old_connected = connected; if (!wifi_port_is_connected(w2000, &connected, poll_for_new_w2000_connection, "w2000")) { w2000_timeout = 0; } else { if (w2000.available()) { do { (void)packet_decoder(&w2000_rxbuf, w2000.read()); } while (w2000.available()); w2000_timeout = 0; } else if (!w2000_timeout) { w2000_timeout = get_timeout(wifi_relay_mode ? (5 * 1000) : p2000_timeout); } else if (time_after(millis(), w2000_timeout)) { w2000_timeout = 0; if (WIFIRELAY_ENABLED && wifi_relay_mode) { /* dummy keep-alive message */ AUXBUS_SEND_MSG(DEV_ESP32, DEV_DUMMY, DEV_GET_VERSION); } else { w2000.stop(); SERIAL_PRINTF("%09lu w2000 Timed-out\r\n", millis()); } } } } static void auxtest_loop (void) { if (!auxtest_timer) { if (auxtest_remaining > 0) { if (millis() > 15000) auxtest_remaining = 0; else auxtest_timer = get_timeout(1000); } } else if (time_after(millis(), auxtest_timer)) { auxtest_timer = 0; if (auxtest_remaining > 0) auxtest_send_msg(); } } #if defined(AIO_H) #include "aio.h" #endif #if defined(ETH_H) #include "eth.h" #endif static void non_wifi_loop (void) { if (nvram_delayed_save && time_after(millis(), nvram_delayed_save)) nvram_save(); if (blue_led_timer && time_after(millis(), blue_led_timer)) set_blue_led(LED_OFF); // Also clears the timer if (SerialDebug) serial_loop(); auxtest_loop(); bus_loop(&auxbus); if (bt_protocol == bt_protocol_aux) bt_aux_loop(); if (auxrelay_detected) bus_loop(&auxrelay); musb_loop(); #if EMULATE_GPS gps_loop(); #endif /* EMULATE_GPS */ #if EMULATE_FOCUS stepper_loop(); #endif /* EMULATE_FOCUS */ #if EMULATE_DEW if (dew_enabled) dew_loop(); #endif #if OLED_ENABLED oled_loop(); #endif #if NCHUCK_ENABLED nchuck_loop(); #endif #ifdef BT_USB_H bt_usb_loop(); #endif #if ETHERNET_ENABLED if (ethernet_detected) ethernet_loop(); #endif monitor_musb_switch(); if (esp32_reset_pending && time_after(millis(), esp32_reset_pending)) ESP.restart(); } void loop (void) { #if NTP_ENABLED if (wifi_client_mode && !wifi_client_mode_override) ntp_loop(); #endif /* NTP_ENABLED */ non_wifi_loop(); manage_wifi_onoff(); w2000_loop(); w3000_loop(); } static void detect_simple_mode (void) { #ifndef AIO_H /* * Simple mode is selected with a 50K-pulldown and 100K-pullup on the ESP32_WIFI_MODE_PIN (no switch wired). * Normally this would read 2200+ with nothing, or 0 (zero) when a switch is activated. * With the resistors and no switch, it reads around 1280, give or take. */ int val = analogRead(ESP32_WIFI_MODE_PIN); //SERIAL_PRINTF("%s: analogRead(ESP32_WIFI_MODE_PIN): %d\r\n", __func__, val); simple_mode = (val > 1000 && val < 1400); #endif } void setup (void) { // Get the buses configured first: bus_init(&auxbus, false, "auxbus", &auxbus_uart, AUXBUS_RX_PIN, AUXBUS_TX_PIN, AUXBUS_BUSYIN_PIN, AUXBUS_BUSYOUT_PIN); auxrelay_detected = bus_init(&auxrelay, true, "relay", &auxrelay_uart, AUXRELAY_RX_PIN, AUXRELAY_TX_PIN, AUXRELAY_BUSYIN_PIN, AUXRELAY_BUSYOUT_PIN); DEV_ESP32 = auxrelay_detected ? DEV_RELAY : DEV_HBG3; hbg3_version[0] = auxrelay_detected ? 'r' : 'v'; Wire.begin(); // Prepare the I2C bus for use. Needed early for OLED and AIO, then for FRAM and NCHUCK. Wire.setTimeOut(500); // This supposedly helps mitigate I2C vs. WiFi issues #if OLED_ENABLED oled_setup(); #endif fram_setup(); #ifdef AIO_H aio_setup_early(); // needed here for READ_MUSB_SELECT_PIN(), which accesses a USB chip GPIO. #endif // Now sort out the other uart assignments: if (MUSB_SELECT_PIN != -1) { pinMode(MUSB_SELECT_PIN, INPUT_PULLUP); delay(5); } hwserial0->end(); musb_selected = musb_switch_is_on = (READ_MUSB_SELECT_PIN() == LOW); if (!musb_selected) { musb_uart = NULL; SerialDebug = hwserial0; } else { if (EMULATE_GPS || auxrelay_detected) { SerialDebug = NULL; musb_uart = hwserial0; } else { SerialDebug = hwserial0; musb_uart = &hwserial1; } init_rxbuf(&musb_rxbuf, "musb_rx"); musb_uart->begin(19200, SERIAL_8N1, USB_RX_PIN, USB_TX_PIN); musb_uart->setDebugOutput(false); // Try and prevent ESP32 system debug messages after boot musb_uart->flush(false); } if (SerialDebug) { #define SERIAL_DEBUG_BAUD 115200 SerialDebug->begin(SERIAL_DEBUG_BAUD, SERIAL_8N1, USB_RX_PIN, USB_TX_PIN); // The USB pins are the main debug/command port: SerialDebug->flush(false); } detect_simple_mode(); if (ESP32_WIFI_MODE_PIN != -1) pinMode(ESP32_WIFI_MODE_PIN, INPUT_PULLUP); pinMode(BLUE_LED_PIN, OUTPUT); set_blue_led(LED_ON); WiFi.macAddress(wifi_mac_addr); // read WiFi MAC into wifi_mac_addr[6], BEFORE calling nvram_restore(). nvram_restore(); // Also calls oled_setup() again if non-default oled_type set_blue_led(LED_OFF); SERIAL_PRINTF(HBG3_NAME "-%s: AUXRELAY=%u GPS=%u BLOCK_CFM=%u NCHUCK=%u FOCUS=%u OLED=%u DEW=%u FRAM=%u REVERSED=%u ETH=%s\r\n", hbg3_version, auxrelay_detected, EMULATE_GPS, BLOCK_CFM_CONNECTIONS, NCHUCK_ENABLED, EMULATE_FOCUS, OLED_ENABLED, EMULATE_DEW, fram_detected, mount_reversed_alt, ETH_TYPE); init_rxbuf(&w2000_rxbuf, "w2000_rx"); memset(spaces, ' ', sizeof(spaces) - 1); #if EMULATE_GPS gps_using_swserial = musb_selected || auxrelay_detected; SERIAL_PRINTF("GPS uart is %s\r\n", GPS_UART_NAME()); GPS_UART_BEGIN(9600); gps.invalidate(); // Tell GPS it has no valid data #endif /* EMULATE_GPS */ SERIAL_PRINTF("OLED %sdetected\r\n", oled_detected ? "" : "not "); // Pre-populate routing table with these frequently used entries: routing_add_dev(DEV_AZM, auxbus.rxbuf); routing_add_dev(DEV_ALT, auxbus.rxbuf); routing_add_dev(DEV_ESP32, auxbus.rxbuf); routing_add_dev(DEV_DUMMY, auxbus.rxbuf); #if NCHUCK_ENABLED nchuck_detect(false); // Do this part earlier than the rest so user can touch thumbstick sooner #endif #if EMULATE_FOCUS stepper_setup(); #endif #if ETHERNET_ENABLED #if EMULATE_FOCUS if (stepper_driver == STEPPER_DRIVER_NONE) // Stepper motors use the SPI pins, so cannot have Ethernet as well as stepper. #endif ethernet_setup(); #endif /* ETHERNET_ENABLED */ #if EMULATE_DEW dew_setup(); // MUST come after stepper_setup() and ethernet_setup(), to resolve pin conflicts. #endif bt_setup(); WiFi.persistent(false); if (!simple_mode && is_wifi_client_mode_requested()) { // Workaround for client mode not always working at start-up: wifi_begin_server_mode(); delay(50); esp32_wifi_off = false; set_esp32_wifi_off(true); } #if NCHUCK_ENABLED if (simple_mode) { auxtest_remaining = 0; nchuck_focus_only = ('0' != *nvram_get_val("focus.only")); // default to focus-only mode SERIAL_PRINTF("\r\nSimple mode selected: disabling WiFi+BT+auxtest; focus_only=%u\r\n", nchuck_focus_only); } else { nchuck_focus_only = ('1' == *nvram_get_val("focus.only")); // default to focus+slew mode } if (nchuck_focus_only) fm_request_position(__func__, false); #endif }