[examples] Add PinScan example

This commit is contained in:
Kuba Szczodrzyński
2022-07-31 23:25:40 +02:00
parent 5d00ddf516
commit 3601fa63d8
10 changed files with 727 additions and 0 deletions

View File

@@ -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)

1
examples/PinScan/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.pio

116
examples/PinScan/README.md Normal file
View File

@@ -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.

View File

@@ -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

View File

@@ -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';
}

View File

@@ -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';
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,185 @@
/* Copyright (c) Kuba Szczodrzyński 2022-07-31. */
#include "main.h"
#if USE_WIFI
#include <WiFi.h>
#include <WiFiMulti.h>
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';
}

View File

@@ -0,0 +1,39 @@
/* Copyright (c) Kuba Szczodrzyński 2022-07-31. */
#include <Arduino.h>
#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();

View File

@@ -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;
}