From 3601fa63d8557e6180f40ee725d6fe22282e19a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 31 Jul 2022 23:25:40 +0200 Subject: [PATCH] [examples] Add PinScan example --- SUMMARY.md | 2 + examples/PinScan/.gitignore | 1 + examples/PinScan/README.md | 116 ++++++++++++++++++ examples/PinScan/platformio.ini | 12 ++ examples/PinScan/src/analog.cpp | 70 +++++++++++ examples/PinScan/src/digital.cpp | 200 +++++++++++++++++++++++++++++++ examples/PinScan/src/help.cpp | 65 ++++++++++ examples/PinScan/src/main.cpp | 185 ++++++++++++++++++++++++++++ examples/PinScan/src/main.h | 39 ++++++ examples/PinScan/src/ui.cpp | 37 ++++++ 10 files changed, 727 insertions(+) create mode 100644 examples/PinScan/.gitignore create mode 100644 examples/PinScan/README.md create mode 100644 examples/PinScan/platformio.ini create mode 100644 examples/PinScan/src/analog.cpp create mode 100644 examples/PinScan/src/digital.cpp create mode 100644 examples/PinScan/src/help.cpp create mode 100644 examples/PinScan/src/main.cpp create mode 100644 examples/PinScan/src/main.h create mode 100644 examples/PinScan/src/ui.cpp diff --git a/SUMMARY.md b/SUMMARY.md index 6da5d2f..8927186 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -3,6 +3,8 @@ * [Start here](docs/getting-started/README.md) * [Uploading](docs/getting-started/uploading.md) * [Options & config](docs/reference/config.md) + * Examples + * [PinScan](examples/PinScan/README.md) * [ESPHome port](docs/projects/esphome.md) * [đŸ’» Boards & CPU list](docs/status/supported.md) * [✔ Implementation status](docs/status/arduino.md) diff --git a/examples/PinScan/.gitignore b/examples/PinScan/.gitignore new file mode 100644 index 0000000..03f4a3c --- /dev/null +++ b/examples/PinScan/.gitignore @@ -0,0 +1 @@ +.pio diff --git a/examples/PinScan/README.md b/examples/PinScan/README.md new file mode 100644 index 0000000..444ea6a --- /dev/null +++ b/examples/PinScan/README.md @@ -0,0 +1,116 @@ +# PinScan + +This example allows to quickly check all digital/analog pins of an IoT device. + +By using a simple TUI (text user interface), you can check which I/O pins correspond to i.e. button presses, as well as write to one of the pins to see which LED lights up. + +!!! warning + Messing with pins in a device can potentially damage some parts of it. Be careful when writing voltages to digital and PWM pins. + +Uploading the example and opening up a terminal (e.g. PuTTY) presents this menu: +``` +LibreTuya v0.8.0, PinScan v1.0 +Board: cb2s +I/O count: 11 +Digital I/O count: 11 +Analog input count: 1 +-------- UART2 -------- +Commands: + d - Check digital pins + a - Check analog pins + s - Select UART port + t - Toggle ANSI control codes + r - Reboot (for uploading) + q - Go back to menu, at any time + ? - Print help text, also for subcommands +``` + +The interface expects one-letter commands, without confirmation by `Enter`. The only part which expects an `Enter` keypress is inputting pin numbers and UART port numbers. + +Pressing `q` at any time goes back to the main menu, terminating the current process. + +!!! note + PinScan works best with a terminal supporting ANSI escape codes (PuTTY does), but this behavior can be disabled using `t`. + +Switching to another UART port is possible (for example if the default port is on the pins you want to check) using `s` command. Do not change the port after using any I/O commands, as it won't work; reboot it using `r` before. + +By setting `USE_WIFI` in `main.h` to 1, a Telnet server is enabled on port 23. This allows to test I/O pins without having physical, wired access to the device (i.e. using OTA). Make sure to specify correct WiFi credentials. + +!!! hint + If your board isn't supported by LT yet, use one of the generic boards. + + If your board doesn't even have a known pinout, use `d`/`s` commands of PinScan to ease the mapping of all board pins. + +## Digital pins + +``` +Digital I/O +-------- UART2 -------- +Commands: + r - Realtime readout of all pins + o - Read one pin continuously + s - Manual Scan - toggle each pin + h - Write HIGH to a pin + l - Write LOW to a pin + p - Output using pull up/down (default) + w - Output using write low/high (less safe) +``` + +### Realtime readout of all pins + +``` + D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 + LO LO HI HI HI HI LO LO -- -- LO +``` + +Screen contents will update when voltage on one of the pins changes. Pins marked with `--` mean the currently used UART port (which can be changed using `s` command; after reboot). + +!!! tldr + Try pressing a button to see which pin changes. + +### Read one pin continuously + +Enter the pin number, it will be probed until you press `q`. + +### Manual Scan - toggle each pin + +``` + D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 + HI LO LO LO LO LO LO LO -- -- LO +``` + +A pin will be toggled every 500ms, starting with D0. Type `n` to move to the next pin. + +!!! tldr + Go through the pins to see which lights up an LED. + +### Write HIGH/LOW to a pin + +Self-explanatory. + +### Output using pull/write + +Outputs can be toggled by using internal pull-up/pull-down resistors, or by simply writing a voltage. Writing is more current-efficient, but is also less safe if something else supplies different voltage to the pin. + +This affects scan and write high/low commands. + +!!! tldr + Use `write` output mode (carefully) if there's an LED which doesn't light up with default pull mode. + +## Analog pins + +``` +Analog inputs +-------- UART2 -------- +Commands: + r - Realtime readout of all pins + o - Read one pin once +``` + +### Realtime readout of all pins + +Read voltage (in millivolts) on all available analog pins, until `q` is pressed. + +### Read one pin once + +No need to explain. diff --git a/examples/PinScan/platformio.ini b/examples/PinScan/platformio.ini new file mode 100644 index 0000000..35a5baa --- /dev/null +++ b/examples/PinScan/platformio.ini @@ -0,0 +1,12 @@ +[env] +platform = libretuya +framework = arduino + +[env:bk7231n] +board = generic-bk7231n-qfn32-tuya + +[env:bk7231t] +board = generic-bk7231t-qfn32-tuya + +[env:rtl8710b] +board = generic-rtl8710bn-2mb-788k diff --git a/examples/PinScan/src/analog.cpp b/examples/PinScan/src/analog.cpp new file mode 100644 index 0000000..b1d5922 --- /dev/null +++ b/examples/PinScan/src/analog.cpp @@ -0,0 +1,70 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-07-31. */ + +#include "main.h" + +static unsigned long last = 0; + +static void readAnalog(uint8_t number, pin_size_t pin) { + stream->write("A"); + stream->write('0' + number); + stream->print(": "); + stream->print(analogReadVoltage(pin)); + stream->print(" mV\t"); +} + +void runAnalog() { + if (mode[1] == '\0') + return; + int pin = 0; + switch (mode[1]) { + case '?': + printHelp('a'); + break; + + case 'r': + if (millis() - last < DELAY_READ) + return; + last = millis(); + printAnsi(ANSI_LINE_START); +#ifdef PIN_A0 + readAnalog(0, PIN_A0); +#endif +#ifdef PIN_A1 + readAnalog(1, PIN_A1); +#endif +#ifdef PIN_A2 + readAnalog(2, PIN_A2); +#endif + if (!ansi) + stream->println(); + return; + + case 'o': + pin = inputPin(); + // clang-format off +#ifdef PIN_A0 + if (pin == 0) { + readAnalog(0, PIN_A0); + } else +#endif +#ifdef PIN_A1 + if (pin == 1) { + readAnalog(0, PIN_A0); + } else +#endif +#ifdef PIN_A2 + if (pin == 2) { + readAnalog(2, PIN_A2); + } else +#endif + // clang-format on + { + stream->print("Pin unavailable"); + } + stream->println(); + mode[1] = '?'; + ansiSkipErase = true; + return; + } + mode[1] = '\0'; +} diff --git a/examples/PinScan/src/digital.cpp b/examples/PinScan/src/digital.cpp new file mode 100644 index 0000000..1b01b85 --- /dev/null +++ b/examples/PinScan/src/digital.cpp @@ -0,0 +1,200 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-07-31. */ + +#include "main.h" + +#define PULL 0 +#define WRITE 1 + +static bool pins[NUM_DIGITAL_PINS] = {}; +static bool used[NUM_DIGITAL_PINS] = {}; +static bool active = false; +static int pin = -1; + +static unsigned long last = 0; +static bool outputMode = PULL; + +static void printPin(bool state) { + if (ansi) { + stream->print(state ? (ANSI_RED "HI" ANSI_RESET) : (ANSI_BLUE "LO" ANSI_RESET)); + } else { + stream->print(state ? "HI" : "LO"); + } +} + +static void printState() { + printAnsi(ANSI_TWO_LINES); + printAnsi(ANSI_ERASE_LINE); + for (pin_size_t i = 0; i < NUM_DIGITAL_PINS; i++) { + if (i < 10) + stream->write(' '); + stream->write('D'); + stream->print(i); + stream->write(' '); + } + stream->println(); + printAnsi(ANSI_ERASE_LINE); + for (pin_size_t i = 0; i < NUM_DIGITAL_PINS; i++) { + stream->write(' '); + if (used[i]) { + printPin(pins[i]); + } else { + stream->print("--"); + } + stream->write(' '); + } + stream->println(); +} + +static void irqHandler(void *ptr) { + bool *pin = (bool *)ptr; + pin_size_t i = pin - pins; + pins[i] = digitalRead(i); + // change interrupt level according to current state + PinStatus status = pins[i] ? FALLING : RISING; + attachInterruptParam(i, irqHandler, status, ptr); + printState(); +} + +static void digitalAllIrq() { + for (pin_size_t i = 0; i < NUM_DIGITAL_PINS; i++) { + if (pinSkip[0] == i || pinSkip[1] == i) { + used[i] = false; + continue; + } + pinMode(i, INPUT); + // choose interrupt level according to current state + PinStatus status = digitalRead(i) ? FALLING : RISING; + attachInterruptParam(i, irqHandler, status, pins + i); + used[i] = true; + pins[i] = digitalRead(i); + } + active = true; +} + +static void digitalOut(pin_size_t pin, PinStatus state) { + if (outputMode == PULL) { + pinMode(pin, state ? INPUT_PULLUP : INPUT_PULLDOWN); + } else { + pinMode(pin, OUTPUT); + digitalWrite(pin, state); + } + pins[pin] = state; +} + +static int digitalAllLow() { + int first = -1; + for (pin_size_t i = 0; i < NUM_DIGITAL_PINS; i++) { + if (pinSkip[0] == i || pinSkip[1] == i) { + used[i] = false; + continue; + } + if (first == -1) + first = i; + digitalOut(i, LOW); + used[i] = true; + } + active = true; + return first; +} + +void digitalDetach() { + outputMode = PULL; + if (!active) + return; + for (pin_size_t i = 0; i < NUM_DIGITAL_PINS; i++) { + if (pinSkip[0] == i || pinSkip[1] == i) { + continue; + } + if (used[i]) + detachInterrupt(i); + used[i] = false; + pinMode(i, INPUT_PULLDOWN); + } + active = false; + pin = -1; +} + +void runDigital() { + if (mode[1] == '\0') + return; + switch (mode[1]) { + case '?': + printHelp('d'); + pin = -1; + break; + + case 'p': + outputMode = PULL; + stream->println("Will pull outputs UP/DOWN"); + mode[1] = '?'; + ansiSkipErase = true; + return; + case 'w': + outputMode = WRITE; + stream->println("Will write LOW/HIGH to outputs"); + mode[1] = '?'; + ansiSkipErase = true; + return; + + case 'r': + if (!active) { + digitalAllIrq(); + if (ansi) + printHelp('d'); + printAnsi("\n\n"); // reserve two lines for readouts + printState(); + } + return; + + case 'o': + if (pin == -1) { + pin = inputPin(); + pinMode(pin, INPUT); + } + printAnsi(ANSI_LINE_START); + stream->print("Pin D"); + stream->print(pin); + stream->print(" state: "); + printPin(digitalRead(pin)); + if (!ansi) + stream->println(); + return; + + case 's': + if (pin == -1) { + // choose the first pin + pin = digitalAllLow(); + if (ansi) + printHelp('d'); + printAnsi("\n\n"); // reserve two lines for readouts + printState(); + } + if (mode[2] == 'n') { + // go to next pin; leave the current as LOW + digitalOut(pin, LOW); + do { + if (++pin >= NUM_DIGITAL_PINS) + pin = 0; + } while (used[pin] == false); + printState(); + } + mode[2] = '\0'; + // toggle the pin every 500ms + if (millis() - last < 500) + return; + last = millis(); + digitalOut(pin, (PinStatus)!pins[pin]); + printState(); + return; + + case 'h': + case 'l': + pin = inputPin(); + digitalOut(pin, mode[1] == 'h' ? HIGH : LOW); + stream->println("OK"); + mode[1] = '?'; + ansiSkipErase = true; + return; + } + mode[1] = '\0'; +} diff --git a/examples/PinScan/src/help.cpp b/examples/PinScan/src/help.cpp new file mode 100644 index 0000000..7d301e2 --- /dev/null +++ b/examples/PinScan/src/help.cpp @@ -0,0 +1,65 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-07-31. */ + +#include "main.h" + +void printHelp(uint8_t mode) { + printAnsiErase(); + switch (mode) { + case '\0': + stream->setTimeout(10000); + stream->println("LibreTuya v" LT_VERSION_STR ", PinScan v" EXAMPLE_VER); + stream->println("Board: " LT_BOARD_STR); + stream->print("I/O count: "); + stream->println(PINS_COUNT); + stream->print("Digital I/O count: "); + stream->println(NUM_DIGITAL_PINS); + stream->print("Analog input count: "); + stream->println(NUM_ANALOG_INPUTS); + break; + case 'd': + stream->println("Digital I/O"); + break; + case 'a': + stream->println("Analog inputs"); + break; + } + line(); + stream->println("Commands:"); + switch (mode) { + case '\0': + // clang-format off + stream->println( + TAB "d - Check digital pins" EOL + TAB "a - Check analog pins" EOL + // TAB "p - Check PWM outputs" EOL + TAB "s - Select UART port" EOL + TAB "t - Toggle ANSI control codes" EOL + TAB "r - Reboot (for uploading)" EOL + TAB "q - Go back to menu, at any time" EOL + TAB "? - Print help text, also for subcommands" EOL + ); + // clang-format on + break; + case 'd': + // clang-format off + stream->println( + TAB "r - Realtime readout of all pins" EOL + TAB "o - Read one pin continuously" EOL + TAB "s - Manual Scan - toggle each pin" EOL + TAB "h - Write HIGH to a pin" EOL + TAB "l - Write LOW to a pin" EOL + TAB "p - Output using pull up/down (default)" EOL + TAB "w - Output using write low/high (less safe)" EOL + ); + // clang-format on + break; + case 'a': + // clang-format off + stream->println( + TAB "r - Realtime readout of all pins" EOL + TAB "o - Read one pin once" EOL + ); + // clang-format on + break; + } +} diff --git a/examples/PinScan/src/main.cpp b/examples/PinScan/src/main.cpp new file mode 100644 index 0000000..cb7d503 --- /dev/null +++ b/examples/PinScan/src/main.cpp @@ -0,0 +1,185 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-07-31. */ + +#include "main.h" + +#if USE_WIFI +#include +#include + +static WiFiMulti wm; +static WiFiServer server(23); +static WiFiClient client; +#endif + +Stream *stream = NULL; +uint8_t mode[] = {'q', '\0', '\0', '\0'}; +pin_size_t pinSkip[2]; +int output = -1; + +static void parseMode(); + +void setup() { + Serial.begin(115200); +#if USE_WIFI + wm.addAP(WIFI_SSID, WIFI_PASS); + while (wm.run() != WL_CONNECTED) { + Serial.println("WiFi connection failed, retrying in 5s"); + delay(5000); + } + server.begin(); + server.setNoDelay(true); +#else + stream = &Serial; +#if LT_UART_DEFAULT_SERIAL == 0 + output = 0; + pinSkip[0] = PIN_SERIAL0_TX; + pinSkip[1] = PIN_SERIAL0_RX; +#elif LT_UART_DEFAULT_SERIAL == 1 + output = 1; + pinSkip[0] = PIN_SERIAL1_TX; + pinSkip[1] = PIN_SERIAL1_RX; +#elif LT_UART_DEFAULT_SERIAL == 2 + output = 2; + pinSkip[0] = PIN_SERIAL2_TX; + pinSkip[1] = PIN_SERIAL2_RX; +#endif +#endif +} + +void loop() { +#if USE_WIFI + while (wm.run() != WL_CONNECTED) { + Serial.println("WiFi connection dropped, retrying in 5s"); + delay(5000); + } + if (!stream) { + client = server.accept(); + delay(500); + if (client) { + stream = &client; + printHelp(); + } + } else if (!client.connected()) { + stream = NULL; + client.stop(); + client = WiFiClient(); + } +#endif + + if (stream && stream->available()) { + // put the char in first free mode item + bool modeSet = false; + for (uint8_t i = 0; i < sizeof(mode) / sizeof(uint8_t); i++) { + if (mode[i] == '\0') { + mode[i] = stream->read(); + // make the next mode show help screen + if (i < sizeof(mode) - 1) { + mode[++i] = '?'; + } + // clear the following items, if any + while (i < sizeof(mode) - 1) { + mode[++i] = '\0'; + } + modeSet = true; + break; + } + } + if (!modeSet) { + // no free mode items, reset everything + memset(mode, '\0', sizeof(mode)); + while (stream->available()) { + stream->read(); + } + } + // LT_I("New mode: %s", mode); + } + + // check for any 'q' keypresses + for (uint8_t i = 0; i < sizeof(mode) / sizeof(uint8_t); i++) { + if (mode[i] == 'q') { + if (mode[0] == 'd') + digitalDetach(); + memset(mode, '\0', sizeof(mode)); + mode[0] = '?'; + break; + } + } + + parseMode(); +} + +static void parseMode() { + if (mode[0] == '\0') + return; + int serial = 0; + switch (mode[0]) { + case '?': + printHelp(); + break; + case 'd': + runDigital(); + return; + case 'a': + runAnalog(); + return; + case 'r': + LT.restart(); + while (1) {} + return; + +#if !USE_WIFI + case 's': + stream->print("Choose output UART: "); + serial = stream->parseInt(); + stream->println(); + // clang-format off +#ifdef PIN_SERIAL0_TX + if (serial == 0) { + stream = &Serial0; + pinSkip[0] = PIN_SERIAL0_TX; + pinSkip[1] = PIN_SERIAL0_RX; + } else +#endif +#ifdef PIN_SERIAL1_TX + if (serial == 1) { + stream = &Serial1; + pinSkip[0] = PIN_SERIAL1_TX; + pinSkip[1] = PIN_SERIAL1_RX; + } else +#endif +#ifdef PIN_SERIAL2_TX + if (serial == 2) { + stream = &Serial2; + pinSkip[0] = PIN_SERIAL2_TX; + pinSkip[1] = PIN_SERIAL2_RX; + } else +#endif + // clang-format on + { + stream->print("Port unavailable"); + break; + } + output = serial; + ((SerialClass *)stream)->begin(115200); + stream->println(); + mode[0] = '?'; + return; +#endif + + case 't': + ansi = !ansi; + stream->print("ANSI control codes "); + if (ansi) { + stream->println("enabled"); + mode[0] = '?'; + ansiSkipErase = true; + return; + } else { + stream->println("disabled"); + } + break; + default: + break; + } + mode[0] = '\0'; +} diff --git a/examples/PinScan/src/main.h b/examples/PinScan/src/main.h new file mode 100644 index 0000000..c053204 --- /dev/null +++ b/examples/PinScan/src/main.h @@ -0,0 +1,39 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-07-31. */ + +#include + +#define USE_WIFI 0 // set up a WiFi telnet server +#define DELAY_READ 100 // interval for realtime readouts + +#define WIFI_SSID "MySSID" +#define WIFI_PASS "Secr3tPa$$w0rd" + +#define EXAMPLE_VER "1.0" + +#define EOL "\r\n" +#define TAB "\t" + +#define ANSI_ERASE "\x1B[2J" +#define ANSI_HOME "\x1B[H" +#define ANSI_LINE_START "\x1B[0G" +#define ANSI_TWO_LINES "\x1B[2F" +#define ANSI_ERASE_LINE "\x1B[0K" +#define ANSI_BLUE "\x1B[0;34m" +#define ANSI_RED "\x1B[0;31m" +#define ANSI_RESET "\x1B[0m" + +extern Stream *stream; +extern uint8_t mode[]; +extern pin_size_t pinSkip[2]; +extern int output; +extern bool ansi; +extern bool ansiSkipErase; + +extern void printHelp(uint8_t mode = '\0'); +extern void printAnsi(const char *str); +extern void printAnsiErase(); +extern void line(); +extern int inputPin(); +extern void runAnalog(); +extern void runDigital(); +extern void digitalDetach(); diff --git a/examples/PinScan/src/ui.cpp b/examples/PinScan/src/ui.cpp new file mode 100644 index 0000000..8fb843c --- /dev/null +++ b/examples/PinScan/src/ui.cpp @@ -0,0 +1,37 @@ +/* Copyright (c) Kuba SzczodrzyƄski 2022-07-31. */ + +#include "main.h" + +bool ansi = true; +bool ansiSkipErase = false; + +void printAnsi(const char *str) { + if (ansi) + stream->print(str); +} + +void printAnsiErase() { + if (!ansiSkipErase) + printAnsi(ANSI_ERASE ANSI_HOME); + ansiSkipErase = false; +} + +void line() { + stream->print("--------"); + if (output == -1) { + stream->print("TELNET"); + } else { + stream->print(" UART"); + stream->print(output); + stream->write(' '); + } + stream->print("--------"); + stream->println(); +} + +int inputPin() { + stream->print("Enter pin number: "); + int pin = stream->parseInt(); + stream->println(); + return pin; +}